1a1b16873ec5ffc0465fd428b3d9dc5b4ece7c08
[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._lastSearchedPageSetting = new WebInspector.Setting("last-searched-page", null);
51
52         this._searchQuerySetting = new WebInspector.Setting("search-sidebar-query", "");
53         this._inputElement.value = this._searchQuerySetting.value;
54
55         WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
56
57         this.contentTreeOutline.onselect = this._treeElementSelected.bind(this);
58     }
59
60     // Public
61
62     closed()
63     {
64         super.closed();
65
66         WebInspector.Frame.removeEventListener(null, null, this);
67     }
68
69     focusSearchField()
70     {
71         this.show();
72
73         this._inputElement.select();
74     }
75
76     performSearch(searchQuery)
77     {
78         // Before performing a new search, clear the old search.
79         this.contentTreeOutline.removeChildren();
80         this.contentBrowser.contentViewContainer.closeAllContentViews();
81
82         this._inputElement.value = searchQuery;
83         this._searchQuerySetting.value = searchQuery;
84         this._lastSearchedPageSetting.value = searchQuery && WebInspector.frameResourceManager.mainFrame ? WebInspector.frameResourceManager.mainFrame.url.hash : null;
85
86         this.hideEmptyContentPlaceholder();
87
88         searchQuery = searchQuery.trim();
89         if (!searchQuery.length)
90             return;
91
92         // FIXME: Provide UI to toggle regex and case sensitive searches.
93         var isCaseSensitive = false;
94         var isRegex = false;
95
96         var updateEmptyContentPlaceholderTimeout = null;
97
98         function updateEmptyContentPlaceholderSoon()
99         {
100             if (updateEmptyContentPlaceholderTimeout)
101                 return;
102             updateEmptyContentPlaceholderTimeout = setTimeout(updateEmptyContentPlaceholder.bind(this), 100);
103         }
104
105         function updateEmptyContentPlaceholder()
106         {
107             if (updateEmptyContentPlaceholderTimeout) {
108                 clearTimeout(updateEmptyContentPlaceholderTimeout);
109                 updateEmptyContentPlaceholderTimeout = null;
110             }
111
112             this.updateEmptyContentPlaceholder(WebInspector.UIString("No Search Results"));
113         }
114
115         function forEachMatch(searchQuery, lineContent, callback)
116         {
117             var lineMatch;
118             var searchRegex = new RegExp(searchQuery.escapeForRegExp(), "gi");
119             while ((searchRegex.lastIndex < lineContent.length) && (lineMatch = searchRegex.exec(lineContent)))
120                 callback(lineMatch, searchRegex.lastIndex);
121         }
122
123         function resourcesCallback(error, result)
124         {
125             updateEmptyContentPlaceholderSoon.call(this);
126
127             if (error)
128                 return;
129
130             function resourceCallback(url, error, resourceMatches)
131             {
132                 updateEmptyContentPlaceholderSoon.call(this);
133
134                 if (error || !resourceMatches || !resourceMatches.length)
135                     return;
136
137                 var frame = WebInspector.frameResourceManager.frameForIdentifier(searchResult.frameId);
138                 if (!frame)
139                     return;
140
141                 var resource = frame.url === url ? frame.mainResource : frame.resourceForURL(url);
142                 if (!resource)
143                     return;
144
145                 var resourceTreeElement = this._searchTreeElementForResource(resource);
146
147                 for (var i = 0; i < resourceMatches.length; ++i) {
148                     var match = resourceMatches[i];
149                     forEachMatch(searchQuery, match.lineContent, function(lineMatch, lastIndex) {
150                         var matchObject = new WebInspector.SourceCodeSearchMatchObject(resource, match.lineContent, searchQuery, new WebInspector.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex));
151                         var matchTreeElement = new WebInspector.SearchResultTreeElement(matchObject);
152                         resourceTreeElement.appendChild(matchTreeElement);
153                         if (!this.contentTreeOutline.selectedTreeElement)
154                             matchTreeElement.revealAndSelect(false, true);
155                     }.bind(this));
156                 }
157
158                 updateEmptyContentPlaceholder.call(this);
159             }
160
161             for (var i = 0; i < result.length; ++i) {
162                 var searchResult = result[i];
163                 if (!searchResult.url || !searchResult.frameId)
164                     continue;
165
166                 PageAgent.searchInResource(searchResult.frameId, searchResult.url, searchQuery, isCaseSensitive, isRegex, resourceCallback.bind(this, searchResult.url));
167             }
168         }
169
170         function searchScripts(scriptsToSearch)
171         {
172             updateEmptyContentPlaceholderSoon.call(this);
173
174             if (!scriptsToSearch.length)
175                 return;
176
177             function scriptCallback(script, error, scriptMatches)
178             {
179                 updateEmptyContentPlaceholderSoon.call(this);
180
181                 if (error || !scriptMatches || !scriptMatches.length)
182                     return;
183
184                 var scriptTreeElement = this._searchTreeElementForScript(script);
185
186                 for (var i = 0; i < scriptMatches.length; ++i) {
187                     var match = scriptMatches[i];
188                     forEachMatch(searchQuery, match.lineContent, function(lineMatch, lastIndex) {
189                         var matchObject = new WebInspector.SourceCodeSearchMatchObject(script, match.lineContent, searchQuery, new WebInspector.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex));
190                         var matchTreeElement = new WebInspector.SearchResultTreeElement(matchObject);
191                         scriptTreeElement.appendChild(matchTreeElement);
192                         if (!this.contentTreeOutline.selectedTreeElement)
193                             matchTreeElement.revealAndSelect(false, true);
194                     }.bind(this));
195                 }
196
197                 updateEmptyContentPlaceholder.call(this);
198             }
199
200             for (var script of scriptsToSearch)
201                 DebuggerAgent.searchInContent(script.id, searchQuery, isCaseSensitive, isRegex, scriptCallback.bind(this, script));
202         }
203
204         function domCallback(error, searchId, resultsCount)
205         {
206             updateEmptyContentPlaceholderSoon.call(this);
207
208             if (error || !resultsCount)
209                 return;
210
211             console.assert(searchId);
212
213             this._domSearchIdentifier = searchId;
214
215             function domSearchResults(error, nodeIds)
216             {
217                 updateEmptyContentPlaceholderSoon.call(this);
218
219                 if (error)
220                     return;
221
222                 for (var i = 0; i < nodeIds.length; ++i) {
223                     // If someone started a new search, then return early and stop showing seach results from the old query.
224                     if (this._domSearchIdentifier !== searchId)
225                         return;
226
227                     var domNode = WebInspector.domTreeManager.nodeForId(nodeIds[i]);
228                     if (!domNode || !domNode.ownerDocument)
229                         continue;
230
231                     // 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.
232                     if (domNode.nodeType() === Node.DOCUMENT_NODE)
233                         continue;
234
235                     // FIXME: This should use a frame to do resourceForURL, but DOMAgent does not provide a frameId.
236                     var resource = WebInspector.frameResourceManager.resourceForURL(domNode.ownerDocument.documentURL);
237                     if (!resource)
238                         continue;
239
240                     var resourceTreeElement = this._searchTreeElementForResource(resource);
241                     var domNodeTitle = WebInspector.DOMSearchMatchObject.titleForDOMNode(domNode);
242
243                     // Textual matches.
244                     var didFindTextualMatch = false;
245                     forEachMatch(searchQuery, domNodeTitle, function(lineMatch, lastIndex) {
246                         var matchObject = new WebInspector.DOMSearchMatchObject(resource, domNode, domNodeTitle, searchQuery, new WebInspector.TextRange(0, lineMatch.index, 0, lastIndex));
247                         var matchTreeElement = new WebInspector.SearchResultTreeElement(matchObject);
248                         resourceTreeElement.appendChild(matchTreeElement);
249                         if (!this.contentTreeOutline.selectedTreeElement)
250                             matchTreeElement.revealAndSelect(false, true);
251                         didFindTextualMatch = true;
252                     }.bind(this));
253
254                     // Non-textual matches are CSS Selector or XPath matches. In such cases, display the node entirely highlighted.
255                     if (!didFindTextualMatch) {
256                         var matchObject = new WebInspector.DOMSearchMatchObject(resource, domNode, domNodeTitle, domNodeTitle, new WebInspector.TextRange(0, 0, 0, domNodeTitle.length));
257                         var matchTreeElement = new WebInspector.SearchResultTreeElement(matchObject);
258                         resourceTreeElement.appendChild(matchTreeElement);
259                         if (!this.contentTreeOutline.selectedTreeElement)
260                             matchTreeElement.revealAndSelect(false, true);
261                     }
262
263                     updateEmptyContentPlaceholder.call(this);
264                 }
265             }
266
267             DOMAgent.getSearchResults(searchId, 0, resultsCount, domSearchResults.bind(this));
268         }
269
270         if (window.DOMAgent)
271             WebInspector.domTreeManager.requestDocument(function(){});
272
273         if (window.PageAgent)
274             PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex, resourcesCallback.bind(this));
275
276         setTimeout(searchScripts.bind(this, WebInspector.debuggerManager.knownNonResourceScripts), 0);
277
278         if (window.DOMAgent) {
279             if (this._domSearchIdentifier) {
280                 DOMAgent.discardSearchResults(this._domSearchIdentifier);
281                 this._domSearchIdentifier = undefined;
282             }
283
284             DOMAgent.performSearch(searchQuery, domCallback.bind(this));
285         }
286
287         // FIXME: Resource search should work in JSContext inspection.
288         // <https://webkit.org/b/131252> Web Inspector: JSContext inspection Resource search does not work
289         if (!window.DOMAgent && !window.PageAgent)
290             updateEmptyContentPlaceholderSoon.call(this);
291     }
292
293     // Private
294
295     _searchFieldChanged(event)
296     {
297         this.performSearch(event.target.value);
298     }
299
300     _searchFieldInput(event)
301     {
302         // If the search field is cleared, immediately clear the search results tree outline.
303         if (!event.target.value.length)
304             this.performSearch("");
305     }
306
307     _searchTreeElementForResource(resource)
308     {
309         var resourceTreeElement = this.contentTreeOutline.getCachedTreeElement(resource);
310         if (!resourceTreeElement) {
311             resourceTreeElement = new WebInspector.ResourceTreeElement(resource);
312             resourceTreeElement.hasChildren = true;
313             resourceTreeElement.expand();
314
315             this.contentTreeOutline.appendChild(resourceTreeElement);
316         }
317
318         return resourceTreeElement;
319     }
320
321     _searchTreeElementForScript(script)
322     {
323         var scriptTreeElement = this.contentTreeOutline.getCachedTreeElement(script);
324         if (!scriptTreeElement) {
325             scriptTreeElement = new WebInspector.ScriptTreeElement(script);
326             scriptTreeElement.hasChildren = true;
327             scriptTreeElement.expand();
328
329             this.contentTreeOutline.appendChild(scriptTreeElement);
330         }
331
332         return scriptTreeElement;
333     }
334
335     _mainResourceDidChange(event)
336     {
337         if (!event.target.isMainFrame())
338             return;
339
340         if (this._delayedSearchTimeout) {
341             clearTimeout(this._delayedSearchTimeout);
342             this._delayedSearchTimeout = undefined;
343         }
344
345         this.contentBrowser.contentViewContainer.closeAllContentViews();
346
347         if (this.visible)
348             this.focusSearchField();
349
350         // Only if the last page searched is the same as the current page.
351         var mainFrame = event.target;
352         if (this._lastSearchedPageSetting.value !== mainFrame.url.hash)
353             return;
354
355         function delayedWork()
356         {
357             this._delayedSearchTimeout = undefined;
358
359             // Search for whatever is in the input field. This was populated with the last used search term.
360             this.performSearch(this._inputElement.value);
361         }
362
363         // Perform the search on a delay so we have a better chance of finding subresource results.
364         this._delayedSearchTimeout = setTimeout(delayedWork.bind(this), 500);
365     }
366
367     _treeElementSelected(treeElement, selectedByUser)
368     {
369         if (treeElement instanceof WebInspector.FolderTreeElement)
370             return;
371
372         if (treeElement instanceof WebInspector.ResourceTreeElement || treeElement instanceof WebInspector.ScriptTreeElement) {
373             WebInspector.showRepresentedObject(treeElement.representedObject);
374             return;
375         }
376
377         console.assert(treeElement instanceof WebInspector.SearchResultTreeElement);
378         if (!(treeElement instanceof WebInspector.SearchResultTreeElement))
379             return;
380
381         if (treeElement.representedObject instanceof WebInspector.DOMSearchMatchObject)
382             WebInspector.showMainFrameDOMTree(treeElement.representedObject.domNode);
383         else if (treeElement.representedObject instanceof WebInspector.SourceCodeSearchMatchObject)
384             WebInspector.showOriginalOrFormattedSourceCodeTextRange(treeElement.representedObject.sourceCodeTextRange);
385     }
386 };