/* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC. * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @constructor */ WebInspector.AdvancedSearchController = function() { this._shortcut = WebInspector.AdvancedSearchController.createShortcut(); this._searchId = 0; WebInspector.settings.advancedSearchConfig = WebInspector.settings.createSetting("advancedSearchConfig", new WebInspector.SearchConfig("", true, false)); WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameNavigated, this._frameNavigated, this); } /** * @return {!WebInspector.KeyboardShortcut.Descriptor} */ WebInspector.AdvancedSearchController.createShortcut = function() { if (WebInspector.isMac()) return WebInspector.KeyboardShortcut.makeDescriptor("f", WebInspector.KeyboardShortcut.Modifiers.Meta | WebInspector.KeyboardShortcut.Modifiers.Alt); else return WebInspector.KeyboardShortcut.makeDescriptor("f", WebInspector.KeyboardShortcut.Modifiers.Ctrl | WebInspector.KeyboardShortcut.Modifiers.Shift); } WebInspector.AdvancedSearchController.prototype = { /** * @param {KeyboardEvent} event * @return {boolean} */ handleShortcut: function(event) { if (WebInspector.KeyboardShortcut.makeKeyFromEvent(event) === this._shortcut.key) { if (!this._searchView || !this._searchView.isShowing() || this._searchView._search !== document.activeElement) { WebInspector.showPanel("scripts"); this.show(); } else this.close(); event.consume(true); return true; } return false; }, _frameNavigated: function() { this.resetSearch(); }, /** * @param {WebInspector.SearchScope} searchScope */ registerSearchScope: function(searchScope) { // FIXME: implement multiple search scopes. this._searchScope = searchScope; }, show: function() { if (!this._searchView) this._searchView = new WebInspector.SearchView(this); this._searchView.syncToSelection(); if (this._searchView.isShowing()) this._searchView.focus(); else WebInspector.showViewInDrawer(this._searchView._searchPanelElement, this._searchView, this.stopSearch.bind(this)); this.startIndexing(); }, close: function() { this.stopSearch(); WebInspector.closeViewInDrawer(); }, /** * @param {boolean} finished */ _onIndexingFinished: function(finished) { delete this._isIndexing; this._searchView.indexingFinished(finished); if (!finished) delete this._pendingSearchConfig; if (!this._pendingSearchConfig) return; var searchConfig = this._pendingSearchConfig delete this._pendingSearchConfig; this._innerStartSearch(searchConfig); }, startIndexing: function() { this._isIndexing = true; // FIXME: this._currentSearchScope should be initialized based on searchConfig this._currentSearchScope = this._searchScope; if (this._progressIndicator) this._progressIndicator.done(); this._progressIndicator = new WebInspector.ProgressIndicator(); this._searchView.indexingStarted(this._progressIndicator); this._currentSearchScope.performIndexing(this._progressIndicator, this._onIndexingFinished.bind(this)); }, /** * @param {number} searchId * @param {WebInspector.FileBasedSearchResultsPane.SearchResult} searchResult */ _onSearchResult: function(searchId, searchResult) { if (searchId !== this._searchId) return; this._searchView.addSearchResult(searchResult); if (!searchResult.searchMatches.length) return; if (!this._searchResultsPane) this._searchResultsPane = this._currentSearchScope.createSearchResultsPane(this._searchConfig); this._searchView.resultsPane = this._searchResultsPane; this._searchResultsPane.addSearchResult(searchResult); }, /** * @param {number} searchId * @param {boolean} finished */ _onSearchFinished: function(searchId, finished) { if (searchId !== this._searchId) return; if (!this._searchResultsPane) this._searchView.nothingFound(); this._searchView.searchFinished(finished); delete this._searchConfig; }, /** * @param {WebInspector.SearchConfig} searchConfig */ startSearch: function(searchConfig) { this.resetSearch(); ++this._searchId; if (!this._isIndexing) this.startIndexing(); this._pendingSearchConfig = searchConfig; }, /** * @param {WebInspector.SearchConfig} searchConfig */ _innerStartSearch: function(searchConfig) { this._searchConfig = searchConfig; // FIXME: this._currentSearchScope should be initialized based on searchConfig this._currentSearchScope = this._searchScope; if (this._progressIndicator) this._progressIndicator.done(); this._progressIndicator = new WebInspector.ProgressIndicator(); var totalSearchResultsCount = this._currentSearchScope.performSearch(searchConfig, this._progressIndicator, this._onSearchResult.bind(this, this._searchId), this._onSearchFinished.bind(this, this._searchId)); this._searchView.searchStarted(this._progressIndicator); }, resetSearch: function() { this.stopSearch(); if (this._searchResultsPane) { this._searchView.resetResults(); delete this._searchResultsPane; } }, stopSearch: function() { if (this._progressIndicator) this._progressIndicator.cancel(); if (this._currentSearchScope) this._currentSearchScope.stopSearch(); delete this._searchConfig; } } /** * @constructor * @extends {WebInspector.View} * @param {WebInspector.AdvancedSearchController} controller */ WebInspector.SearchView = function(controller) { WebInspector.View.call(this); this._controller = controller; this.element.className = "search-view"; this._searchPanelElement = document.createElement("span"); this._searchPanelElement.className = "search-drawer-header"; this._searchPanelElement.addEventListener("keydown", this._onKeyDown.bind(this), false); this._searchResultsElement = this.element.createChild("div"); this._searchResultsElement.className = "search-results"; this._searchLabel = this._searchPanelElement.createChild("span"); this._searchLabel.textContent = WebInspector.UIString("Search sources"); this._search = this._searchPanelElement.createChild("input"); this._search.setAttribute("type", "search"); this._search.addStyleClass("search-config-search"); this._search.setAttribute("results", "0"); this._search.setAttribute("size", 30); this._ignoreCaseLabel = this._searchPanelElement.createChild("label"); this._ignoreCaseLabel.addStyleClass("search-config-label"); this._ignoreCaseCheckbox = this._ignoreCaseLabel.createChild("input"); this._ignoreCaseCheckbox.setAttribute("type", "checkbox"); this._ignoreCaseCheckbox.addStyleClass("search-config-checkbox"); this._ignoreCaseLabel.appendChild(document.createTextNode(WebInspector.UIString("Ignore case"))); this._regexLabel = this._searchPanelElement.createChild("label"); this._regexLabel.addStyleClass("search-config-label"); this._regexCheckbox = this._regexLabel.createChild("input"); this._regexCheckbox.setAttribute("type", "checkbox"); this._regexCheckbox.addStyleClass("search-config-checkbox"); this._regexLabel.appendChild(document.createTextNode(WebInspector.UIString("Regular expression"))); this._searchStatusBarElement = document.createElement("div"); this._searchStatusBarElement.className = "search-status-bar-item"; this._searchMessageElement = this._searchStatusBarElement.createChild("div"); this._searchMessageElement.className = "search-status-bar-message"; this._searchResultsMessageElement = document.createElement("span"); this._searchResultsMessageElement.className = "search-results-status-bar-message"; this._load(); } // Number of recent search queries to store. WebInspector.SearchView.maxQueriesCount = 20; WebInspector.SearchView.prototype = { /** * @return {Array.} */ get statusBarItems() { return [this._searchStatusBarElement, this._searchResultsMessageElement]; }, /** * @return {WebInspector.SearchConfig} */ get searchConfig() { return new WebInspector.SearchConfig(this._search.value, this._ignoreCaseCheckbox.checked, this._regexCheckbox.checked); }, syncToSelection: function() { var selection = window.getSelection(); if (selection.rangeCount) { var queryCandidate = selection.toString().replace(/\r?\n.*/, ""); if (queryCandidate) this._search.value = queryCandidate; } }, /** * @type {WebInspector.SearchResultsPane} */ set resultsPane(resultsPane) { this.resetResults(); this._searchResultsElement.appendChild(resultsPane.element); }, /** * @param {WebInspector.ProgressIndicator} progressIndicator */ searchStarted: function(progressIndicator) { this.resetResults(); this._resetCounters(); this._searchMessageElement.textContent = WebInspector.UIString("Searching..."); progressIndicator.show(this._searchStatusBarElement); this._updateSearchResultsMessage(); if (!this._searchingView) this._searchingView = new WebInspector.EmptyView(WebInspector.UIString("Searching...")); this._searchingView.show(this._searchResultsElement); }, /** * @param {WebInspector.ProgressIndicator} progressIndicator */ indexingStarted: function(progressIndicator) { this._searchMessageElement.textContent = WebInspector.UIString("Indexing..."); progressIndicator.show(this._searchStatusBarElement); }, /** * @param {boolean} finished */ indexingFinished: function(finished) { this._searchMessageElement.textContent = finished ? "" : WebInspector.UIString("Indexing interrupted."); }, _updateSearchResultsMessage: function() { if (this._searchMatchesCount && this._searchResultsCount) this._searchResultsMessageElement.textContent = WebInspector.UIString("Found %d matches in %d files.", this._searchMatchesCount, this._nonEmptySearchResultsCount); else this._searchResultsMessageElement.textContent = ""; }, resetResults: function() { if (this._searchingView) this._searchingView.detach(); if (this._notFoundView) this._notFoundView.detach(); this._searchResultsElement.removeChildren(); }, _resetCounters: function() { this._searchMatchesCount = 0; this._searchResultsCount = 0; this._nonEmptySearchResultsCount = 0; }, nothingFound: function() { this.resetResults(); if (!this._notFoundView) this._notFoundView = new WebInspector.EmptyView(WebInspector.UIString("No matches found.")); this._notFoundView.show(this._searchResultsElement); this._searchResultsMessageElement.textContent = WebInspector.UIString("No matches found."); }, /** * @param {WebInspector.FileBasedSearchResultsPane.SearchResult} searchResult */ addSearchResult: function(searchResult) { this._searchMatchesCount += searchResult.searchMatches.length; this._searchResultsCount++; if (searchResult.searchMatches.length) this._nonEmptySearchResultsCount++; this._updateSearchResultsMessage(); }, /** * @param {boolean} finished */ searchFinished: function(finished) { this._searchMessageElement.textContent = finished ? WebInspector.UIString("Search finished.") : WebInspector.UIString("Search interrupted."); }, focus: function() { WebInspector.setCurrentFocusElement(this._search); this._search.select(); }, wasShown: function() { this.focus(); }, willHide: function() { this._controller.stopSearch(); }, /** * @param {Event} event */ _onKeyDown: function(event) { switch (event.keyCode) { case WebInspector.KeyboardShortcut.Keys.Enter.code: this._onAction(); break; case WebInspector.KeyboardShortcut.Keys.Esc.code: this._controller.close(); event.consume(true); break; } }, _save: function() { var searchConfig = new WebInspector.SearchConfig(this.searchConfig.query, this.searchConfig.ignoreCase, this.searchConfig.isRegex); WebInspector.settings.advancedSearchConfig.set(searchConfig); }, _load: function() { var searchConfig = WebInspector.settings.advancedSearchConfig.get(); this._search.value = searchConfig.query; this._ignoreCaseCheckbox.checked = searchConfig.ignoreCase; this._regexCheckbox.checked = searchConfig.isRegex; }, _onAction: function() { if (!this.searchConfig.query || !this.searchConfig.query.length) return; this._save(); this._controller.startSearch(this.searchConfig); }, __proto__: WebInspector.View.prototype } /** * @constructor * @param {string} query * @param {boolean} ignoreCase * @param {boolean} isRegex */ WebInspector.SearchConfig = function(query, ignoreCase, isRegex) { this.query = query; this.ignoreCase = ignoreCase; this.isRegex = isRegex; } /** * @interface */ WebInspector.SearchScope = function() { } WebInspector.SearchScope.prototype = { /** * @param {WebInspector.SearchConfig} searchConfig * @param {WebInspector.Progress} progress * @param {function(WebInspector.FileBasedSearchResultsPane.SearchResult)} searchResultCallback * @param {function(boolean)} searchFinishedCallback */ performSearch: function(searchConfig, progress, searchResultCallback, searchFinishedCallback) { }, stopSearch: function() { }, /** * @param {WebInspector.SearchConfig} searchConfig * @return {WebInspector.SearchResultsPane} */ createSearchResultsPane: function(searchConfig) { } } /** * @constructor * @param {number} offset * @param {number} length */ WebInspector.SearchResult = function(offset, length) { this.offset = offset; this.length = length; } /** * @constructor * @param {WebInspector.SearchConfig} searchConfig */ WebInspector.SearchResultsPane = function(searchConfig) { this._searchConfig = searchConfig; this.element = document.createElement("div"); } WebInspector.SearchResultsPane.prototype = { /** * @return {WebInspector.SearchConfig} */ get searchConfig() { return this._searchConfig; }, /** * @param {WebInspector.FileBasedSearchResultsPane.SearchResult} searchResult */ addSearchResult: function(searchResult) { } } /** * @constructor * @extends {WebInspector.SearchResultsPane} * @param {WebInspector.SearchConfig} searchConfig */ WebInspector.FileBasedSearchResultsPane = function(searchConfig) { WebInspector.SearchResultsPane.call(this, searchConfig); this._searchResults = []; this.element.id ="search-results-pane-file-based"; this._treeOutlineElement = document.createElement("ol"); this._treeOutlineElement.className = "search-results-outline-disclosure"; this.element.appendChild(this._treeOutlineElement); this._treeOutline = new TreeOutline(this._treeOutlineElement); this._matchesExpandedCount = 0; } WebInspector.FileBasedSearchResultsPane.matchesExpandedByDefaultCount = 20; WebInspector.FileBasedSearchResultsPane.fileMatchesShownAtOnce = 20; WebInspector.FileBasedSearchResultsPane.prototype = { /** * @param {WebInspector.UISourceCode} uiSourceCode * @param {number} lineNumber * @param {number} columnNumber * @return {Element} */ _createAnchor: function(uiSourceCode, lineNumber, columnNumber) { var anchor = document.createElement("a"); anchor.preferredPanel = "scripts"; anchor.href = sanitizeHref(uiSourceCode.originURL()); anchor.uiSourceCode = uiSourceCode; anchor.lineNumber = lineNumber; return anchor; }, /** * @param {WebInspector.FileBasedSearchResultsPane.SearchResult} searchResult */ addSearchResult: function(searchResult) { this._searchResults.push(searchResult); var uiSourceCode = searchResult.uiSourceCode; if (!uiSourceCode) return; var searchMatches = searchResult.searchMatches; var fileTreeElement = this._addFileTreeElement(uiSourceCode.fullDisplayName(), searchMatches.length, this._searchResults.length - 1); }, /** * @param {WebInspector.FileBasedSearchResultsPane.SearchResult} searchResult * @param {TreeElement} fileTreeElement */ _fileTreeElementExpanded: function(searchResult, fileTreeElement) { if (fileTreeElement._initialized) return; var toIndex = Math.min(searchResult.searchMatches.length, WebInspector.FileBasedSearchResultsPane.fileMatchesShownAtOnce); if (toIndex < searchResult.searchMatches.length) { this._appendSearchMatches(fileTreeElement, searchResult, 0, toIndex - 1); this._appendShowMoreMatchesElement(fileTreeElement, searchResult, toIndex - 1); } else this._appendSearchMatches(fileTreeElement, searchResult, 0, toIndex); fileTreeElement._initialized = true; }, /** * @param {TreeElement} fileTreeElement * @param {WebInspector.FileBasedSearchResultsPane.SearchResult} searchResult * @param {number} fromIndex * @param {number} toIndex */ _appendSearchMatches: function(fileTreeElement, searchResult, fromIndex, toIndex) { var uiSourceCode = searchResult.uiSourceCode; var searchMatches = searchResult.searchMatches; var regex = createSearchRegex(this._searchConfig.query, !this._searchConfig.ignoreCase, this._searchConfig.isRegex); for (var i = fromIndex; i < toIndex; ++i) { var lineNumber = searchMatches[i].lineNumber; var lineContent = searchMatches[i].lineContent; var matchRanges = this._regexMatchRanges(lineContent, regex); var anchor = this._createAnchor(uiSourceCode, lineNumber, matchRanges[0].offset); var numberString = numberToStringWithSpacesPadding(lineNumber + 1, 4); var lineNumberSpan = document.createElement("span"); lineNumberSpan.addStyleClass("search-match-line-number"); lineNumberSpan.textContent = numberString; anchor.appendChild(lineNumberSpan); var contentSpan = this._createContentSpan(lineContent, matchRanges); anchor.appendChild(contentSpan); var searchMatchElement = new TreeElement("", null, false); searchMatchElement.selectable = false; fileTreeElement.appendChild(searchMatchElement); searchMatchElement.listItemElement.className = "search-match source-code"; searchMatchElement.listItemElement.appendChild(anchor); } }, /** * @param {TreeElement} fileTreeElement * @param {WebInspector.FileBasedSearchResultsPane.SearchResult} searchResult * @param {number} startMatchIndex */ _appendShowMoreMatchesElement: function(fileTreeElement, searchResult, startMatchIndex) { var matchesLeftCount = searchResult.searchMatches.length - startMatchIndex; var showMoreMatchesText = WebInspector.UIString("Show all matches (%d more).", matchesLeftCount); var showMoreMatchesElement = new TreeElement(showMoreMatchesText, null, false); fileTreeElement.appendChild(showMoreMatchesElement); showMoreMatchesElement.listItemElement.addStyleClass("show-more-matches"); showMoreMatchesElement.onselect = this._showMoreMatchesElementSelected.bind(this, searchResult, startMatchIndex, showMoreMatchesElement); }, /** * @param {WebInspector.FileBasedSearchResultsPane.SearchResult} searchResult * @param {number} startMatchIndex * @param {TreeElement} showMoreMatchesElement */ _showMoreMatchesElementSelected: function(searchResult, startMatchIndex, showMoreMatchesElement) { var fileTreeElement = showMoreMatchesElement.parent; fileTreeElement.removeChild(showMoreMatchesElement); this._appendSearchMatches(fileTreeElement, searchResult, startMatchIndex, searchResult.searchMatches.length); }, /** * @param {string} fileName * @param {number} searchMatchesCount * @param {number} searchResultIndex */ _addFileTreeElement: function(fileName, searchMatchesCount, searchResultIndex) { var fileTreeElement = new TreeElement("", null, true); fileTreeElement.toggleOnClick = true; fileTreeElement.selectable = false; this._treeOutline.appendChild(fileTreeElement); fileTreeElement.listItemElement.addStyleClass("search-result"); var fileNameSpan = document.createElement("span"); fileNameSpan.className = "search-result-file-name"; fileNameSpan.textContent = fileName; fileTreeElement.listItemElement.appendChild(fileNameSpan); var matchesCountSpan = document.createElement("span"); matchesCountSpan.className = "search-result-matches-count"; if (searchMatchesCount === 1) matchesCountSpan.textContent = WebInspector.UIString("(%d match)", searchMatchesCount); else matchesCountSpan.textContent = WebInspector.UIString("(%d matches)", searchMatchesCount); fileTreeElement.listItemElement.appendChild(matchesCountSpan); var searchResult = this._searchResults[searchResultIndex]; fileTreeElement.onexpand = this._fileTreeElementExpanded.bind(this, searchResult, fileTreeElement); // Expand until at least certain amount of matches is expanded. if (this._matchesExpandedCount < WebInspector.FileBasedSearchResultsPane.matchesExpandedByDefaultCount) fileTreeElement.expand(); this._matchesExpandedCount += searchResult.searchMatches.length; return fileTreeElement; }, /** * @param {string} lineContent * @param {RegExp} regex * @return {Array.} */ _regexMatchRanges: function(lineContent, regex) { regex.lastIndex = 0; var match; var offset = 0; var matchRanges = []; while ((regex.lastIndex < lineContent.length) && (match = regex.exec(lineContent))) matchRanges.push(new WebInspector.SearchResult(match.index, match[0].length)); return matchRanges; }, /** * @param {string} lineContent * @param {Array.} matchRanges */ _createContentSpan: function(lineContent, matchRanges) { var contentSpan = document.createElement("span"); contentSpan.className = "search-match-content"; contentSpan.textContent = lineContent; WebInspector.highlightRangesWithStyleClass(contentSpan, matchRanges, "highlighted-match"); return contentSpan; }, __proto__: WebInspector.SearchResultsPane.prototype } /** * @constructor * @param {WebInspector.UISourceCode} uiSourceCode * @param {Array.} searchMatches */ WebInspector.FileBasedSearchResultsPane.SearchResult = function(uiSourceCode, searchMatches) { this.uiSourceCode = uiSourceCode; this.searchMatches = searchMatches; } /** * @type {WebInspector.AdvancedSearchController} */ WebInspector.advancedSearchController = null;