tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 5486f5260b4e046a1b1d80e656327f48a14855ab
parent 7bbe749383b00366d48a5a7b34f24f6ae2e29800
Author: iulian moraru <imoraru@mozilla.com>
Date:   Wed,  1 Oct 2025 22:25:42 +0300

Revert "Bug 1987972 - Expand context of target word in find flow appox 40 characters before and after r=Mardak,firefox-ai-ml-reviewers" for causing bc failures on browser_page_assist_actors.js.

This reverts commit e2918df3f45976be7a0a2dbc4cfb480c9cbb311c.

Diffstat:
Mbrowser/components/genai/content/page-assist.css | 9+++++----
Mbrowser/components/genai/content/page-assist.mjs | 167+++++++------------------------------------------------------------------------
Mtoolkit/actors/FinderChild.sys.mjs | 10+++++-----
Mtoolkit/content/widgets/findbar.js | 7++++---
Mtoolkit/modules/Finder.sys.mjs | 42+++++++++++-------------------------------
Mtoolkit/modules/FinderIterator.sys.mjs | 96++++---------------------------------------------------------------------------
Mtoolkit/modules/FinderParent.sys.mjs | 22+++-------------------
Mtoolkit/modules/tests/xpcshell/test_FinderIterator.js | 125++++---------------------------------------------------------------------------
8 files changed, 50 insertions(+), 428 deletions(-)

