mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-07-15 03:29:01 +00:00
755 lines
22 KiB
JavaScript
755 lines
22 KiB
JavaScript
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
|
* 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-globals-from ../debugger-controller.js */
|
||
|
/* import-globals-from ../debugger-view.js */
|
||
|
/* import-globals-from ../utils.js */
|
||
|
/* globals document, window */
|
||
|
"use strict";
|
||
|
|
||
|
/**
|
||
|
* Functions handling the global search UI.
|
||
|
*/
|
||
|
function GlobalSearchView(DebuggerController, DebuggerView) {
|
||
|
dumpn("GlobalSearchView was instantiated");
|
||
|
|
||
|
this.SourceScripts = DebuggerController.SourceScripts;
|
||
|
this.DebuggerView = DebuggerView;
|
||
|
|
||
|
this._onHeaderClick = this._onHeaderClick.bind(this);
|
||
|
this._onLineClick = this._onLineClick.bind(this);
|
||
|
this._onMatchClick = this._onMatchClick.bind(this);
|
||
|
}
|
||
|
|
||
|
GlobalSearchView.prototype = Heritage.extend(WidgetMethods, {
|
||
|
/**
|
||
|
* Initialization function, called when the debugger is started.
|
||
|
*/
|
||
|
initialize: function() {
|
||
|
dumpn("Initializing the GlobalSearchView");
|
||
|
|
||
|
this.widget = new SimpleListWidget(document.getElementById("globalsearch"));
|
||
|
this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter");
|
||
|
|
||
|
this.emptyText = L10N.getStr("noMatchingStringsText");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Destruction function, called when the debugger is closed.
|
||
|
*/
|
||
|
destroy: function() {
|
||
|
dumpn("Destroying the GlobalSearchView");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Sets the results container hidden or visible. It's hidden by default.
|
||
|
* @param boolean aFlag
|
||
|
*/
|
||
|
set hidden(aFlag) {
|
||
|
this.widget.setAttribute("hidden", aFlag);
|
||
|
this._splitter.setAttribute("hidden", aFlag);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the visibility state of the global search container.
|
||
|
* @return boolean
|
||
|
*/
|
||
|
get hidden() {
|
||
|
return this.widget.getAttribute("hidden") == "true" ||
|
||
|
this._splitter.getAttribute("hidden") == "true";
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Hides and removes all items from this search container.
|
||
|
*/
|
||
|
clearView: function() {
|
||
|
this.hidden = true;
|
||
|
this.empty();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Selects the next found item in this container.
|
||
|
* Does not change the currently focused node.
|
||
|
*/
|
||
|
selectNext: function() {
|
||
|
let totalLineResults = LineResults.size();
|
||
|
if (!totalLineResults) {
|
||
|
return;
|
||
|
}
|
||
|
if (++this._currentlyFocusedMatch >= totalLineResults) {
|
||
|
this._currentlyFocusedMatch = 0;
|
||
|
}
|
||
|
this._onMatchClick({
|
||
|
target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Selects the previously found item in this container.
|
||
|
* Does not change the currently focused node.
|
||
|
*/
|
||
|
selectPrev: function() {
|
||
|
let totalLineResults = LineResults.size();
|
||
|
if (!totalLineResults) {
|
||
|
return;
|
||
|
}
|
||
|
if (--this._currentlyFocusedMatch < 0) {
|
||
|
this._currentlyFocusedMatch = totalLineResults - 1;
|
||
|
}
|
||
|
this._onMatchClick({
|
||
|
target: LineResults.getElementAtIndex(this._currentlyFocusedMatch)
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Schedules searching for a string in all of the sources.
|
||
|
*
|
||
|
* @param string aToken
|
||
|
* The string to search for.
|
||
|
* @param number aWait
|
||
|
* The amount of milliseconds to wait until draining.
|
||
|
*/
|
||
|
scheduleSearch: function(aToken, aWait) {
|
||
|
// The amount of time to wait for the requests to settle.
|
||
|
let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY;
|
||
|
let delay = aWait === undefined ? maxDelay / aToken.length : aWait;
|
||
|
|
||
|
// Allow requests to settle down first.
|
||
|
setNamedTimeout("global-search", delay, () => {
|
||
|
// Start fetching as many sources as possible, then perform the search.
|
||
|
let actors = this.DebuggerView.Sources.values;
|
||
|
let sourcesFetched = DebuggerController.dispatch(actions.getTextForSources(actors));
|
||
|
sourcesFetched.then(aSources => this._doSearch(aToken, aSources));
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Finds string matches in all the sources stored in the controller's cache,
|
||
|
* and groups them by url and line number.
|
||
|
*
|
||
|
* @param string aToken
|
||
|
* The string to search for.
|
||
|
* @param array aSources
|
||
|
* An array of [url, text] tuples for each source.
|
||
|
*/
|
||
|
_doSearch: function(aToken, aSources) {
|
||
|
// Don't continue filtering if the searched token is an empty string.
|
||
|
if (!aToken) {
|
||
|
this.clearView();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Search is not case sensitive, prepare the actual searched token.
|
||
|
let lowerCaseToken = aToken.toLowerCase();
|
||
|
let tokenLength = aToken.length;
|
||
|
|
||
|
// Create a Map containing search details for each source.
|
||
|
let globalResults = new GlobalResults();
|
||
|
|
||
|
// Search for the specified token in each source's text.
|
||
|
for (let [actor, text] of aSources) {
|
||
|
let item = this.DebuggerView.Sources.getItemByValue(actor);
|
||
|
let url = item.attachment.source.url;
|
||
|
if (!url) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Verify that the search token is found anywhere in the source.
|
||
|
if (!text.toLowerCase().includes(lowerCaseToken)) {
|
||
|
continue;
|
||
|
}
|
||
|
// ...and if so, create a Map containing search details for each line.
|
||
|
let sourceResults = new SourceResults(actor,
|
||
|
globalResults,
|
||
|
this.DebuggerView.Sources);
|
||
|
|
||
|
// Search for the specified token in each line's text.
|
||
|
text.split("\n").forEach((aString, aLine) => {
|
||
|
// Search is not case sensitive, prepare the actual searched line.
|
||
|
let lowerCaseLine = aString.toLowerCase();
|
||
|
|
||
|
// Verify that the search token is found anywhere in this line.
|
||
|
if (!lowerCaseLine.includes(lowerCaseToken)) {
|
||
|
return;
|
||
|
}
|
||
|
// ...and if so, create a Map containing search details for each word.
|
||
|
let lineResults = new LineResults(aLine, sourceResults);
|
||
|
|
||
|
// Search for the specified token this line's text.
|
||
|
lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => {
|
||
|
let prevLength = aPrev.length;
|
||
|
let currLength = aCurr.length;
|
||
|
|
||
|
// Everything before the token is unmatched.
|
||
|
let unmatched = aString.substr(prevLength, currLength);
|
||
|
lineResults.add(unmatched);
|
||
|
|
||
|
// The lowered-case line was split by the lowered-case token. So,
|
||
|
// get the actual matched text from the original line's text.
|
||
|
if (aIndex != aArray.length - 1) {
|
||
|
let matched = aString.substr(prevLength + currLength, tokenLength);
|
||
|
let range = { start: prevLength + currLength, length: matched.length };
|
||
|
lineResults.add(matched, range, true);
|
||
|
}
|
||
|
|
||
|
// Continue with the next sub-region in this line's text.
|
||
|
return aPrev + aToken + aCurr;
|
||
|
}, "");
|
||
|
|
||
|
if (lineResults.matchCount) {
|
||
|
sourceResults.add(lineResults);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (sourceResults.matchCount) {
|
||
|
globalResults.add(sourceResults);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Rebuild the results, then signal if there are any matches.
|
||
|
if (globalResults.matchCount) {
|
||
|
this.hidden = false;
|
||
|
this._currentlyFocusedMatch = -1;
|
||
|
this._createGlobalResultsUI(globalResults);
|
||
|
window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND);
|
||
|
} else {
|
||
|
window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Creates global search results entries and adds them to this container.
|
||
|
*
|
||
|
* @param GlobalResults aGlobalResults
|
||
|
* An object containing all source results, grouped by source location.
|
||
|
*/
|
||
|
_createGlobalResultsUI: function(aGlobalResults) {
|
||
|
let i = 0;
|
||
|
|
||
|
for (let sourceResults of aGlobalResults) {
|
||
|
if (i++ == 0) {
|
||
|
this._createSourceResultsUI(sourceResults);
|
||
|
} else {
|
||
|
// Dispatch subsequent document manipulation operations, to avoid
|
||
|
// blocking the main thread when a large number of search results
|
||
|
// is found, thus giving the impression of faster searching.
|
||
|
Services.tm.currentThread.dispatch({ run:
|
||
|
this._createSourceResultsUI.bind(this, sourceResults)
|
||
|
}, 0);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Creates source search results entries and adds them to this container.
|
||
|
*
|
||
|
* @param SourceResults aSourceResults
|
||
|
* An object containing all the matched lines for a specific source.
|
||
|
*/
|
||
|
_createSourceResultsUI: function(aSourceResults) {
|
||
|
// Create the element node for the source results item.
|
||
|
let container = document.createElement("hbox");
|
||
|
aSourceResults.createView(container, {
|
||
|
onHeaderClick: this._onHeaderClick,
|
||
|
onLineClick: this._onLineClick,
|
||
|
onMatchClick: this._onMatchClick
|
||
|
});
|
||
|
|
||
|
// Append a source results item to this container.
|
||
|
let item = this.push([container], {
|
||
|
index: -1, /* specifies on which position should the item be appended */
|
||
|
attachment: {
|
||
|
sourceResults: aSourceResults
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The click listener for a results header.
|
||
|
*/
|
||
|
_onHeaderClick: function(e) {
|
||
|
let sourceResultsItem = SourceResults.getItemForElement(e.target);
|
||
|
sourceResultsItem.instance.toggle(e);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The click listener for a results line.
|
||
|
*/
|
||
|
_onLineClick: function(e) {
|
||
|
let lineResultsItem = LineResults.getItemForElement(e.target);
|
||
|
this._onMatchClick({ target: lineResultsItem.firstMatch });
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The click listener for a result match.
|
||
|
*/
|
||
|
_onMatchClick: function(e) {
|
||
|
if (e instanceof Event) {
|
||
|
e.preventDefault();
|
||
|
e.stopPropagation();
|
||
|
}
|
||
|
|
||
|
let target = e.target;
|
||
|
let sourceResultsItem = SourceResults.getItemForElement(target);
|
||
|
let lineResultsItem = LineResults.getItemForElement(target);
|
||
|
|
||
|
sourceResultsItem.instance.expand();
|
||
|
this._currentlyFocusedMatch = LineResults.indexOfElement(target);
|
||
|
this._scrollMatchIntoViewIfNeeded(target);
|
||
|
this._bounceMatch(target);
|
||
|
|
||
|
let actor = sourceResultsItem.instance.actor;
|
||
|
let line = lineResultsItem.instance.line;
|
||
|
|
||
|
this.DebuggerView.setEditorLocation(actor, line + 1, { noDebug: true });
|
||
|
|
||
|
let range = lineResultsItem.lineData.range;
|
||
|
let cursor = this.DebuggerView.editor.getOffset({ line: line, ch: 0 });
|
||
|
let [ anchor, head ] = this.DebuggerView.editor.getPosition(
|
||
|
cursor + range.start,
|
||
|
cursor + range.start + range.length
|
||
|
);
|
||
|
|
||
|
this.DebuggerView.editor.setSelection(anchor, head);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Scrolls a match into view if not already visible.
|
||
|
*
|
||
|
* @param nsIDOMNode aMatch
|
||
|
* The match to scroll into view.
|
||
|
*/
|
||
|
_scrollMatchIntoViewIfNeeded: function(aMatch) {
|
||
|
this.widget.ensureElementIsVisible(aMatch);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Starts a bounce animation for a match.
|
||
|
*
|
||
|
* @param nsIDOMNode aMatch
|
||
|
* The match to start a bounce animation for.
|
||
|
*/
|
||
|
_bounceMatch: function(aMatch) {
|
||
|
Services.tm.currentThread.dispatch({ run: () => {
|
||
|
aMatch.addEventListener("transitionend", function onEvent() {
|
||
|
aMatch.removeEventListener("transitionend", onEvent);
|
||
|
aMatch.removeAttribute("focused");
|
||
|
});
|
||
|
aMatch.setAttribute("focused", "");
|
||
|
}}, 0);
|
||
|
aMatch.setAttribute("focusing", "");
|
||
|
},
|
||
|
|
||
|
_splitter: null,
|
||
|
_currentlyFocusedMatch: -1,
|
||
|
_forceExpandResults: false
|
||
|
});
|
||
|
|
||
|
DebuggerView.GlobalSearch = new GlobalSearchView(DebuggerController, DebuggerView);
|
||
|
|
||
|
/**
|
||
|
* An object containing all source results, grouped by source location.
|
||
|
* Iterable via "for (let [location, sourceResults] of globalResults) { }".
|
||
|
*/
|
||
|
function GlobalResults() {
|
||
|
this._store = [];
|
||
|
SourceResults._itemsByElement = new Map();
|
||
|
LineResults._itemsByElement = new Map();
|
||
|
}
|
||
|
|
||
|
GlobalResults.prototype = {
|
||
|
/**
|
||
|
* Adds source results to this store.
|
||
|
*
|
||
|
* @param SourceResults aSourceResults
|
||
|
* An object containing search results for a specific source.
|
||
|
*/
|
||
|
add: function(aSourceResults) {
|
||
|
this._store.push(aSourceResults);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the number of source results in this store.
|
||
|
*/
|
||
|
get matchCount() {
|
||
|
return this._store.length;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* An object containing all the matched lines for a specific source.
|
||
|
* Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }".
|
||
|
*
|
||
|
* @param string aActor
|
||
|
* The target source actor id.
|
||
|
* @param GlobalResults aGlobalResults
|
||
|
* An object containing all source results, grouped by source location.
|
||
|
*/
|
||
|
function SourceResults(aActor, aGlobalResults, sourcesView) {
|
||
|
let item = sourcesView.getItemByValue(aActor);
|
||
|
this.actor = aActor;
|
||
|
this.label = item.attachment.source.url;
|
||
|
this._globalResults = aGlobalResults;
|
||
|
this._store = [];
|
||
|
}
|
||
|
|
||
|
SourceResults.prototype = {
|
||
|
/**
|
||
|
* Adds line results to this store.
|
||
|
*
|
||
|
* @param LineResults aLineResults
|
||
|
* An object containing search results for a specific line.
|
||
|
*/
|
||
|
add: function(aLineResults) {
|
||
|
this._store.push(aLineResults);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the number of line results in this store.
|
||
|
*/
|
||
|
get matchCount() {
|
||
|
return this._store.length;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Expands the element, showing all the added details.
|
||
|
*/
|
||
|
expand: function() {
|
||
|
this._resultsContainer.removeAttribute("hidden");
|
||
|
this._arrow.setAttribute("open", "");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Collapses the element, hiding all the added details.
|
||
|
*/
|
||
|
collapse: function() {
|
||
|
this._resultsContainer.setAttribute("hidden", "true");
|
||
|
this._arrow.removeAttribute("open");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Toggles between the element collapse/expand state.
|
||
|
*/
|
||
|
toggle: function(e) {
|
||
|
this.expanded ^= 1;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets this element's expanded state.
|
||
|
* @return boolean
|
||
|
*/
|
||
|
get expanded() {
|
||
|
return this._resultsContainer.getAttribute("hidden") != "true" &&
|
||
|
this._arrow.hasAttribute("open");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Sets this element's expanded state.
|
||
|
* @param boolean aFlag
|
||
|
*/
|
||
|
set expanded(aFlag) {
|
||
|
this[aFlag ? "expand" : "collapse"]();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the element associated with this item.
|
||
|
* @return nsIDOMNode
|
||
|
*/
|
||
|
get target() {
|
||
|
return this._target;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Customization function for creating this item's UI.
|
||
|
*
|
||
|
* @param nsIDOMNode aElementNode
|
||
|
* The element associated with the displayed item.
|
||
|
* @param object aCallbacks
|
||
|
* An object containing all the necessary callback functions:
|
||
|
* - onHeaderClick
|
||
|
* - onMatchClick
|
||
|
*/
|
||
|
createView: function(aElementNode, aCallbacks) {
|
||
|
this._target = aElementNode;
|
||
|
|
||
|
let arrow = this._arrow = document.createElement("box");
|
||
|
arrow.className = "arrow";
|
||
|
|
||
|
let locationNode = document.createElement("label");
|
||
|
locationNode.className = "plain dbg-results-header-location";
|
||
|
locationNode.setAttribute("value", this.label);
|
||
|
|
||
|
let matchCountNode = document.createElement("label");
|
||
|
matchCountNode.className = "plain dbg-results-header-match-count";
|
||
|
matchCountNode.setAttribute("value", "(" + this.matchCount + ")");
|
||
|
|
||
|
let resultsHeader = this._resultsHeader = document.createElement("hbox");
|
||
|
resultsHeader.className = "dbg-results-header";
|
||
|
resultsHeader.setAttribute("align", "center")
|
||
|
resultsHeader.appendChild(arrow);
|
||
|
resultsHeader.appendChild(locationNode);
|
||
|
resultsHeader.appendChild(matchCountNode);
|
||
|
resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false);
|
||
|
|
||
|
let resultsContainer = this._resultsContainer = document.createElement("vbox");
|
||
|
resultsContainer.className = "dbg-results-container";
|
||
|
resultsContainer.setAttribute("hidden", "true");
|
||
|
|
||
|
// Create lines search results entries and add them to this container.
|
||
|
// Afterwards, if the number of matches is reasonable, expand this
|
||
|
// container automatically.
|
||
|
for (let lineResults of this._store) {
|
||
|
lineResults.createView(resultsContainer, aCallbacks);
|
||
|
}
|
||
|
if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) {
|
||
|
this.expand();
|
||
|
}
|
||
|
|
||
|
let resultsBox = document.createElement("vbox");
|
||
|
resultsBox.setAttribute("flex", "1");
|
||
|
resultsBox.appendChild(resultsHeader);
|
||
|
resultsBox.appendChild(resultsContainer);
|
||
|
|
||
|
aElementNode.id = "source-results-" + this.actor;
|
||
|
aElementNode.className = "dbg-source-results";
|
||
|
aElementNode.appendChild(resultsBox);
|
||
|
|
||
|
SourceResults._itemsByElement.set(aElementNode, { instance: this });
|
||
|
},
|
||
|
|
||
|
actor: "",
|
||
|
_globalResults: null,
|
||
|
_store: null,
|
||
|
_target: null,
|
||
|
_arrow: null,
|
||
|
_resultsHeader: null,
|
||
|
_resultsContainer: null
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* An object containing all the matches for a specific line.
|
||
|
* Iterable via "for (let chunk of lineResults) { }".
|
||
|
*
|
||
|
* @param number aLine
|
||
|
* The target line in the source.
|
||
|
* @param SourceResults aSourceResults
|
||
|
* An object containing all the matched lines for a specific source.
|
||
|
*/
|
||
|
function LineResults(aLine, aSourceResults) {
|
||
|
this.line = aLine;
|
||
|
this._sourceResults = aSourceResults;
|
||
|
this._store = [];
|
||
|
this._matchCount = 0;
|
||
|
}
|
||
|
|
||
|
LineResults.prototype = {
|
||
|
/**
|
||
|
* Adds string details to this store.
|
||
|
*
|
||
|
* @param string aString
|
||
|
* The text contents chunk in the line.
|
||
|
* @param object aRange
|
||
|
* An object containing the { start, length } of the chunk.
|
||
|
* @param boolean aMatchFlag
|
||
|
* True if the chunk is a matched string, false if just text content.
|
||
|
*/
|
||
|
add: function(aString, aRange, aMatchFlag) {
|
||
|
this._store.push({ string: aString, range: aRange, match: !!aMatchFlag });
|
||
|
this._matchCount += aMatchFlag ? 1 : 0;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the number of word results in this store.
|
||
|
*/
|
||
|
get matchCount() {
|
||
|
return this._matchCount;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the element associated with this item.
|
||
|
* @return nsIDOMNode
|
||
|
*/
|
||
|
get target() {
|
||
|
return this._target;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Customization function for creating this item's UI.
|
||
|
*
|
||
|
* @param nsIDOMNode aElementNode
|
||
|
* The element associated with the displayed item.
|
||
|
* @param object aCallbacks
|
||
|
* An object containing all the necessary callback functions:
|
||
|
* - onMatchClick
|
||
|
* - onLineClick
|
||
|
*/
|
||
|
createView: function(aElementNode, aCallbacks) {
|
||
|
this._target = aElementNode;
|
||
|
|
||
|
let lineNumberNode = document.createElement("label");
|
||
|
lineNumberNode.className = "plain dbg-results-line-number";
|
||
|
lineNumberNode.classList.add("devtools-monospace");
|
||
|
lineNumberNode.setAttribute("value", this.line + 1);
|
||
|
|
||
|
let lineContentsNode = document.createElement("hbox");
|
||
|
lineContentsNode.className = "dbg-results-line-contents";
|
||
|
lineContentsNode.classList.add("devtools-monospace");
|
||
|
lineContentsNode.setAttribute("flex", "1");
|
||
|
|
||
|
let lineString = "";
|
||
|
let lineLength = 0;
|
||
|
let firstMatch = null;
|
||
|
|
||
|
for (let lineChunk of this._store) {
|
||
|
let { string, range, match } = lineChunk;
|
||
|
lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength);
|
||
|
lineLength += string.length;
|
||
|
|
||
|
let lineChunkNode = document.createElement("label");
|
||
|
lineChunkNode.className = "plain dbg-results-line-contents-string";
|
||
|
lineChunkNode.setAttribute("value", lineString);
|
||
|
lineChunkNode.setAttribute("match", match);
|
||
|
lineContentsNode.appendChild(lineChunkNode);
|
||
|
|
||
|
if (match) {
|
||
|
this._entangleMatch(lineChunkNode, lineChunk);
|
||
|
lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false);
|
||
|
firstMatch = firstMatch || lineChunkNode;
|
||
|
}
|
||
|
if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) {
|
||
|
lineContentsNode.appendChild(this._ellipsis.cloneNode(true));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this._entangleLine(lineContentsNode, firstMatch);
|
||
|
lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false);
|
||
|
|
||
|
let searchResult = document.createElement("hbox");
|
||
|
searchResult.className = "dbg-search-result";
|
||
|
searchResult.appendChild(lineNumberNode);
|
||
|
searchResult.appendChild(lineContentsNode);
|
||
|
|
||
|
aElementNode.appendChild(searchResult);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Handles a match while creating the view.
|
||
|
* @param nsIDOMNode aNode
|
||
|
* @param object aMatchChunk
|
||
|
*/
|
||
|
_entangleMatch: function(aNode, aMatchChunk) {
|
||
|
LineResults._itemsByElement.set(aNode, {
|
||
|
instance: this,
|
||
|
lineData: aMatchChunk
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Handles a line while creating the view.
|
||
|
* @param nsIDOMNode aNode
|
||
|
* @param nsIDOMNode aFirstMatch
|
||
|
*/
|
||
|
_entangleLine: function(aNode, aFirstMatch) {
|
||
|
LineResults._itemsByElement.set(aNode, {
|
||
|
instance: this,
|
||
|
firstMatch: aFirstMatch,
|
||
|
ignored: true
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* An nsIDOMNode label with an ellipsis value.
|
||
|
*/
|
||
|
_ellipsis: (function() {
|
||
|
let label = document.createElement("label");
|
||
|
label.className = "plain dbg-results-line-contents-string";
|
||
|
label.setAttribute("value", L10N.ellipsis);
|
||
|
return label;
|
||
|
})(),
|
||
|
|
||
|
line: 0,
|
||
|
_sourceResults: null,
|
||
|
_store: null,
|
||
|
_target: null
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* A generator-iterator over the global, source or line results.
|
||
|
*/
|
||
|
GlobalResults.prototype[Symbol.iterator] =
|
||
|
SourceResults.prototype[Symbol.iterator] =
|
||
|
LineResults.prototype[Symbol.iterator] = function*() {
|
||
|
yield* this._store;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Gets the item associated with the specified element.
|
||
|
*
|
||
|
* @param nsIDOMNode aElement
|
||
|
* The element used to identify the item.
|
||
|
* @return object
|
||
|
* The matched item, or null if nothing is found.
|
||
|
*/
|
||
|
SourceResults.getItemForElement =
|
||
|
LineResults.getItemForElement = function(aElement) {
|
||
|
return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true });
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Gets the element associated with a particular item at a specified index.
|
||
|
*
|
||
|
* @param number aIndex
|
||
|
* The index used to identify the item.
|
||
|
* @return nsIDOMNode
|
||
|
* The matched element, or null if nothing is found.
|
||
|
*/
|
||
|
SourceResults.getElementAtIndex =
|
||
|
LineResults.getElementAtIndex = function(aIndex) {
|
||
|
for (let [element, item] of this._itemsByElement) {
|
||
|
if (!item.ignored && !aIndex--) {
|
||
|
return element;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Gets the index of an item associated with the specified element.
|
||
|
*
|
||
|
* @param nsIDOMNode aElement
|
||
|
* The element to get the index for.
|
||
|
* @return number
|
||
|
* The index of the matched element, or -1 if nothing is found.
|
||
|
*/
|
||
|
SourceResults.indexOfElement =
|
||
|
LineResults.indexOfElement = function(aElement) {
|
||
|
let count = 0;
|
||
|
for (let [element, item] of this._itemsByElement) {
|
||
|
if (element == aElement) {
|
||
|
return count;
|
||
|
}
|
||
|
if (!item.ignored) {
|
||
|
count++;
|
||
|
}
|
||
|
}
|
||
|
return -1;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Gets the number of cached items associated with a specified element.
|
||
|
*
|
||
|
* @return number
|
||
|
* The number of key/value pairs in the corresponding map.
|
||
|
*/
|
||
|
SourceResults.size =
|
||
|
LineResults.size = function() {
|
||
|
let count = 0;
|
||
|
for (let [, item] of this._itemsByElement) {
|
||
|
if (!item.ignored) {
|
||
|
count++;
|
||
|
}
|
||
|
}
|
||
|
return count;
|
||
|
};
|