feat: show all found words and allow jump to them (beta)

This commit is contained in:
2025-11-20 15:48:28 +03:00
parent 1c58357418
commit 1a4c91fd5e
22 changed files with 652 additions and 12 deletions

View File

@@ -47,21 +47,28 @@ export class ContentScript {
}
private setupMessageListener(): void {
MessageService.onMessage((message: MessageData) => {
MessageService.onMessage((message: MessageData, sender: any, sendResponse: (response?: any) => void) => {
switch (message.type) {
case 'WORD_LIST_UPDATED':
this.handleWordListUpdate();
break;
return false;
case 'GLOBAL_TOGGLE_UPDATED':
this.handleGlobalToggleUpdate(message.enabled!);
break;
return false;
case 'MATCH_OPTIONS_UPDATED':
this.handleMatchOptionsUpdate(message.matchCase!, message.matchWhole!);
break;
return false;
case 'EXCEPTIONS_LIST_UPDATED':
this.handleExceptionsUpdate();
break;
return false;
case 'GET_PAGE_HIGHLIGHTS':
this.handleGetPageHighlights(sendResponse);
return true;
case 'SCROLL_TO_HIGHLIGHT':
this.handleScrollToHighlight(message.word!, message.index!);
return false;
}
return false;
});
}
@@ -106,4 +113,27 @@ export class ContentScript {
this.isProcessing = false;
}
}
private handleGetPageHighlights(sendResponse: (response: any) => void): void {
const activeWords: ActiveWord[] = [];
for (const list of this.lists) {
if (!list.active) continue;
for (const word of list.words) {
if (!word.active) continue;
activeWords.push({
text: word.wordStr,
background: word.background || list.background,
foreground: word.foreground || list.foreground
});
}
}
const highlights = this.highlightEngine.getPageHighlights(activeWords);
sendResponse({ highlights });
}
private handleScrollToHighlight(word: string, index: number): void {
this.highlightEngine.scrollToHighlight(word, index);
}
}

View File

@@ -4,8 +4,10 @@ import { DOMUtils } from '../utils/DOMUtils.js';
export class HighlightEngine {
private styleSheet: CSSStyleSheet | null = null;
private highlights = new Map<string, Highlight>();
private highlightsByWord = new Map<string, Range[]>();
private observer: MutationObserver;
private isHighlighting = false;
private currentMatchCase = false;
constructor(private onUpdate: () => void) {
this.observer = new MutationObserver(DOMUtils.debounce((mutations: MutationRecord[]) => {
@@ -106,6 +108,8 @@ export class HighlightEngine {
if (this.isHighlighting) return;
this.isHighlighting = true;
this.currentMatchCase = matchCase;
this.observer.disconnect();
this.clearHighlightsInternal();
@@ -143,6 +147,7 @@ export class HighlightEngine {
const textNodes = this.getTextNodes();
const rangesByStyle = new Map<number, Range[]>();
this.highlightsByWord.clear();
for (const node of textNodes) {
if (!node.nodeValue) continue;
@@ -164,6 +169,11 @@ export class HighlightEngine {
rangesByStyle.set(styleIdx, []);
}
rangesByStyle.get(styleIdx)!.push(range);
if (!this.highlightsByWord.has(lookup)) {
this.highlightsByWord.set(lookup, []);
}
this.highlightsByWord.get(lookup)!.push(range);
}
}
}
@@ -187,6 +197,7 @@ export class HighlightEngine {
CSS.highlights.delete(name);
}
this.highlights.clear();
this.highlightsByWord.clear();
if (this.styleSheet && this.styleSheet.cssRules.length > 0) {
while (this.styleSheet.cssRules.length > 0) {
@@ -195,6 +206,67 @@ export class HighlightEngine {
}
}
getPageHighlights(activeWords: ActiveWord[]): Array<{ word: string; count: number; background: string; foreground: string }> {
const seen = new Map<string, { word: string; count: number; background: string; foreground: string }>();
for (const activeWord of activeWords) {
const lookup = this.currentMatchCase ? activeWord.text : activeWord.text.toLowerCase();
const ranges = this.highlightsByWord.get(lookup);
if (ranges && ranges.length > 0 && !seen.has(lookup)) {
seen.set(lookup, {
word: activeWord.text,
count: ranges.length,
background: activeWord.background,
foreground: activeWord.foreground
});
}
}
return Array.from(seen.values());
}
scrollToHighlight(word: string, index: number): void {
const lookup = this.currentMatchCase ? word : word.toLowerCase();
const ranges = this.highlightsByWord.get(lookup);
if (!ranges || ranges.length === 0) return;
const targetIndex = Math.min(index, ranges.length - 1);
const range = ranges[targetIndex];
if (!range) return;
try {
const rect = range.getBoundingClientRect();
const absoluteTop = window.pageYOffset + rect.top;
const middle = absoluteTop - (window.innerHeight / 2) + (rect.height / 2);
window.scrollTo({
top: middle,
behavior: 'smooth'
});
const flashHighlight = new Highlight(range);
CSS.highlights.set('gh-flash', flashHighlight);
if (this.styleSheet) {
const flashRule = '::highlight(gh-flash) { background-color: rgba(255, 165, 0, 0.8); box-shadow: 0 0 10px 3px rgba(255, 165, 0, 0.8); }';
const ruleIndex = this.styleSheet.insertRule(flashRule, this.styleSheet.cssRules.length);
setTimeout(() => {
CSS.highlights.delete('gh-flash');
if (this.styleSheet && ruleIndex < this.styleSheet.cssRules.length) {
this.styleSheet.deleteRule(ruleIndex);
}
}, 600);
}
} catch (e) {
console.error('Error scrolling to highlight:', e);
}
}
stopObserving(): void {
this.observer.disconnect();
}