Web Inspector: Search Results tab causes jump to Resources tab on reload
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SearchSidebarPanel.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.SearchSidebarPanel = class SearchSidebarPanel extends WebInspector.NavigationSidebarPanel
27 {
28     constructor(contentBrowser)
29     {
30         super("search", WebInspector.UIString("Search"), true, true);
31
32         this.contentBrowser = contentBrowser;
33
34         var searchElement = document.createElement("div");
35         searchElement.classList.add("search-bar");
36         this.element.appendChild(searchElement);
37
38         this._inputElement = document.createElement("input");
39         this._inputElement.type = "search";
40         this._inputElement.spellcheck = false;
41         this._inputElement.addEventListener("search", this._searchFieldChanged.bind(this));
42         this._inputElement.addEventListener("input", this._searchFieldInput.bind(this));
43         this._inputElement.setAttribute("results", 5);
44         this._inputElement.setAttribute("autosave", "inspector-search-autosave");
45         this._inputElement.setAttribute("placeholder", WebInspector.UIString("Search Resource Content"));
46         searchElement.appendChild(this._inputElement);
47
48         this.filterBar.placeholder = WebInspector.UIString("Filter Search Results");
49
50         this._searchQuerySetting = new WebInspector.Setting("search-sidebar-query", "");
51         this._inputElement.value = this._searchQuerySetting.value;
52
53         WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
54
55         this.contentTreeOutline.onselect = this._treeElementSelected.bind(this);
56     }
57
58     // Public
59
60     closed()
61     {
62         super.closed();
63
64         WebInspector.Frame.removeEventListener(null, null, this);
65     }
66
67     focusSearchField()
68     {
69         this.show();
70
71         this._inputElement.select();
72     }
73
74     performSearch(searchQuery)
75     {
76         // Before performing a new search, clear the old search.
77         this.contentTreeOutline.removeChildren();
78         this.contentBrowser.contentViewContainer.closeAllContentViews();
79
80         this._inputElement.value = searchQuery;
81         this._searchQuerySetting.value = searchQuery;
82
83         this.hideEmptyContentPlaceholder();
84
85         searchQuery = searchQuery.trim();
86         if (!searchQuery.length)
87             return;
88
89         // FIXME: Provide UI to toggle regex and case sensitive searches.
90         var isCaseSensitive = false;
91         var isRegex = false;
92
93         var updateEmptyContentPlaceholderTimeout = null;
94
95         function updateEmptyContentPlaceholderSoon()
96         {
97             if (updateEmptyContentPlaceholderTimeout)
98                 return;
99             updateEmptyContentPlaceholderTimeout = setTimeout(updateEmptyContentPlaceholder.bind(this), 100);
100         }
101
102         function updateEmptyContentPlaceholder()
103         {
104             if (updateEmptyContentPlaceholderTimeout) {
105                 clearTimeout(updateEmptyContentPlaceholderTimeout);
106                 updateEmptyContentPlaceholderTimeout = null;
107             }
108
109             this.updateEmptyContentPlaceholder(WebInspector.UIString("No Search Results"));
110         }
111
112         function forEachMatch(searchQuery, lineContent, callback)
113         {
114             var lineMatch;
115             var searchRegex = new RegExp(searchQuery.escapeForRegExp(), "gi");
116             while ((searchRegex.lastIndex < lineContent.length) && (lineMatch = searchRegex.exec(lineContent)))
117                 callback(lineMatch, searchRegex.lastIndex);
118         }
119
120         function resourcesCallback(error, result)
121         {
122             updateEmptyContentPlaceholderSoon.call(this);
123
124             if (error)
125                 return;
126
127             function resourceCallback(url, error, resourceMatches)
128             {
129                 updateEmptyContentPlaceholderSoon.call(this);
130
131                 if (error || !resourceMatches || !resourceMatches.length)
132                     return;
133
134                 var frame = WebInspector.frameResourceManager.frameForIdentifier(searchResult.frameId);
135                 if (!frame)
136                     return;
137
138                 var resource = frame.url === url ? frame.mainResource : frame.resourceForURL(url);
139                 if (!resource)
140                     return;
141
142                 var resourceTreeElement = this._searchTreeElementForResource(resource);
143
144                 for (var i = 0; i < resourceMatches.length; ++i) {
145                     var match = resourceMatches[i];
146                     forEachMatch(searchQuery, match.lineContent, function(lineMatch, lastIndex) {
147                         var matchObject = new WebInspector.SourceCodeSearchMatchObject(resource, match.lineContent, searchQuery, new WebInspector.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex));
148                         var matchTreeElement = new WebInspector.SearchResultTreeElement(matchObject);
149                         resourceTreeElement.appendChild(matchTreeElement);
150                         if (!this.contentTreeOutline.selectedTreeElement)
151                             matchTreeElement.revealAndSelect(false, true);
152                     }.bind(this));
153                 }
154
155                 updateEmptyContentPlaceholder.call(this);
156             }
157
158             for (var i = 0; i < result.length; ++i) {
159                 var searchResult = result[i];
160                 if (!searchResult.url || !searchResult.frameId)
161                     continue;
162
163                 PageAgent.searchInResource(searchResult.frameId, searchResult.url, searchQuery, isCaseSensitive, isRegex, resourceCallback.bind(this, searchResult.url));
164             }
165         }
166
167         function searchScripts(scriptsToSearch)
168         {
169             updateEmptyContentPlaceholderSoon.call(this);
170
171             if (!scriptsToSearch.length)
172                 return;
173
174             function scriptCallback(script, error, scriptMatches)
175             {
176                 updateEmptyContentPlaceholderSoon.call(this);
177
178                 if (error || !scriptMatches || !scriptMatches.length)
179                     return;
180
181                 var scriptTreeElement = this._searchTreeElementForScript(script);
182
183                 for (var i = 0; i < scriptMatches.length; ++i) {
184                     var match = scriptMatches[i];
185                     forEachMatch(searchQuery, match.lineContent, function(lineMatch, lastIndex) {
186                         var matchObject = new WebInspector.SourceCodeSearchMatchObject(script, match.lineContent, searchQuery, new WebInspector.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex));
187                         var matchTreeElement = new WebInspector.SearchResultTreeElement(matchObject);
188                         scriptTreeElement.appendChild(matchTreeElement);
189                         if (!this.contentTreeOutline.selectedTreeElement)
190                             matchTreeElement.revealAndSelect(false, true);
191                     }.bind(this));
192                 }
193
194                 updateEmptyContentPlaceholder.call(this);
195             }
196
197             for (var script of scriptsToSearch)
198                 DebuggerAgent.searchInContent(script.id, searchQuery, isCaseSensitive, isRegex, scriptCallback.bind(this, script));
199         }
200
201         function domCallback(error, searchId, resultsCount)
202         {
203             updateEmptyContentPlaceholderSoon.call(this);
204
205             if (error || !resultsCount)
206                 return;
207
208             console.assert(searchId);
209
210             this._domSearchIdentifier = searchId;
211
212             function domSearchResults(error, nodeIds)
213             {
214                 updateEmptyContentPlaceholderSoon.call(this);
215
216                 if (error)
217                     return;
218
219                 for (var i = 0; i < nodeIds.length; ++i) {
220                     // If someone started a new search, then return early and stop showing seach results from the old query.
221                     if (this._domSearchIdentifier !== searchId)
222                         return;
223
224                     var domNode = WebInspector.domTreeManager.nodeForId(nodeIds[i]);
225                     if (!domNode || !domNode.ownerDocument)
226                         continue;
227
228                     // We do not display the document node when the search query is "/". We don't have anything to display in the content view for it.
229                     if (domNode.nodeType() === Node.DOCUMENT_NODE)
230                         continue;
231
232                     // FIXME: This should use a frame to do resourceForURL, but DOMAgent does not provide a frameId.
233                     var resource = WebInspector.frameResourceManager.resourceForURL(domNode.ownerDocument.documentURL);
234                     if (!resource)
235                         continue;
236
237                     var resourceTreeElement = this._searchTreeElementForResource(resource);
238                     var domNodeTitle = WebInspector.DOMSearchMatchObject.titleForDOMNode(domNode);
239
240                     // Textual matches.
241                     var didFindTextualMatch = false;
242                     forEachMatch(searchQuery, domNodeTitle, function(lineMatch, lastIndex) {
243                         var matchObject = new WebInspector.DOMSearchMatchObject(resource, domNode, domNodeTitle, searchQuery, new WebInspector.TextRange(0, lineMatch.index, 0, lastIndex));
244                         var matchTreeElement = new WebInspector.SearchResultTreeElement(matchObject);
245                         resourceTreeElement.appendChild(matchTreeElement);
246                         if (!this.contentTreeOutline.selectedTreeElement)
247                             matchTreeElement.revealAndSelect(false, true);
248                         didFindTextualMatch = true;
249                     }.bind(this));
250
251                     // Non-textual matches are CSS Selector or XPath matches. In such cases, display the node entirely highlighted.
252                     if (!didFindTextualMatch) {
253                         var matchObject = new WebInspector.DOMSearchMatchObject(resource, domNode, domNodeTitle, domNodeTitle, new WebInspector.TextRange(0, 0, 0, domNodeTitle.length));
254                         var matchTreeElement = new WebInspector.SearchResultTreeElement(matchObject);
255                         resourceTreeElement.appendChild(matchTreeElement);
256                         if (!this.contentTreeOutline.selectedTreeElement)
257                             matchTreeElement.revealAndSelect(false, true);
258                     }
259
260                     updateEmptyContentPlaceholder.call(this);
261                 }
262             }
263
264             DOMAgent.getSearchResults(searchId, 0, resultsCount, domSearchResults.bind(this));
265         }
266
267         if (window.DOMAgent)
268             WebInspector.domTreeManager.requestDocument(function(){});
269
270         if (window.PageAgent)
271             PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex, resourcesCallback.bind(this));
272
273         setTimeout(searchScripts.bind(this, WebInspector.debuggerManager.knownNonResourceScripts), 0);
274
275         if (window.DOMAgent) {
276             if (this._domSearchIdentifier) {
277                 DOMAgent.discardSearchResults(this._domSearchIdentifier);
278                 this._domSearchIdentifier = undefined;
279             }
280
281             DOMAgent.performSearch(searchQuery, domCallback.bind(this));
282         }
283
284         // FIXME: Resource search should work in JSContext inspection.
285         // <https://webkit.org/b/131252> Web Inspector: JSContext inspection Resource search does not work
286         if (!window.DOMAgent && !window.PageAgent)
287             updateEmptyContentPlaceholderSoon.call(this);
288     }
289
290     // Private
291
292     _searchFieldChanged(event)
293     {
294         this.performSearch(event.target.value);
295     }
296
297     _searchFieldInput(event)
298     {
299         // If the search field is cleared, immediately clear the search results tree outline.
300         if (!event.target.value.length)
301             this.performSearch("");
302     }
303
304     _searchTreeElementForResource(resource)
305     {
306         var resourceTreeElement = this.contentTreeOutline.getCachedTreeElement(resource);
307         if (!resourceTreeElement) {
308             resourceTreeElement = new WebInspector.ResourceTreeElement(resource);
309             resourceTreeElement.hasChildren = true;
310             resourceTreeElement.expand();
311
312             this.contentTreeOutline.appendChild(resourceTreeElement);
313         }
314
315         return resourceTreeElement;
316     }
317
318     _searchTreeElementForScript(script)
319     {
320         var scriptTreeElement = this.contentTreeOutline.getCachedTreeElement(script);
321         if (!scriptTreeElement) {
322             scriptTreeElement = new WebInspector.ScriptTreeElement(script);
323             scriptTreeElement.hasChildren = true;
324             scriptTreeElement.expand();
325
326             this.contentTreeOutline.appendChild(scriptTreeElement);
327         }
328
329         return scriptTreeElement;
330     }
331
332     _mainResourceDidChange(event)
333     {
334         if (!event.target.isMainFrame())
335             return;
336
337         if (this._delayedSearchTimeout) {
338             clearTimeout(this._delayedSearchTimeout);
339             this._delayedSearchTimeout = undefined;
340         }
341
342         this.contentTreeOutline.removeChildren();
343         this.contentBrowser.contentViewContainer.closeAllContentViews();
344
345         if (this.visible)
346             this.focusSearchField();
347     }
348
349     _treeElementSelected(treeElement, selectedByUser)
350     {
351         if (treeElement instanceof WebInspector.FolderTreeElement)
352             return;
353
354         if (treeElement instanceof WebInspector.ResourceTreeElement || treeElement instanceof WebInspector.ScriptTreeElement) {
355             WebInspector.showRepresentedObject(treeElement.representedObject);
356             return;
357         }
358
359         console.assert(treeElement instanceof WebInspector.SearchResultTreeElement);
360         if (!(treeElement instanceof WebInspector.SearchResultTreeElement))
361             return;
362
363         if (treeElement.representedObject instanceof WebInspector.DOMSearchMatchObject)
364             WebInspector.showMainFrameDOMTree(treeElement.representedObject.domNode);
365         else if (treeElement.representedObject instanceof WebInspector.SourceCodeSearchMatchObject)
366             WebInspector.showOriginalOrFormattedSourceCodeTextRange(treeElement.representedObject.sourceCodeTextRange);
367     }
368 };