Web Inspector: Search: no results when opening to Search tab
[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 WI.SearchSidebarPanel = class SearchSidebarPanel extends WI.NavigationSidebarPanel
27 {
28     constructor()
29     {
30         super("search", WI.UIString("Search"), true, true);
31
32         this._searchInputSettings = WI.SearchUtilities.createSettings("search-sidebar", {
33             handleChanged: (event) => {
34                 this.focusSearchField(true);
35             },
36         });
37
38         var searchElement = document.createElement("div");
39         searchElement.classList.add("search-bar");
40         this.element.appendChild(searchElement);
41
42         this._inputElement = document.createElement("input");
43         this._inputElement.type = "search";
44         this._inputElement.spellcheck = false;
45         this._inputElement.addEventListener("search", this._searchFieldChanged.bind(this));
46         this._inputElement.addEventListener("input", this._searchFieldInput.bind(this));
47         this._inputElement.setAttribute("results", 5);
48         this._inputElement.setAttribute("autosave", "inspector-search-autosave");
49         this._inputElement.setAttribute("placeholder", WI.UIString("Search Resource Content"));
50         searchElement.appendChild(this._inputElement);
51
52         searchElement.appendChild(WI.SearchUtilities.createSettingsButton(this._searchInputSettings));
53
54         this._searchQuerySetting = new WI.Setting("search-sidebar-query", "");
55         this._inputElement.value = this._searchQuerySetting.value;
56
57         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
58
59         this.contentTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this);
60     }
61
62     // Public
63
64     closed()
65     {
66         super.closed();
67
68         WI.Frame.removeEventListener(null, null, this);
69     }
70
71     focusSearchField(performSearch)
72     {
73         if (!this.parentSidebar)
74             return;
75
76         this.parentSidebar.selectedSidebarPanel = this;
77         this.parentSidebar.collapsed = false;
78
79         this._inputElement.select();
80
81         if (performSearch)
82             this.performSearch(this._inputElement.value);
83     }
84
85     performSearch(searchQuery)
86     {
87         // Before performing a new search, clear the old search.
88         this.contentTreeOutline.removeChildren();
89         this.contentBrowser.contentViewContainer.closeAllContentViews();
90
91         this._inputElement.value = searchQuery;
92         this._searchQuerySetting.value = searchQuery;
93
94         this.hideEmptyContentPlaceholder();
95
96         this.element.classList.remove("changed");
97         if (this._changedBanner)
98             this._changedBanner.remove();
99
100         if (!searchQuery.length)
101             return;
102
103         let createSearchingPlaceholder = () => {
104             let searchingPlaceholder = WI.createMessageTextView("");
105             String.format(WI.UIString("Searching %s"), [(new WI.IndeterminateProgressSpinner).element], String.standardFormatters, searchingPlaceholder, (a, b) => {
106                 a.append(b);
107                 return a;
108             });
109             this.updateEmptyContentPlaceholder(searchingPlaceholder);
110         };
111
112         if (!WI.targetsAvailable()) {
113             createSearchingPlaceholder();
114             WI.whenTargetsAvailable().then(() => {
115                 if (this._searchQuerySetting.value === searchQuery)
116                     this.performSearch(searchQuery);
117             });
118             return;
119         }
120
121         let promiseCount = 0;
122         let countPromise = async (promise, callback) => {
123             ++promiseCount;
124             if (promiseCount === 1)
125                 createSearchingPlaceholder();
126
127             let value = await promise;
128
129             if (callback)
130                 callback(value);
131
132             --promiseCount;
133             console.assert(promiseCount >= 0);
134             if (promiseCount === 0)
135                 this.updateEmptyContentPlaceholder(WI.UIString("No Search Results"));
136         };
137
138         let isCaseSensitive = !!this._searchInputSettings.caseSensitive.value;
139         let isRegex = !!this._searchInputSettings.regularExpression.value;
140
141         function createTreeElementForMatchObject(matchObject, parentTreeElement)
142         {
143             let matchTreeElement = new WI.SearchResultTreeElement(matchObject);
144             matchTreeElement.addEventListener(WI.TreeElement.Event.DoubleClick, this._treeElementDoubleClick, this);
145
146             parentTreeElement.appendChild(matchTreeElement);
147
148             if (!this.contentTreeOutline.selectedTreeElement)
149                 matchTreeElement.revealAndSelect(false, true);
150         }
151
152         function forEachMatch(searchQuery, lineContent, callback)
153         {
154             var lineMatch;
155             let searchRegex = WI.SearchUtilities.regExpForString(searchQuery, {
156                 caseSensitive: isCaseSensitive,
157                 regularExpression: isRegex,
158             });
159             while ((searchRegex.lastIndex < lineContent.length) && (lineMatch = searchRegex.exec(lineContent)))
160                 callback(lineMatch, searchRegex.lastIndex);
161         }
162
163         let resourceCallback = (frameId, url, {result}) => {
164             if (!result || !result.length)
165                 return;
166
167             var frame = WI.networkManager.frameForIdentifier(frameId);
168             if (!frame)
169                 return;
170
171             var resource = frame.url === url ? frame.mainResource : frame.resourceForURL(url);
172             if (!resource)
173                 return;
174
175             var resourceTreeElement = this._searchTreeElementForResource(resource);
176
177             for (var i = 0; i < result.length; ++i) {
178                 var match = result[i];
179                 forEachMatch(searchQuery, match.lineContent, (lineMatch, lastIndex) => {
180                     var matchObject = new WI.SourceCodeSearchMatchObject(resource, match.lineContent, searchQuery, new WI.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex));
181                     createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement);
182                 });
183             }
184
185             if (!resourceTreeElement.children.length)
186                 this.contentTreeOutline.removeChild(resourceTreeElement);
187         };
188
189         let resourcesCallback = ({result}) => {
190             let preventDuplicates = new Set;
191
192             for (let searchResult of result) {
193                 if (!searchResult.url || !searchResult.frameId)
194                     continue;
195
196                 // FIXME: Backend sometimes searches files twice.
197                 // <https://webkit.org/b/188287> Web Inspector: [Backend] Page.searchInResources sometimes returns duplicate results for a resource
198                 // Note we will still want this to fix legacy backends.
199                 let key = searchResult.frameId + ":" + searchResult.url;
200                 if (preventDuplicates.has(key))
201                     continue;
202                 preventDuplicates.add(key);
203
204                 // COMPATIBILITY (iOS 9): Page.searchInResources did not have the optional requestId parameter.
205                 countPromise(PageAgent.searchInResource(searchResult.frameId, searchResult.url, searchQuery, isCaseSensitive, isRegex, searchResult.requestId), resourceCallback.bind(this, searchResult.frameId, searchResult.url));
206             }
207
208             let promises = [
209                 WI.Frame.awaitEvent(WI.Frame.Event.ResourceWasAdded),
210                 WI.Target.awaitEvent(WI.Target.Event.ResourceAdded)
211             ];
212             Promise.race(promises).then(this._contentChanged.bind(this));
213         };
214
215         let scriptCallback = (script, {result}) => {
216             if (!result || !result.length)
217                 return;
218
219             var scriptTreeElement = this._searchTreeElementForScript(script);
220
221             for (let match of result) {
222                 forEachMatch(searchQuery, match.lineContent, (lineMatch, lastIndex) => {
223                     var matchObject = new WI.SourceCodeSearchMatchObject(script, match.lineContent, searchQuery, new WI.TextRange(match.lineNumber, lineMatch.index, match.lineNumber, lastIndex));
224                     createTreeElementForMatchObject.call(this, matchObject, scriptTreeElement);
225                 });
226             }
227
228             if (!scriptTreeElement.children.length)
229                 this.contentTreeOutline.removeChild(scriptTreeElement);
230         };
231
232         let searchScripts = (scriptsToSearch) => {
233             if (!scriptsToSearch.length)
234                 return;
235
236             for (let script of scriptsToSearch)
237                 countPromise(script.target.DebuggerAgent.searchInContent(script.id, searchQuery, isCaseSensitive, isRegex), scriptCallback.bind(this, script));
238         };
239
240         let domCallback = ({searchId, resultCount}) => {
241             if (!resultCount)
242                 return;
243
244             console.assert(searchId);
245
246             this._domSearchIdentifier = searchId;
247
248             let domSearchResults = ({nodeIds}) => {
249                 // If someone started a new search, then return early and stop showing seach results from the old query.
250                 if (this._domSearchIdentifier !== searchId)
251                     return;
252
253                 for (let nodeId of nodeIds) {
254                     let domNode = WI.domManager.nodeForId(nodeId);
255                     if (!domNode || !domNode.ownerDocument)
256                         continue;
257
258                     // 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.
259                     if (domNode.nodeType() === Node.DOCUMENT_NODE)
260                         continue;
261
262                     // FIXME: This should use a frame to do resourceForURL, but DOMAgent does not provide a frameId.
263                     var resource = WI.networkManager.resourceForURL(domNode.ownerDocument.documentURL);
264                     if (!resource)
265                         continue;
266
267                     var resourceTreeElement = this._searchTreeElementForResource(resource);
268                     var domNodeTitle = WI.DOMSearchMatchObject.titleForDOMNode(domNode);
269
270                     // Textual matches.
271                     var didFindTextualMatch = false;
272                     forEachMatch(searchQuery, domNodeTitle, (lineMatch, lastIndex) => {
273                         var matchObject = new WI.DOMSearchMatchObject(resource, domNode, domNodeTitle, searchQuery, new WI.TextRange(0, lineMatch.index, 0, lastIndex));
274                         createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement);
275                         didFindTextualMatch = true;
276                     });
277
278                     // Non-textual matches are CSS Selector or XPath matches. In such cases, display the node entirely highlighted.
279                     if (!didFindTextualMatch) {
280                         var matchObject = new WI.DOMSearchMatchObject(resource, domNode, domNodeTitle, domNodeTitle, new WI.TextRange(0, 0, 0, domNodeTitle.length));
281                         createTreeElementForMatchObject.call(this, matchObject, resourceTreeElement);
282                     }
283
284                     if (!resourceTreeElement.children.length)
285                         this.contentTreeOutline.removeChild(resourceTreeElement);
286
287                 }
288             };
289
290             countPromise(DOMAgent.getSearchResults(searchId, 0, resultCount), domSearchResults);
291         };
292
293         if (window.DOMAgent)
294             WI.domManager.ensureDocument();
295
296         if (window.PageAgent)
297             countPromise(PageAgent.searchInResources(searchQuery, isCaseSensitive, isRegex), resourcesCallback);
298
299         setTimeout(searchScripts.bind(this, WI.debuggerManager.searchableScripts), 0);
300
301         if (window.DOMAgent) {
302             if (this._domSearchIdentifier) {
303                 DOMAgent.discardSearchResults(this._domSearchIdentifier);
304                 this._domSearchIdentifier = undefined;
305             }
306
307             countPromise(DOMAgent.performSearch(searchQuery), domCallback);
308         }
309
310         // FIXME: Resource search should work in JSContext inspection.
311         // <https://webkit.org/b/131252> Web Inspector: JSContext inspection Resource search does not work
312     }
313
314     // Private
315
316     _searchFieldChanged(event)
317     {
318         this.performSearch(event.target.value);
319     }
320
321     _searchFieldInput(event)
322     {
323         // If the search field is cleared, immediately clear the search results tree outline.
324         if (!event.target.value.length)
325             this.performSearch("");
326     }
327
328     _searchTreeElementForResource(resource)
329     {
330         var resourceTreeElement = this.contentTreeOutline.getCachedTreeElement(resource);
331         if (!resourceTreeElement) {
332             resourceTreeElement = new WI.ResourceTreeElement(resource);
333             resourceTreeElement.hasChildren = true;
334             resourceTreeElement.expand();
335
336             this.contentTreeOutline.appendChild(resourceTreeElement);
337         }
338
339         return resourceTreeElement;
340     }
341
342     _searchTreeElementForScript(script)
343     {
344         var scriptTreeElement = this.contentTreeOutline.getCachedTreeElement(script);
345         if (!scriptTreeElement) {
346             scriptTreeElement = new WI.ScriptTreeElement(script);
347             scriptTreeElement.hasChildren = true;
348             scriptTreeElement.expand();
349
350             this.contentTreeOutline.appendChild(scriptTreeElement);
351         }
352
353         return scriptTreeElement;
354     }
355
356     _mainResourceDidChange(event)
357     {
358         if (!event.target.isMainFrame())
359             return;
360
361         if (this._delayedSearchTimeout) {
362             clearTimeout(this._delayedSearchTimeout);
363             this._delayedSearchTimeout = undefined;
364         }
365
366         this.contentTreeOutline.removeChildren();
367         this.contentBrowser.contentViewContainer.closeAllContentViews();
368
369         if (this.visible) {
370             const performSearch = true;
371             this.focusSearchField(performSearch);
372         }
373     }
374
375     _treeSelectionDidChange(event)
376     {
377         if (!this.selected)
378             return;
379
380         let treeElement = this.contentTreeOutline.selectedTreeElement;
381         if (!treeElement || treeElement instanceof WI.FolderTreeElement)
382             return;
383
384         const options = {
385             ignoreNetworkTab: true,
386         };
387
388         if (treeElement instanceof WI.ResourceTreeElement || treeElement instanceof WI.ScriptTreeElement) {
389             const cookie = null;
390             WI.showRepresentedObject(treeElement.representedObject, cookie, options);
391             return;
392         }
393
394         console.assert(treeElement instanceof WI.SearchResultTreeElement);
395         if (!(treeElement instanceof WI.SearchResultTreeElement))
396             return;
397
398         if (treeElement.representedObject instanceof WI.DOMSearchMatchObject)
399             WI.showMainFrameDOMTree(treeElement.representedObject.domNode);
400         else if (treeElement.representedObject instanceof WI.SourceCodeSearchMatchObject)
401             WI.showOriginalOrFormattedSourceCodeTextRange(treeElement.representedObject.sourceCodeTextRange, options);
402     }
403
404     _treeElementDoubleClick(event)
405     {
406         let treeElement = event.target;
407         if (!treeElement)
408             return;
409
410         if (treeElement.representedObject instanceof WI.DOMSearchMatchObject) {
411             WI.showMainFrameDOMTree(treeElement.representedObject.domNode, {
412                 ignoreSearchTab: true,
413             });
414         } else if (treeElement.representedObject instanceof WI.SourceCodeSearchMatchObject) {
415             WI.showOriginalOrFormattedSourceCodeTextRange(treeElement.representedObject.sourceCodeTextRange, {
416                 ignoreNetworkTab: true,
417                 ignoreSearchTab: true,
418             });
419         }
420     }
421
422     _contentChanged(event)
423     {
424         this.element.classList.add("changed");
425
426         if (!this._changedBanner) {
427             this._changedBanner = document.createElement("div");
428             this._changedBanner.classList.add("banner");
429             this._changedBanner.append(WI.UIString("The page's content has changed"), document.createElement("br"));
430
431             let performSearchLink = this._changedBanner.appendChild(document.createElement("a"));
432             performSearchLink.textContent = WI.UIString("Search Again");
433             performSearchLink.addEventListener("click", () => {
434                 const performSearch = true;
435                 this.focusSearchField(performSearch);
436             });
437         }
438
439         this.element.appendChild(this._changedBanner);
440     }
441 };