🔍 Add functionality for gamemaster search in composable
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { debounce } from '@/utilities/performance-utils.js';
|
||||
import { useSearchHistory } from '@/composables/useLocalStorage.js';
|
||||
|
||||
/**
|
||||
* useGamemasterSearch - Composable for managing gamemaster file search operations
|
||||
*
|
||||
* Handles:
|
||||
* - Search query state management
|
||||
* - Search results tracking (line numbers with matches)
|
||||
* - Result navigation (next/prev)
|
||||
* - Search history
|
||||
* - Fuzzy matching and regex pattern support
|
||||
* - Web worker integration for large file searches
|
||||
*
|
||||
* @param {Ref<string>} fileLines - Full file content split into lines
|
||||
* @param {Ref<Array>} displayLines - Currently displayed lines with metadata
|
||||
* @returns {Object} Search composable API
|
||||
*/
|
||||
export function useGamemasterSearch(fileLines, displayLines) {
|
||||
// Search state
|
||||
const searchQuery = ref('');
|
||||
const searchResults = ref([]);
|
||||
const currentResultIndex = ref(0);
|
||||
const isSearching = ref(false);
|
||||
const searchError = ref(null);
|
||||
|
||||
// Search history
|
||||
const searchHistory = useSearchHistory();
|
||||
|
||||
// Web worker for large file searches
|
||||
let searchWorker = null;
|
||||
let searchWorkerRequestId = 0;
|
||||
|
||||
/**
|
||||
* Initialize Web Worker for search operations
|
||||
* Prevents blocking UI on large files
|
||||
*/
|
||||
function initSearchWorker() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (searchWorker) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
searchWorker = new Worker('/workers/search-worker.js');
|
||||
|
||||
searchWorker.onmessage = event => {
|
||||
const { id, results, error } = event.data;
|
||||
|
||||
// Ignore stale results
|
||||
if (id !== searchWorkerRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
searchError.value = error;
|
||||
isSearching.value = false;
|
||||
} else {
|
||||
searchResults.value = results;
|
||||
currentResultIndex.value = 0;
|
||||
|
||||
// Update display lines with match indicators
|
||||
displayLines.value?.forEach(line => {
|
||||
line.hasMatch = results.includes(line.lineNumber - 1);
|
||||
});
|
||||
|
||||
isSearching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
searchWorker.onerror = error => {
|
||||
console.error('Search worker error:', error);
|
||||
searchError.value = error.message;
|
||||
isSearching.value = false;
|
||||
reject(error);
|
||||
};
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
searchError.value = error.message;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform search with debouncing to avoid excessive processing
|
||||
* Supports both simple text and regex patterns
|
||||
*/
|
||||
const performSearch = debounce(async () => {
|
||||
if (!searchQuery.value.trim()) {
|
||||
clearSearchResults();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
searchError.value = null;
|
||||
isSearching.value = true;
|
||||
|
||||
// Initialize worker if needed
|
||||
await initSearchWorker();
|
||||
|
||||
if (!searchWorker) {
|
||||
throw new Error('Search worker not available');
|
||||
}
|
||||
|
||||
// Send search to worker
|
||||
searchWorkerRequestId++;
|
||||
const searchTerm = searchQuery.value.toLowerCase();
|
||||
const plainLines = Array.from(fileLines.value);
|
||||
|
||||
searchWorker.postMessage({
|
||||
lines: plainLines,
|
||||
searchTerm: searchTerm,
|
||||
id: searchWorkerRequestId
|
||||
});
|
||||
|
||||
// Add to search history
|
||||
searchHistory.addToHistory(searchQuery.value);
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
searchError.value = error.message;
|
||||
isSearching.value = false;
|
||||
|
||||
// Fallback to synchronous search for small files
|
||||
if (fileLines.value.length < 5000) {
|
||||
performSynchronousSearch();
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
/**
|
||||
* Fallback synchronous search for small files
|
||||
* Used when Web Worker is unavailable
|
||||
*/
|
||||
function performSynchronousSearch() {
|
||||
const results = [];
|
||||
const searchTerm = searchQuery.value.toLowerCase();
|
||||
const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
||||
|
||||
try {
|
||||
// Try regex pattern first
|
||||
const regex = new RegExp(escapedTerm, 'gi');
|
||||
fileLines.value.forEach((line, index) => {
|
||||
if (regex.test(line)) {
|
||||
results.push(index);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Fall back to simple text search
|
||||
fileLines.value.forEach((line, index) => {
|
||||
if (line.toLowerCase().includes(searchTerm)) {
|
||||
results.push(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
searchResults.value = results;
|
||||
currentResultIndex.value = 0;
|
||||
|
||||
// Update display lines with match indicators
|
||||
displayLines.value?.forEach(line => {
|
||||
line.hasMatch = results.includes(line.lineNumber - 1);
|
||||
});
|
||||
|
||||
isSearching.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute search when query changes
|
||||
*/
|
||||
const executeSearch = async (query) => {
|
||||
searchQuery.value = query;
|
||||
await performSearch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all search results
|
||||
*/
|
||||
function clearSearchResults() {
|
||||
searchResults.value = [];
|
||||
currentResultIndex.value = 0;
|
||||
displayLines.value?.forEach(line => {
|
||||
line.hasMatch = false;
|
||||
});
|
||||
searchError.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear search query and results
|
||||
*/
|
||||
function clearSearch() {
|
||||
searchQuery.value = '';
|
||||
clearSearchResults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to next search result
|
||||
*/
|
||||
function goToNextResult() {
|
||||
if (searchResults.value.length === 0) return;
|
||||
currentResultIndex.value =
|
||||
(currentResultIndex.value + 1) % searchResults.value.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to previous search result
|
||||
*/
|
||||
function goToPrevResult() {
|
||||
if (searchResults.value.length === 0) return;
|
||||
currentResultIndex.value =
|
||||
(currentResultIndex.value - 1 + searchResults.value.length) %
|
||||
searchResults.value.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current result line number (1-based)
|
||||
*/
|
||||
const currentResultLineNumber = computed(() => {
|
||||
if (searchResults.value.length === 0) return null;
|
||||
return searchResults.value[currentResultIndex.value] + 1;
|
||||
});
|
||||
|
||||
/**
|
||||
* Get result count display text
|
||||
*/
|
||||
const resultCountDisplay = computed(() => {
|
||||
if (searchResults.value.length === 0) return '0 results';
|
||||
return `${currentResultIndex.value + 1} / ${searchResults.value.length}`;
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if search is active
|
||||
*/
|
||||
const hasSearchResults = computed(() => searchResults.value.length > 0);
|
||||
|
||||
/**
|
||||
* Get highlighted HTML for search term in text
|
||||
*/
|
||||
function getHighlightedContent(lineContent) {
|
||||
if (!searchQuery.value.trim()) return lineContent;
|
||||
|
||||
const searchTerm = searchQuery.value;
|
||||
const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
||||
const regex = new RegExp(`(${escapedTerm})`, 'gi');
|
||||
|
||||
return lineContent.replaceAll(regex, '<mark>$1</mark>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update filters and rerun search
|
||||
* Can support fuzzy matching and advanced filters in future
|
||||
*/
|
||||
function updateFilters(options = {}) {
|
||||
// Extensible for fuzzy matching, regex flags, etc.
|
||||
if (options.fuzzy !== undefined) {
|
||||
// Future: implement fuzzy matching
|
||||
}
|
||||
if (options.wholeWord !== undefined) {
|
||||
// Future: implement whole word matching
|
||||
}
|
||||
if (options.caseSensitive !== undefined) {
|
||||
// Future: implement case-sensitive matching
|
||||
}
|
||||
// Trigger search with updated filters
|
||||
performSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply search history item
|
||||
*/
|
||||
function applyHistoryItem(item) {
|
||||
searchQuery.value = item;
|
||||
performSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for manual searchQuery changes
|
||||
*/
|
||||
watch(searchQuery, () => {
|
||||
if (searchQuery.value.trim()) {
|
||||
performSearch();
|
||||
} else {
|
||||
clearSearchResults();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
searchQuery,
|
||||
searchResults,
|
||||
currentResultIndex,
|
||||
isSearching,
|
||||
searchError,
|
||||
searchHistory,
|
||||
|
||||
// Computed
|
||||
currentResultLineNumber,
|
||||
resultCountDisplay,
|
||||
hasSearchResults,
|
||||
|
||||
// Methods
|
||||
executeSearch,
|
||||
clearSearch,
|
||||
clearSearchResults,
|
||||
goToNextResult,
|
||||
goToPrevResult,
|
||||
updateFilters,
|
||||
applyHistoryItem,
|
||||
getHighlightedContent,
|
||||
performSearch
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user