diff --git a/browser/components/genai/content/page-assist.css b/browser/components/genai/content/page-assist.css @@ -6,10 +6,11 @@ padding: var(--space-large); } -.ai-response { - white-space: pre-wrap; +.prompt-textarea { + columns: 3; + width: 100%; } -#input.find-input { - --input-background-icon: url(chrome://global/skin/icons/search-glass.svg); +.ai-response { + white-space: pre-wrap; } diff --git a/browser/components/genai/content/page-assist.mjs b/browser/components/genai/content/page-assist.mjs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; +import { html } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; // eslint-disable-next-line import/no-unassigned-import @@ -14,37 +14,6 @@ ChromeUtils.defineESModuleGetters(lazy, { AboutReaderParent: "resource:///actors/AboutReaderParent.sys.mjs", }); -import MozInputText from "chrome://global/content/elements/moz-input-text.mjs"; - -export class PageAssistInput extends MozInputText { - static properties = { - class: { type: String, reflect: true }, - }; - - inputTemplate() { - return html` - <link - rel="stylesheet" - href="chrome://browser/content/genai/content/page-assist.css" - /> - <input - id="input" - class=${"with-icon " + ifDefined(this.class)} - name=${this.name} - .value=${this.value || ""} - ?disabled=${this.disabled || this.parentDisabled} - accesskey=${ifDefined(this.accessKey)} - placeholder=${ifDefined(this.placeholder)} - aria-label=${ifDefined(this.ariaLabel ?? undefined)} - aria-describedby="description" - @input=${this.handleInput} - @change=${this.redispatchEvent} - /> - `; - } -} -customElements.define("page-assists-input", PageAssistInput); - export class PageAssist extends MozLitElement { _progressListener = null; _onTabSelect = null; @@ -55,10 +24,6 @@ export class PageAssist extends MozLitElement { userPrompt: { type: String }, aiResponse: { type: String }, isCurrentPageReaderable: { type: Boolean }, - matchCountQty: { type: Number }, - currentMatchIndex: { type: Number }, - highlightAll: { type: Boolean }, - snippets: { type: Array }, }; constructor() { @@ -66,10 +31,6 @@ export class PageAssist extends MozLitElement { this.userPrompt = ""; this.aiResponse = ""; this.isCurrentPageReaderable = true; - this.matchCountQty = 0; - this.currentMatchIndex = 0; - this.highlightAll = true; - this.snippets = []; } get _browserWin() { @@ -84,16 +45,10 @@ export class PageAssist extends MozLitElement { this._attachReaderModeListener(); this._initURLChange(); this._onUnload = () => this._cleanup(); - this._setupFinder(); this.ownerGlobal.addEventListener("unload", this._onUnload, { once: true }); } disconnectedCallback() { - // Clean up finder listener - if (this.browser && this.browser.finder) { - this.browser.finder.removeResultListener(this); - } - if (this._onUnload) { this.ownerGlobal.removeEventListener("unload", this._onUnload); this._onUnload = null; @@ -102,35 +57,6 @@ export class PageAssist extends MozLitElement { super.disconnectedCallback(); } - _setupFinder() { - const gBrowser = this._gBrowser; - - if (!gBrowser) { - console.warn("No gBrowser found."); - return; - } - - const selected = gBrowser.selectedBrowser; - - // If already attached to this browser, skip - if (this.browser === selected) { - return; - } - - // Clean up old listener if needed - if (this.browser && this.browser.finder) { - this.browser.finder.removeResultListener(this); - } - - this.browser = selected; - - if (this.browser && this.browser.finder) { - this.browser.finder.addResultListener(this); - } else { - console.warn("PageAssist: no finder on selected browser."); - } - } - _cleanup() { try { const gBrowser = this._gBrowser; @@ -188,7 +114,6 @@ export class PageAssist extends MozLitElement { } this._onTabSelect = () => { - this._setupFinder(); const browser = gBrowser.selectedBrowser; this.isCurrentPageReaderable = !!browser?.isArticle; }; @@ -236,66 +161,11 @@ export class PageAssist extends MozLitElement { return await actor.fetchPageData(); } - _clearFinder() { - if (this.browser?.finder) { - this.browser.finder.removeSelection(); - this.browser.finder.highlight(false, "", false); - } - this.matchCountQty = 0; - this.currentMatchIndex = 0; - this.snippets = []; - } - _handlePromptInput = e => { const value = e.target.value; this.userPrompt = value; - - // If input is empty, clear values - if (!value) { - this._clearFinder(); - return; - } - - // Perform the search - this.browser.finder.fastFind(value, false, false); - - if (this.highlightAll) { - // Todo this also needs to take contextRange. - this.browser.finder.highlight(true, value, false); - } - - // Request match count - this method will trigger onMatchesCountResult callback - this.browser.finder.requestMatchesCount(value, { - linksOnly: false, - contextRange: 30, - }); }; - onMatchesCountResult(result) { - this.matchCountQty = result.total; - this.currentMatchIndex = result.current; - this.snippets = result.snippets || []; - } - - // Abstract method need to be implemented or it will error - onHighlightFinished() { - // Noop. - } - - // Finder result listener methods - onFindResult(result) { - switch (result.result) { - case Ci.nsITypeAheadFind.FIND_NOTFOUND: - this.matchCountQty = 0; - this.currentMatchIndex = 0; - this.snippets = []; - break; - - default: - break; - } - } - _handleSubmit = async () => { const pageData = await this._fetchPageData(); if (!pageData) { @@ -326,29 +196,18 @@ export class PageAssist extends MozLitElement { ? html`<div class="ai-response">${this.aiResponse}</div>` : ""} <div> - <page-assists-input - class="find-input" - type="text" - placeholder="Find in page..." - .value=${this.userPrompt} - @input=${this._handlePromptInput} - ></page-assists-input> - </div> - - <div> - ${this.snippets.length - ? html`<div class="snippets"> - <h3>Snippets</h3> - <ul> - ${this.snippets.map( - snippet => - html`<li> - ${snippet.before}<b>${snippet.match}</b>${snippet.after} - </li>` - )} - </ul> - </div>` - : ""} + <textarea + class="prompt-textarea" + @input=${e => this._handlePromptInput(e)} + ></textarea> + <moz-button + id="submit-user-prompt-btn" + type="primary" + size="small" + @click=${this._handleSubmit} + > + Submit + </moz-button> </div> </div> </div> diff --git a/toolkit/actors/FinderChild.sys.mjs b/toolkit/actors/FinderChild.sys.mjs @@ -103,11 +103,11 @@ export class FinderChild extends JSWindowActorChild { case "Finder:MatchesCount": return this.finder - .requestMatchesCount(data.searchString, { - linksOnly: data.linksOnly, - useSubFrames: data.useSubFrames, - contextRange: data.contextRange, - }) + .requestMatchesCount( + data.searchString, + data.linksOnly, + data.useSubFrames + ) .then(result => { if (result) { result.browsingContextId = this.browsingContext.id; diff --git a/toolkit/content/widgets/findbar.js b/toolkit/content/widgets/findbar.js @@ -500,9 +500,10 @@ return; } - this.browser.finder.requestMatchesCount(this._findField.value, { - linksOnly: this.findMode == this.FIND_LINKS, - }); + this.browser.finder.requestMatchesCount( + this._findField.value, + this.findMode == this.FIND_LINKS + ); } /** diff --git a/toolkit/modules/Finder.sys.mjs b/toolkit/modules/Finder.sys.mjs @@ -372,11 +372,11 @@ Finder.prototype = { aArgs, aArgs.useSubFrames ? false : aArgs.foundInThisFrame ); - - let matchCountPromise = this.requestMatchesCount(aArgs.searchString, { - linksOnly: aArgs.linksOnly, - useSubFrames: aArgs.useSubFrames, - }); + let matchCountPromise = this.requestMatchesCount( + aArgs.searchString, + aArgs.linksOnly, + aArgs.useSubFrames + ); let results = await Promise.all([highlightPromise, matchCountPromise]); @@ -606,7 +606,7 @@ Finder.prototype = { return result; }, - async requestMatchesCount(aWord, optionalArgs) { + async requestMatchesCount(aWord, aLinksOnly, aUseSubFrames = true) { if ( this._lastFindResult == Ci.nsITypeAheadFind.FIND_NOTFOUND || this.searchString == "" || @@ -624,15 +624,11 @@ Finder.prototype = { let params = { caseSensitive: this._fastFind.caseSensitive, entireWord: this._fastFind.entireWord, - linksOnly: optionalArgs.linksOnly || false, + linksOnly: aLinksOnly, matchDiacritics: this._fastFind.matchDiacritics, word: aWord, - useSubFrames: - optionalArgs.useSubFrames !== undefined - ? optionalArgs.useSubFrames - : true, + useSubFrames: aUseSubFrames, }; - if (!this.iterator.continueRunning(params)) { this.iterator.stop(); } @@ -643,11 +639,7 @@ Finder.prototype = { limit: this.matchesCountLimit, listener: this, useCache: true, - useSubFrames: - optionalArgs.useSubFrames !== undefined - ? optionalArgs.useSubFrames - : true, - contextRange: optionalArgs.contextRange || 0, + useSubFrames: aUseSubFrames, }) ); @@ -662,22 +654,13 @@ Finder.prototype = { // FinderIterator listener implementation - onIteratorRangeFound(range, extra) { + onIteratorRangeFound(range) { let result = this._currentMatchesCountResult; if (!result) { return; } ++result.total; - - // Pull out the snippet that finderIterator attached - if (extra?.context) { - if (!result.snippets) { - result.snippets = []; - } - result.snippets.push(extra.context); - } - if (!result._currentFound) { ++result.current; result._currentFound = @@ -692,10 +675,7 @@ Finder.prototype = { onIteratorReset() {}, onIteratorRestart({ word, linksOnly, useSubFrames }) { - this.requestMatchesCount(word, { - linksOnly, - useSubFrames, - }); + this.requestMatchesCount(word, linksOnly, useSubFrames); }, onIteratorStart() { diff --git a/toolkit/modules/FinderIterator.sys.mjs b/toolkit/modules/FinderIterator.sys.mjs @@ -78,8 +78,7 @@ export class FinderIterator { * Optional, defaults to `false`. * @param {Object} options.listener Listener object that implements the * following callback functions: - * - onIteratorRangeFound({Range} range, {Object} extra); - * extra.context contains text snippet around the match + * - onIteratorRangeFound({Range} range); * - onIteratorReset(); * - onIteratorRestart({Object} iterParams); * - onIteratorStart({Object} iterParams); @@ -91,7 +90,6 @@ export class FinderIterator { * @param {Boolean} [options.useSubFrames] Whether to iterate over subframes. * Optional, defaults to `false`. * @param {String} options.word Word to search for - * @param {Number} contextRange - Number of characters to extract before and after target string * @return {Promise} */ start({ @@ -106,7 +104,6 @@ export class FinderIterator { useCache, word, useSubFrames, - contextRange, }) { // Take care of default values for non-required options. if (typeof allowDistance != "number") { @@ -202,7 +199,7 @@ export class FinderIterator { // Start! this.running = true; this._currentParams = iterParams; - this._findAllRanges(finder, ++this._spawnId, contextRange); + this._findAllRanges(finder, ++this._spawnId); return promise; } @@ -373,84 +370,6 @@ export class FinderIterator { } /** - * Extracts context around a found Range. - * Returns normalized text slices so indices are stable with FindBar behavior. - * - * @param {Range} range - DOM range for a single match. - * @param {Number} contextRange - Number of characters to extract before and after target string - */ - _extractSnippet(range, contextRange) { - const blockSelector = - "p,li,dt,dd,td,th,pre,code,h1,h2,h3,h4,h5,h6,article,section,blockquote,div"; - - const defaultResult = { - before: "", - match: "", - after: "", - start: -1, - end: -1, - }; - - try { - const doc = range?.startContainer?.ownerDocument; - if (!doc || range.collapsed) { - return defaultResult; - } - - // Pick a stable block container for context - let node = range.commonAncestorContainer; - if (node.nodeType === Node.TEXT_NODE) { - node = node.parentElement || node.parentNode; - } - - // Find the nearest block-level container (p, div, h1, etc.) to anchor our context extraction - const block = - (node?.closest && node.closest(blockSelector)) || node || doc.body; - - // Build ranges - const blockRange = doc.createRange(); - blockRange.selectNodeContents(block); - - const preRange = blockRange.cloneRange(); - preRange.setEnd(range.startContainer, range.startOffset); - - const postRange = blockRange.cloneRange(); - postRange.setStart(range.endContainer, range.endOffset); - - const preRaw = preRange.toString(); - const matchRaw = range.toString(); - const postRaw = postRange.toString(); - - // Consistent normalization across all three strings - const normalizeWhitespace = text => text.replace(/\s+/g, " "); - const preNorm = normalizeWhitespace(preRaw); - const matchNorm = normalizeWhitespace(matchRaw); - const postNorm = normalizeWhitespace(postRaw); - - const start = preNorm.length; // offset in normalized block text - const end = start + matchNorm.length; - const sliceText = (text, start, end) => text.slice(start, end); - - const beforeStr = sliceText( - preNorm, - Math.max(0, preNorm.length - contextRange), - preNorm.length - ); - const afterStr = sliceText(postNorm, 0, contextRange); - - return { - before: beforeStr, - match: matchNorm, - after: afterStr, - start, - end, - }; - } catch { - return defaultResult; - } - } - - /** * Internal; check if an iteration request is available in the previous result * that we cached. * @@ -615,10 +534,9 @@ export class FinderIterator { * @param {Number} spawnId Since `stop()` is synchronous and this method * is not, this identifier is used to learn if * it's supposed to still continue after a pause. - * @param {Number} contextRange - Number of characters to extract before and after target string * @yield {Range} */ - async _findAllRanges(finder, spawnId, contextRange = 0) { + async _findAllRanges(finder, spawnId) { if (this._timeout) { if (this._timer) { clearTimeout(this._timer); @@ -679,13 +597,7 @@ export class FinderIterator { continue; } - if (contextRange) { - listener.onIteratorRangeFound(range, { - context: this._extractSnippet(range, contextRange), - }); - } else { - listener.onIteratorRangeFound(range); - } + listener.onIteratorRangeFound(range); if (limit !== -1 && --limit === 0) { // We've reached our limit; no need to do more work for this listener. diff --git a/toolkit/modules/FinderParent.sys.mjs b/toolkit/modules/FinderParent.sys.mjs @@ -445,13 +445,9 @@ FinderParent.prototype = { }); }, - requestMatchesCount(aSearchString, optionalArgs) { + requestMatchesCount(aSearchString, aLinksOnly) { let list = this.gatherBrowsingContexts(this.browsingContext); - let args = { - searchString: aSearchString, - linksOnly: optionalArgs.linksOnly, - contextRange: optionalArgs.contextRange || 0, - }; + let args = { searchString: aSearchString, linksOnly: aLinksOnly }; args.useSubFrames = this.needSubFrameSearch(list); @@ -513,7 +509,6 @@ FinderParent.prototype = { let current = 0; let total = 0; let limit = 0; - let allSnippets = []; for (let response of responses) { // A null response can happen if another search was started // and this one became invalid. @@ -532,22 +527,11 @@ FinderParent.prototype = { } total += response.total; limit = response.limit; - - // Collect snippets from each response - if (response.snippets && Array.isArray(response.snippets)) { - allSnippets.push(...response.snippets); - } } if (sendNotification) { this.callListeners("onMatchesCountResult", [ - { - searchString: options.args.searchString, - current, - total, - limit, - snippets: allSnippets, - }, + { searchString: options.args.searchString, current, total, limit }, ]); } } diff --git a/toolkit/modules/tests/xpcshell/test_FinderIterator.js b/toolkit/modules/tests/xpcshell/test_FinderIterator.js @@ -15,54 +15,10 @@ finderIterator._iterateDocument = function* () { finderIterator._rangeStartsInLink = fakeRange => fakeRange.startsInLink; -function FakeRange(textContent, startsInLink = false, mockDocument = null) { - // Create a simple mock document if none provided - if (!mockDocument) { - mockDocument = { - body: { - textContent: `Some text before ${textContent} some text after`, - nodeName: "BODY", - }, - createRange() { - // Return a minimal range object instead of calling FakeRange constructor - return { - selectNodeContents() {}, - cloneRange() { - return this; - }, - setEnd() {}, - setStart() {}, - toString() { - return ""; - }, - }; - }, - }; - } - - this.startContainer = { - nodeType: Node.TEXT_NODE, - ownerDocument: mockDocument, - parentElement: { - closest() { - return mockDocument.body; - }, - nodeName: "P", - }, - }; - this.endContainer = this.startContainer; - this.commonAncestorContainer = this.startContainer; - this.startOffset = 10; - this.endOffset = 10 + textContent.length; - this.collapsed = false; +function FakeRange(textContent, startsInLink = false) { + this.startContainer = {}; this.startsInLink = startsInLink; this.toString = () => textContent; - - this.cloneRange = () => - new FakeRange(textContent, startsInLink, mockDocument); - this.selectNodeContents = () => {}; - this.setEnd = () => {}; - this.setStart = () => {}; } var gMockWindow = { @@ -97,7 +53,7 @@ add_task(async function test_start() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range, _extra) { + onIteratorRangeFound(range) { ++count; Assert.equal(range.toString(), findText, "Text content should match"); }, @@ -128,7 +84,7 @@ add_task(async function test_subframes() { entireWord: false, finder: gMockFinder, listener: { - onIteratorRangeFound(range, _extra) { + onIteratorRangeFound(range) { ++count; Assert.equal(range.toString(), findText, "Text content should match"); }, @@ -351,7 +307,7 @@ add_task(async function test_reset() { add_task(async function test_parallel_starts() { let findText = "tak"; - let rangeCount = 4000; + let rangeCount = 2143; prepareIterator(findText, rangeCount); // Start off the iterator. @@ -486,74 +442,3 @@ add_task(async function test_allowDistance() { finderIterator.reset(); }); - -add_task(async function test_context_extraction() { - let findText = "test"; - let rangeCount = 5; - prepareIterator(findText, rangeCount); - - let contexts = []; - await finderIterator.start({ - caseSensitive: false, - entireWord: false, - finder: gMockFinder, - listener: { - onIteratorRangeFound(range, extra) { - if (extra && extra.context) { - contexts.push(extra.context); - } - }, - }, - matchDiacritics: false, - word: findText, - contextRange: 40, - }); - - Assert.equal( - contexts.length, - rangeCount, - "Should have context for each match" - ); - - // Test the structure of returned context - for (let context of contexts) { - Assert.strictEqual(typeof context, "object", "Context should be an object"); - Assert.ok("before" in context, "Context should have 'before' property"); - Assert.ok("match" in context, "Context should have 'match' property"); - Assert.ok("after" in context, "Context should have 'after' property"); - Assert.ok("start" in context, "Context should have 'start' property"); - Assert.ok("end" in context, "Context should have 'end' property"); - } - - finderIterator.reset(); -}); - -add_task(async function test_no_context_without_range() { - let findText = "test"; - let rangeCount = 3; - prepareIterator(findText, rangeCount); - - let contextsProvided = 0; - await finderIterator.start({ - caseSensitive: false, - entireWord: false, - finder: gMockFinder, - listener: { - onIteratorRangeFound(range, extra) { - if (extra && extra.context) { - contextsProvided++; - } - }, - }, - matchDiacritics: false, - word: findText, - // Note: no contextRange parameter - }); - - Assert.equal( - contextsProvided, - 0, - "Should not provide context without contextRange" - ); - finderIterator.reset(); -});