Bug 796135 - Provide some obvious UI for scripts filtering, r=past
authorVictor Porof <vporof@mozilla.com>
Mon, 10 Dec 2012 16:03:48 +0200
changeset 115918 4eb861243857
parent 115917 5911901b9bcf
child 115919 f7ead5ca7000
push id24033
push userjwalker@mozilla.com
push date2012-12-14 09:50 +0000
treeherdermozilla-central@8a30e07815ff [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs796135
milestone20.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 796135 - Provide some obvious UI for scripts filtering, r=past
browser/devtools/debugger/debugger-panes.js
browser/devtools/debugger/debugger-toolbar.js
browser/devtools/debugger/debugger-view.js
browser/devtools/debugger/debugger.xul
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js
browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js
browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js
browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js
browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js
browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js
browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js
browser/devtools/debugger/test/browser_dbg_scripts-searching-files_ui.js
browser/themes/gnomestripe/devtools/debugger.css
browser/themes/pinstripe/devtools/debugger.css
browser/themes/winstripe/devtools/debugger.css
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -1450,18 +1450,22 @@ create({ constructor: GlobalSearchView, 
       this._onFetchSourcesFinished();
     }
   },
 
   /**
    * Called when all the sources have been fetched.
    */
   _onFetchSourcesFinished: function DVGS__onFetchSourcesFinished() {
+    if (!this._sourcesCount) {
+      return;
+    }
     // All sources are fetched and stored in the cache, we can start searching.
     this._performGlobalSearch();
+    this._sourcesCount = 0;
   },
 
   /**
    * Finds string matches in all the  sources stored in the cache, and groups
    * them by location and line number.
    */
   _performGlobalSearch: function DVGS__performGlobalSearch() {
     // Get the currently searched token from the filtering input.
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -708,17 +708,17 @@ FilterView.prototype = {
    */
   get searchedToken() this.searchboxInfo[2],
 
   /**
    * Clears the text from the searchbox and resets any changed view.
    */
   clearSearch: function DVF_clearSearch() {
     this._searchbox.value = "";
-    this._onSearch();
+    this._searchboxPanel.hidePopup();
   },
 
   /**
    * Performs a file search if necessary.
    *
    * @param string aFile
    *        The source location to search for.
    */
@@ -762,16 +762,19 @@ FilterView.prototype = {
           element.hidden = true;
         }
       }
       // If no matches were found, display the appropriate info.
       if (!found) {
         view.setUnavailable();
       }
     }
+    // Synchronize with the view's filtered sources container.
+    DebuggerView.FilteredSources.syncFileSearch();
+
     this._prevSearchedFile = aFile;
   },
 
   /**
    * Performs a line search if necessary.
    * (Jump to lines in the currently visible source).
    *
    * @param number aLine
@@ -825,23 +828,25 @@ FilterView.prototype = {
   _onSearch: function DVF__onScriptsSearch() {
     this._searchboxPanel.hidePopup();
     let [file, line, token, isGlobal, isVariable] = this.searchboxInfo;
 
     // If this is a global search, schedule it for when the user stops typing,
     // or hide the corresponding pane otherwise.
     if (isGlobal) {
       DebuggerView.GlobalSearch.scheduleSearch(token);
+      this._prevSearchedToken = token;
       return;
     }
 
     // If this is a variable search, defer the action to the corresponding
     // variables view instance.
     if (isVariable) {
       DebuggerView.Variables.scheduleSearch(token);
+      this._prevSearchedToken = token;
       return;
     }
 
     DebuggerView.GlobalSearch.clearView();
     this._performFileSearch(file);
     this._performLineSearch(line);
     this._performTokenSearch(token);
   },
@@ -849,18 +854,25 @@ FilterView.prototype = {
   /**
    * The key press listener for the search container.
    */
   _onKeyPress: function DVF__onScriptsKeyPress(e) {
     // This attribute is not implemented in Gecko at this time, see bug 680830.
     e.char = String.fromCharCode(e.charCode);
 
     let [file, line, token, isGlobal, isVariable] = this.searchboxInfo;
-    let isDifferentToken, isReturnKey, action = -1;
+    let isFileSearch, isLineSearch, isDifferentToken, isReturnKey;
+    let action = -1;
 
+    if (file && !line && !token) {
+      isFileSearch = true;
+    }
+    if (line && !token) {
+      isLineSearch = true;
+    }
     if (this._prevSearchedToken != token) {
       isDifferentToken = true;
     }
 
     // Meta+G and Ctrl+N focus next matches.
     if ((e.char == "g" && e.metaKey) || e.char == "n" && e.ctrlKey) {
       action = 0;
     }
@@ -885,46 +897,61 @@ FilterView.prototype = {
         action = 2;
         break;
     }
 
     if (action == 2) {
       DebuggerView.editor.focus();
       return;
     }
-    if (action == -1 || (token.length == 0 && line == 0)) {
+    if (action == -1 || (!file && !line && !token)) {
+      DebuggerView.FilteredSources.hidden = true;
       return;
     }
 
     e.preventDefault();
     e.stopPropagation();
 
+    // Select the next or previous file search entry.
+    if (isFileSearch) {
+      if (isReturnKey) {
+        DebuggerView.FilteredSources.hidden = true;
+        DebuggerView.editor.focus();
+        this.clearSearch();
+      } else {
+        DebuggerView.FilteredSources[["focusNext", "focusPrev"][action]]();
+      }
+      this._prevSearchedFile = file;
+      return;
+    }
+
     // Perform a global search based on the specified operator.
     if (isGlobal) {
       if (isReturnKey && (isDifferentToken || DebuggerView.GlobalSearch.hidden)) {
         DebuggerView.GlobalSearch.performSearch(token);
       } else {
         DebuggerView.GlobalSearch[["focusNextMatch", "focusPrevMatch"][action]]();
       }
       this._prevSearchedToken = token;
       return;
     }
 
     // Perform a variable search based on the specified operator.
     if (isVariable) {
       if (isReturnKey && isDifferentToken) {
         DebuggerView.Variables.performSearch(token);
+      } else {
         DebuggerView.Variables.expandFirstSearchResults();
       }
       this._prevSearchedToken = token;
       return;
     }
 
     // Increment or decrement the specified line.
-    if (!isReturnKey && token.length == 0 && line > 0) {
+    if (isLineSearch && !isReturnKey) {
       line += action == 0 ? 1 : -1;
       let lineCount = DebuggerView.editor.getLineCount();
       let lineTarget = line < 1 ? 1 : line > lineCount ? lineCount : line;
 
       DebuggerView.editor.setCaretPosition(lineTarget - 1);
       this._searchbox.value = file + SEARCH_LINE_FLAG + lineTarget;
       this._prevSearchedLine = lineTarget;
       return;
@@ -1015,15 +1042,178 @@ FilterView.prototype = {
   _variableSearchKey: "",
   _target: null,
   _prevSearchedFile: "",
   _prevSearchedLine: 0,
   _prevSearchedToken: ""
 };
 
 /**
+ * Functions handling the filtered sources UI.
+ */
+function FilteredSourcesView() {
+  MenuContainer.call(this);
+  this._onClick = this._onClick.bind(this);
+}
+
+create({ constructor: FilteredSourcesView, proto: MenuContainer.prototype }, {
+  /**
+   * Initialization function, called when the debugger is started.
+   */
+  initialize: function DVFS_initialize() {
+    dumpn("Initializing the FilteredSourcesView");
+
+    let panel = this._panel = document.createElement("panel");
+    panel.id = "filtered-sources-panel";
+    panel.setAttribute("noautofocus", "true");
+    panel.setAttribute("position", FILTERED_SOURCES_POPUP_POSITION);
+    document.documentElement.appendChild(panel);
+
+    this._searchbox = document.getElementById("searchbox");
+    this._container = new StackList(panel);
+
+    this._container.itemFactory = this._createItemView;
+    this._container.itemType = "vbox";
+    this._container.addEventListener("click", this._onClick, false);
+  },
+
+  /**
+   * Destruction function, called when the debugger is closed.
+   */
+  destroy: function DVFS_destroy() {
+    dumpn("Destroying the FilteredSourcesView");
+    document.documentElement.removeChild(this._panel);
+    this._container.removeEventListener("click", this._onClick, false);
+  },
+
+  /**
+   * Sets the files container hidden or visible. It's hidden by default.
+   * @param boolean aFlag
+   */
+  set hidden(aFlag) {
+    if (aFlag) {
+      this._container._parent.hidePopup();
+    } else {
+      this._container._parent.openPopup(this._searchbox);
+    }
+  },
+
+  /**
+   * Updates the list of sources displayed in this container.
+   */
+  syncFileSearch: function DVFS_syncFileSearch() {
+    this.empty();
+
+    // If there's no currently searched file, or there are no matches found,
+    // hide the popup.
+    if (!DebuggerView.Filtering.searchedFile ||
+        !DebuggerView.Sources.visibleItems.length) {
+      this.hidden = true;
+      return;
+    }
+
+    // Get the currently visible items in the sources container.
+    let visibleItems = DebuggerView.Sources.visibleItems;
+    let displayedItems = visibleItems.slice(0, FILTERED_SOURCES_MAX_RESULTS);
+
+    for (let item of displayedItems) {
+      // Append a location item item to this container.
+      let trimmedLabel = SourceUtils.trimUrlLength(item.label);
+      let trimmedValue = SourceUtils.trimUrlLength(item.value);
+
+      let locationItem = this.push(trimmedLabel, trimmedValue, {
+        forced: true,
+        relaxed: true,
+        unsorted: true,
+        attachment: {
+          fullLabel: item.label,
+          fullValue: item.value
+        }
+      });
+
+      let element = locationItem.target;
+      element.className = "dbg-source-item list-item";
+      element.labelNode.className = "dbg-source-item-name plain";
+      element.valueNode.className = "dbg-source-item-details plain";
+    }
+
+    this._updateSelection(this.getItemAtIndex(0));
+    this.hidden = false;
+  },
+
+  /**
+   * Focuses the next found match in this container.
+   */
+  focusNext: function DVFS_focusNext() {
+    let nextIndex = this.selectedIndex + 1;
+    if (nextIndex >= this.totalItems) {
+      nextIndex = 0;
+    }
+    this._updateSelection(this.getItemAtIndex(nextIndex));
+  },
+
+  /**
+   * Focuses the previously found match in this container.
+   */
+  focusPrev: function DVFS_focusPrev() {
+    let prevIndex = this.selectedIndex - 1;
+    if (prevIndex < 0) {
+      prevIndex = this.totalItems - 1;
+    }
+    this._updateSelection(this.getItemAtIndex(prevIndex));
+  },
+
+  /**
+   * The click listener for this container.
+   */
+  _onClick: function DVFS__onClick(e) {
+    let locationItem = this.getItemForElement(e.target);
+    if (locationItem) {
+      this._updateSelection(locationItem);
+    }
+  },
+
+  /**
+   * Updates the selected item in this container and other views.
+   *
+   * @param MenuItem aItem
+   *        The item associated with the element to select.
+   */
+  _updateSelection: function DVFS__updateSelection(aItem) {
+    this.selectedItem = aItem;
+    DebuggerView.Filtering._target.selectedValue = aItem.attachment.fullValue;
+  },
+
+  /**
+   * Customization function for creating an item's UI.
+   *
+   * @param string aLabel
+   *        The item's label.
+   * @param string aValue
+   *        The item's value.
+   */
+  _createItemView: function DVFS__createItemView(aElementNode, aLabel, aValue) {
+    let labelNode = document.createElement("label");
+    let valueNode = document.createElement("label");
+
+    labelNode.setAttribute("value", aLabel);
+    valueNode.setAttribute("value", aValue);
+
+    aElementNode.appendChild(labelNode);
+    aElementNode.appendChild(valueNode);
+
+    aElementNode.labelNode = labelNode;
+    aElementNode.valueNode = valueNode;
+  },
+
+  _panel: null,
+  _searchbox: null
+});
+
+/**
  * Preliminary setup for the DebuggerView object.
  */
 DebuggerView.Toolbar = new ToolbarView();
 DebuggerView.Options = new OptionsView();
 DebuggerView.ChromeGlobals = new ChromeGlobalsView();
 DebuggerView.Sources = new SourcesView();
 DebuggerView.Filtering = new FilterView();
+DebuggerView.FilteredSources = new FilteredSourcesView();
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -6,18 +6,20 @@
 "use strict";
 
 const SOURCE_URL_MAX_LENGTH = 64; // chars
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
 const PANES_APPEARANCE_DELAY = 50; // ms
 const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars
 const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "after_start";
 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET = 50; // px
+const FILTERED_SOURCES_POPUP_POSITION = "before_start";
+const FILTERED_SOURCES_MAX_RESULTS = 10;
+const GLOBAL_SEARCH_EXPAND_MAX_RESULTS = 50;
 const GLOBAL_SEARCH_LINE_MAX_LENGTH = 300; // chars
-const GLOBAL_SEARCH_EXPAND_MAX_RESULTS = 50;
 const GLOBAL_SEARCH_ACTION_MAX_DELAY = 1500; // ms
 const SEARCH_GLOBAL_FLAG = "!";
 const SEARCH_TOKEN_FLAG = "#";
 const SEARCH_LINE_FLAG = ":";
 const SEARCH_VARIABLE_FLAG = "*";
 
 /**
  * Object defining the debugger view components.
@@ -35,16 +37,17 @@ let DebuggerView = {
     this._initializeWindow();
     this._initializePanes();
 
     this.Toolbar.initialize();
     this.Options.initialize();
     this.ChromeGlobals.initialize();
     this.Sources.initialize();
     this.Filtering.initialize();
+    this.FilteredSources.initialize();
     this.StackFrames.initialize();
     this.Breakpoints.initialize();
     this.WatchExpressions.initialize();
     this.GlobalSearch.initialize();
 
     this.Variables = new VariablesView(document.getElementById("variables"));
     this.Variables.searchPlaceholder = L10N.getStr("emptyVariablesFilterText");
     this.Variables.emptyText = L10N.getStr("emptyVariablesText");
@@ -65,16 +68,17 @@ let DebuggerView = {
   destroy: function DV_destroy(aCallback) {
     dumpn("Destroying the DebuggerView");
 
     this.Toolbar.destroy();
     this.Options.destroy();
     this.ChromeGlobals.destroy();
     this.Sources.destroy();
     this.Filtering.destroy();
+    this.FilteredSources.destroy();
     this.StackFrames.destroy();
     this.Breakpoints.destroy();
     this.WatchExpressions.destroy();
     this.GlobalSearch.destroy();
 
     this._destroyWindow();
     this._destroyPanes();
     this._destroyEditor();
@@ -465,16 +469,17 @@ let DebuggerView = {
     this.StackFrames.empty();
     this.Breakpoints.empty();
     this.Breakpoints.unhighlightBreakpoint();
     this.Variables.empty();
     SourceUtils.clearLabelsCache();
 
     if (this.editor) {
       this.editor.setText("");
+      this.editor.focus();
       this._editorSource = null;
     }
   },
 
   Toolbar: null,
   Options: null,
   ChromeGlobals: null,
   Sources: null,
@@ -931,33 +936,35 @@ MenuContainer.prototype = {
     let values = [];
     for (let [value] of this._itemsByValue) {
       values.push(value);
     }
     return values;
   },
 
   /**
-   * Gets the total items in this container.
+   * Gets the total number of items in this container.
    * @return number
    */
   get totalItems() {
     return this._itemsByElement.size;
   },
 
   /**
-   * Gets the total visible (non-hidden) items in this container.
-   * @return number
+   * Returns a list of all the visible (non-hidden) items in this container.
+   * @return array
    */
   get visibleItems() {
-    let count = 0;
-    for (let [element] of this._itemsByElement) {
-      count += element.hidden ? 0 : 1;
+    let items = [];
+    for (let [element, item] of this._itemsByElement) {
+      if (!element.hidden) {
+        items.push(item);
+      }
     }
-    return count;
+    return items;
   },
 
   /**
    * Specifies the required conditions for an item to be considered unique.
    * Possible values:
    *   - 1: label AND value are different from all other items
    *   - 2: label OR value are different from all other items
    *   - 3: only label is required to be different
@@ -1129,18 +1136,16 @@ MenuContainer.prototype = {
  * displaying views like the StackFrames, Breakpoints etc.
  *
  * Custom methods introduced by this view, not necessary for a MenuContainer:
  * set emptyText(aValue:string)
  * set permaText(aValue:string)
  * set itemType(aType:string)
  * set itemFactory(aCallback:function)
  *
- * TODO: Use this in #796135 - "Provide some obvious UI for scripts filtering".
- *
  * @param nsIDOMNode aAssociatedNode
  *        The element associated with the displayed container.
  */
 function StackList(aAssociatedNode) {
   this._parent = aAssociatedNode;
 
   // Create an internal list container.
   this._list = document.createElement("vbox");
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -214,19 +214,21 @@
         <toolbarbutton id="step-in"
                        class="devtools-toolbarbutton"
                        tabindex="0"/>
         <toolbarbutton id="step-out"
                        class="devtools-toolbarbutton"
                        tabindex="0"/>
       </hbox>
       <menulist id="chrome-globals"
-                class="devtools-menulist" hidden="true"/>
+                class="devtools-menulist"
+                sizetopopup="none" hidden="true"/>
       <menulist id="sources"
-                class="devtools-menulist"/>
+                class="devtools-menulist"
+                sizetopopup="none"/>
       <textbox id="searchbox"
                class="devtools-searchinput" type="search"/>
       <spacer flex="1"/>
       <toolbarbutton id="toggle-panes"
                      class="devtools-toolbarbutton"
                      tooltiptext="&debuggerUI.panesButton.tooltip;"
                      tabindex="0"/>
       <toolbarbutton id="debugger-options"
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -61,16 +61,17 @@ MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_scripts-searching-01.js \
 	browser_dbg_scripts-searching-02.js \
 	browser_dbg_scripts-searching-03.js \
 	browser_dbg_scripts-searching-04.js \
 	browser_dbg_scripts-searching-05.js \
 	browser_dbg_scripts-searching-06.js \
 	browser_dbg_scripts-searching-07.js \
 	browser_dbg_scripts-searching-08.js \
+	browser_dbg_scripts-searching-files_ui.js \
 	browser_dbg_scripts-searching-popup.js \
 	browser_dbg_pause-resume.js \
 	browser_dbg_update-editor-mode.js \
 	$(filter temporarily-disabled-due-to-oranges--bug-726609, browser_dbg_select-line.js) \
 	browser_dbg_clean-exit.js \
 	browser_dbg_bug723069_editor-breakpoints.js \
 	browser_dbg_bug723071_editor-breakpoints-pane.js \
 	browser_dbg_bug740825_conditional-breakpoints-01.js \
--- a/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
@@ -138,19 +138,19 @@ function test()
     gDebugger.addEventListener("Debugger:AfterFramesCleared", function listener() {
       gDebugger.removeEventListener("Debugger:AfterFramesCleared", listener, true);
 
       is(gBreakpointsPane.selectedItem, null,
         "There should be no selected breakpoint in the breakpoints pane.")
       is(gBreakpointsPane._popupShown, false,
         "The breakpoint conditional expression popup should not be shown.");
 
-      is(gDebugger.DebuggerView.StackFrames.visibleItems, 0,
+      is(gDebugger.DebuggerView.StackFrames.visibleItems.length, 0,
         "There should be no visible stackframes.");
-      is(gDebugger.DebuggerView.Breakpoints.visibleItems, 13,
+      is(gDebugger.DebuggerView.Breakpoints.visibleItems.length, 13,
         "There should be thirteen visible breakpoints.");
 
       testReload();
     }, true);
 
     gDebugger.DebuggerController.activeThread.resume();
   }
 
@@ -336,17 +336,17 @@ function test()
     let count = 0;
     let intervalID = window.setInterval(function() {
       info("count: " + count + " ");
       if (++count > 50) {
         ok(false, "Timed out while polling for the breakpoints.");
         window.clearInterval(intervalID);
         return closeDebuggerAndFinish();
       }
-      if (gBreakpointsPane.visibleItems != total) {
+      if (gBreakpointsPane.visibleItems.length != total) {
         return;
       }
       // We got all the breakpoints, it's safe to callback.
       window.clearInterval(intervalID);
       callback();
     }, 100);
   }
 
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js
@@ -20,17 +20,16 @@ function test()
   let scriptShown = false;
   let framesAdded = false;
 
   debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
-    gDebugger.SourceResults.prototype.alwaysExpand = false;
 
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.simpleCall();
   });
@@ -278,17 +277,17 @@ function testScriptSearching() {
        gEditor.getCaretPosition().col == 4 + token.length,
       "The editor didn't jump to the correct token. (18.1)");
 
 
     clear();
     ok(gEditor.getCaretPosition().line == 19 &&
        gEditor.getCaretPosition().col == 4 + token.length,
       "The editor didn't remain at the correct token. (19)");
-    is(gScripts.visibleItems, 1,
+    is(gScripts.visibleItems.length, 1,
       "Not all the scripts are shown after the search. (20)");
     isnot(gMenulist.getAttribute("label"), noMatchingScripts,
       "The menulist should not display a notice that matches are found.");
 
     closeDebuggerAndFinish();
   });
 }
 
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js
@@ -22,17 +22,16 @@ function test()
   let scriptShown = false;
   let framesAdded = false;
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
-    gDebugger.SourceResults.prototype.alwaysExpand = false;
 
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
@@ -74,17 +73,17 @@ function firstSearch() {
     if (url.indexOf("-01.js") != -1) {
       window.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 4 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't jump to the correct line. (1)");
-        is(gScripts.visibleItems, 1,
+        is(gScripts.visibleItems.length, 1,
           "Not all the correct scripts are shown after the search. (1)");
 
         secondSearch();
       });
     }
   });
   write(".*-01\.js:5");
 }
@@ -102,17 +101,17 @@ function secondSearch() {
 
       executeSoon(function() {
         append("#" + token);
 
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 8 + token.length,
           "The editor didn't jump to the correct line. (2)");
-        is(gScripts.visibleItems, 1,
+        is(gScripts.visibleItems.length, 1,
           "Not all the correct scripts are shown after the search. (2)");
 
         waitForFirstScript();
       });
     }
   });
   gScripts.selectedIndex = 1;
 }
@@ -145,17 +144,17 @@ function thirdSearch() {
     if (url.indexOf("-02.js") != -1) {
       window.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 8 + token.length,
           "The editor didn't jump to the correct line. (3)");
-        is(gScripts.visibleItems, 1,
+        is(gScripts.visibleItems.length, 1,
           "Not all the correct scripts are shown after the search. (3)");
 
         fourthSearch(0, "ugger;", token);
       });
     }
   });
   write(".*-02\.js#" + token);
 }
@@ -176,41 +175,41 @@ function fourthSearch(i, string, token) 
   clear();
   ok(gEditor.getCaretPosition().line == 5 &&
      gEditor.getCaretPosition().col == 8 + token.length + i,
     "The editor didn't remain at the correct token. (5)");
 
   executeSoon(function() {
     let noMatchingScripts = gDebugger.L10N.getStr("noMatchingScriptsText");
 
-    is(gScripts.visibleItems, 2,
+    is(gScripts.visibleItems.length, 2,
       "Not all the scripts are shown after the searchbox was emptied.");
     is(gMenulist.selectedIndex, 1,
       "The menulist should have retained its selected index after the searchbox was emptied.");
 
     write("BOGUS");
     ok(gEditor.getCaretPosition().line == 5 &&
        gEditor.getCaretPosition().col == 8 + token.length + i,
       "The editor didn't remain at the correct token. (6)");
 
     is(gMenulist.getAttribute("label"), noMatchingScripts,
       "The menulist should display a notice that no scripts match the searched token.");
-    is(gScripts.visibleItems, 0,
+    is(gScripts.visibleItems.length, 0,
       "No scripts should be displayed in the menulist after a bogus search.");
     is(gMenulist.selectedIndex, 1,
       "The menulist should retain its selected index after a bogus search.");
 
     clear();
     ok(gEditor.getCaretPosition().line == 5 &&
        gEditor.getCaretPosition().col == 8 + token.length + i,
       "The editor didn't remain at the correct token. (7)");
 
     isnot(gMenulist.getAttribute("label"), noMatchingScripts,
       "The menulist should not display a notice after the searchbox was emptied.");
-    is(gScripts.visibleItems, 2,
+    is(gScripts.visibleItems.length, 2,
       "Not all the scripts are shown after the searchbox was emptied.");
     is(gMenulist.selectedIndex, 1,
       "The menulist should have retained its selected index after the searchbox was emptied of a bogus search.");
 
     noMatchingScriptsSingleCharCheck(token, i);
   });
 }
 
@@ -219,17 +218,17 @@ function noMatchingScriptsSingleCharChec
 
   write("x");
   ok(gEditor.getCaretPosition().line == 5 &&
      gEditor.getCaretPosition().col == 8 + token.length + i,
     "The editor didn't remain at the correct token. (8)");
 
   is(gMenulist.getAttribute("label"), noMatchingScripts,
     "The menulist should display a notice after no matches are found.");
-  is(gScripts.visibleItems, 0,
+  is(gScripts.visibleItems.length, 0,
     "No scripts should be shown after no matches are found.");
   is(gMenulist.selectedIndex, 1,
     "The menulist should have retained its selected index after no matches are found.");
 
   closeDebuggerAndFinish();
 }
 
 function clear() {
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js
@@ -81,17 +81,17 @@ function firstSearch() {
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 0,
           "The editor shouldn't have jumped to a matching line yet.");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the scripts are shown after the global search.");
 
         let scriptResults = gDebugger.document.querySelectorAll(".dbg-source-results");
         is(scriptResults.length, 2,
           "There should be matches found in two scripts.");
 
         let item0 = gDebugger.SourceResults.getItemForElement(scriptResults[0]);
         let item1 = gDebugger.SourceResults.getItemForElement(scriptResults[1]);
@@ -197,17 +197,17 @@ function secondSearch() {
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 0,
           "The editor shouldn't have jumped to a matching line yet.");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the scripts are shown after the global search.");
 
         let scriptResults = gDebugger.document.querySelectorAll(".dbg-source-results");
         is(scriptResults.length, 2,
           "There should be matches found in two scripts.");
 
         let item0 = gDebugger.SourceResults.getItemForElement(scriptResults[0]);
         let item1 = gDebugger.SourceResults.getItemForElement(scriptResults[1]);
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js
@@ -81,17 +81,17 @@ function doSearch() {
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 0,
           "The editor shouldn't have jumped to a matching line yet.");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the scripts are shown after the global search.");
 
         isnot(gSearchView._container._list.childNodes.length, 0,
           "The global search pane should be visible now.");
         isnot(gSearchView._container._parent.hidden, true,
           "The global search pane should be visible now.");
         isnot(gSearchView._splitter.hidden, true,
           "The global search pane splitter should be visible now.");
@@ -115,17 +115,17 @@ function doFirstJump() {
 
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 4 &&
            gEditor.getCaretPosition().col == 6,
           "The editor didn't jump to the correct line. (1)");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the correct scripts are shown after the search. (1)");
 
         doSecondJump();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
@@ -142,17 +142,17 @@ function doSecondJump() {
 
     let url = aEvent.detail.url;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 6,
           "The editor didn't jump to the correct line. (2)");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the correct scripts are shown after the search. (2)");
 
         doWrapAroundJump();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
@@ -169,17 +169,17 @@ function doWrapAroundJump() {
 
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 4 &&
            gEditor.getCaretPosition().col == 6,
           "The editor didn't jump to the correct line. (3)");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the correct scripts are shown after the search. (3)");
 
         doBackwardsWrapAroundJump();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
@@ -196,17 +196,17 @@ function doBackwardsWrapAroundJump() {
 
     let url = aEvent.detail.url;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 6,
           "The editor didn't jump to the correct line. (4)");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the correct scripts are shown after the search. (4)");
 
         testSearchTokenEmpty();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
@@ -223,17 +223,17 @@ function testSearchTokenEmpty() {
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 6,
           "The editor didn't remain at the correct line. (5)");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the correct scripts are shown after the search. (5)");
 
         is(gSearchView._container._list.childNodes.length, 0,
           "The global search pane shouldn't have any child nodes after clear().");
         is(gSearchView._container._parent.hidden, true,
           "The global search pane shouldn't be visible after clear().");
         is(gSearchView._splitter.hidden, true,
           "The global search pane splitter shouldn't be visible after clear().");
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js
@@ -81,17 +81,17 @@ function doSearch() {
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 0,
           "The editor shouldn't have jumped to a matching line yet.");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the scripts are shown after the global search.");
 
         isnot(gSearchView._container._list.childNodes.length, 0,
           "The global search pane should be visible now.");
         isnot(gSearchView._container._parent.hidden, true,
           "The global search pane should be visible now.");
         isnot(gSearchView._splitter.hidden, true,
           "The global search pane splitter should be visible now.");
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js
@@ -74,17 +74,17 @@ function doSearch() {
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 0,
           "The editor shouldn't have jumped to a matching line yet.");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the scripts are shown after the global search.");
 
         testSearchMatchNotFound();
       });
     } else {
       ok(false, "The current script shouldn't have changed after a global search.");
     }
   });
@@ -101,17 +101,17 @@ function testSearchMatchNotFound() {
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't remain at the correct line.");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the correct scripts are shown after the search.");
 
         closeDebuggerAndFinish();
       });
     } else {
       ok(false, "How did you get here? Go away!");
     }
   });
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js
@@ -177,17 +177,17 @@ function testClickLineToJump(scriptResul
 
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 0 &&
            gEditor.getCaretPosition().col == 4,
           "The editor didn't jump to the correct line. (1)");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the correct scripts are shown after the search. (1)");
 
         callbacks[0](scriptResults, callbacks.slice(1));
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
@@ -212,17 +212,17 @@ function testClickMatchToJump(scriptResu
 
     let url = aEvent.detail.url;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 5,
           "The editor didn't jump to the correct line. (1)");
-        is(gScripts.visibleItems, 2,
+        is(gScripts.visibleItems.length, 2,
           "Not all the correct scripts are shown after the search. (1)");
 
         callbacks[0]();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-files_ui.js
@@ -0,0 +1,604 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_update-editor-mode.html";
+
+/**
+ * Tests basic functionality of scripts filtering (file search) helper UI.
+ */
+
+var gPane = null;
+var gTab = null;
+var gDebuggee = null;
+var gDebugger = null;
+var gEditor = null;
+var gScripts = null;
+var gSearchBox = null;
+var gFilteredSources = null;
+var gMenulist = null;
+
+function test()
+{
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPane = aPane;
+    gDebugger = gPane.panelWin;
+  });
+
+  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent);
+    Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+  });
+}
+
+function testScriptSearching() {
+  gEditor = gDebugger.DebuggerView.editor;
+  gScripts = gDebugger.DebuggerView.Sources;
+  gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
+  gFilteredSources = gDebugger.DebuggerView.FilteredSources;
+  gMenulist = gScripts._container;
+
+  firstSearch();
+}
+
+function firstSearch() {
+  window.addEventListener("popupshown", function _onEvent(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent);
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 3,
+      "The filtered sources view should have 3 items available.");
+    is(gFilteredSources.visibleItems.length, 3,
+      "The filtered sources view should have 3 items visible.");
+
+    for (let i = 0; i < gFilteredSources.totalItems; i++) {
+      is(gFilteredSources.labels[i],
+         gDebugger.SourceUtils.trimUrlLength(gScripts.labels[i]),
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.values[i],
+         gDebugger.SourceUtils.trimUrlLength(gScripts.values[i]),
+        "The filtered sources view should have the correct values.");
+
+      is(gFilteredSources.visibleItems[i].label,
+         gDebugger.SourceUtils.trimUrlLength(gScripts.labels[i]),
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.visibleItems[i].value,
+         gDebugger.SourceUtils.trimUrlLength(gScripts.values[i]),
+        "The filtered sources view should have the correct values.");
+
+      is(gFilteredSources.visibleItems[i].attachment.fullLabel, gScripts.labels[i],
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.visibleItems[i].attachment.fullValue, gScripts.values[i],
+        "The filtered sources view should have the correct values.");
+    }
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("update-editor-mode.html") != -1) {
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        secondSearch();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  write(".");
+}
+
+function secondSearch() {
+  let sourceshown = false;
+  let popupshown = false;
+  let proceeded = false;
+
+  window.addEventListener("Debugger:SourceShown", function _onEvent1(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent1);
+    sourceshown = true;
+    executeSoon(proceed);
+  });
+  window.addEventListener("popupshown", function _onEvent2(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent2);
+    popupshown = true;
+    executeSoon(proceed);
+  });
+
+  function proceed() {
+    if (!sourceshown || !popupshown || proceeded) {
+      return;
+    }
+    proceeded = true;
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 1,
+      "The filtered sources view should have 1 items available.");
+    is(gFilteredSources.visibleItems.length, 1,
+      "The filtered sources view should have 1 items visible.");
+
+    for (let i = 0; i < gFilteredSources.totalItems; i++) {
+      is(gFilteredSources.labels[i],
+         gDebugger.SourceUtils.trimUrlLength(gScripts.visibleItems[i].label),
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.values[i],
+         gDebugger.SourceUtils.trimUrlLength(gScripts.visibleItems[i].value),
+        "The filtered sources view should have the correct values.");
+
+      is(gFilteredSources.visibleItems[i].label,
+         gDebugger.SourceUtils.trimUrlLength(gScripts.visibleItems[i].label),
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.visibleItems[i].value,
+         gDebugger.SourceUtils.trimUrlLength(gScripts.visibleItems[i].value),
+        "The filtered sources view should have the correct values.");
+
+      is(gFilteredSources.visibleItems[i].attachment.fullLabel, gScripts.visibleItems[i].label,
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.visibleItems[i].attachment.fullValue, gScripts.visibleItems[i].value,
+        "The filtered sources view should have the correct values.");
+    }
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("test-script-switching-01.js") != -1) {
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 1,
+          "Not all the correct scripts are shown after the search.");
+
+        thirdSearch();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  }
+  append("-0")
+}
+
+function thirdSearch() {
+  let sourceshown = false;
+  let popupshown = false;
+  let proceeded = false;
+
+  window.addEventListener("Debugger:SourceShown", function _onEvent1(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent1);
+    sourceshown = true;
+    executeSoon(proceed);
+  });
+  window.addEventListener("popupshown", function _onEvent2(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent2);
+    popupshown = true;
+    executeSoon(proceed);
+  });
+
+  function proceed() {
+    if (!sourceshown || !popupshown || proceeded) {
+      return;
+    }
+    proceeded = true;
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 3,
+      "The filtered sources view should have 3 items available.");
+    is(gFilteredSources.visibleItems.length, 3,
+      "The filtered sources view should have 3 items visible.");
+
+    for (let i = 0; i < gFilteredSources.totalItems; i++) {
+      is(gFilteredSources.labels[i],
+         gDebugger.SourceUtils.trimUrlLength(gScripts.visibleItems[i].label),
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.values[i],
+         gDebugger.SourceUtils.trimUrlLength(gScripts.visibleItems[i].value),
+        "The filtered sources view should have the correct values.");
+
+      is(gFilteredSources.visibleItems[i].label,
+         gDebugger.SourceUtils.trimUrlLength(gScripts.visibleItems[i].label),
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.visibleItems[i].value,
+         gDebugger.SourceUtils.trimUrlLength(gScripts.visibleItems[i].value),
+        "The filtered sources view should have the correct values.");
+
+      is(gFilteredSources.visibleItems[i].attachment.fullLabel, gScripts.visibleItems[i].label,
+        "The filtered sources view should have the correct labels.");
+      is(gFilteredSources.visibleItems[i].attachment.fullValue, gScripts.visibleItems[i].value,
+        "The filtered sources view should have the correct values.");
+    }
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("update-editor-mode.html") != -1) {
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        goDown();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  }
+  backspace(1)
+}
+
+function goDown() {
+  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 3,
+      "The filtered sources view should have 3 items available.");
+    is(gFilteredSources.visibleItems.length, 3,
+      "The filtered sources view should have 3 items visible.");
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("test-editor-mode") != -1) {
+      window.removeEventListener(aEvent.type, _onEvent);
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        goDownAgain();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  EventUtils.sendKey("DOWN");
+}
+
+function goDownAgain() {
+  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 3,
+      "The filtered sources view should have 3 items available.");
+    is(gFilteredSources.visibleItems.length, 3,
+      "The filtered sources view should have 3 items visible.");
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("test-script-switching-01.js") != -1) {
+      window.removeEventListener(aEvent.type, _onEvent);
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        goDownAndWrap();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  EventUtils.synthesizeKey("g", { metaKey: true });
+}
+
+function goDownAndWrap() {
+  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 3,
+      "The filtered sources view should have 3 items available.");
+    is(gFilteredSources.visibleItems.length, 3,
+      "The filtered sources view should have 3 items visible.");
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("update-editor-mode.html") != -1) {
+      window.removeEventListener(aEvent.type, _onEvent);
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        goUpAndWrap();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  EventUtils.synthesizeKey("n", { ctrlKey: true });
+}
+
+function goUpAndWrap() {
+  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 3,
+      "The filtered sources view should have 3 items available.");
+    is(gFilteredSources.visibleItems.length, 3,
+      "The filtered sources view should have 3 items visible.");
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("test-script-switching-01.js") != -1) {
+      window.removeEventListener(aEvent.type, _onEvent);
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        clickAndSwitch();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  EventUtils.sendKey("UP");
+}
+
+function clickAndSwitch() {
+  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 3,
+      "The filtered sources view should have 3 items available.");
+    is(gFilteredSources.visibleItems.length, 3,
+      "The filtered sources view should have 3 items visible.");
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("update-editor-mode.html") != -1) {
+      window.removeEventListener(aEvent.type, _onEvent);
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        clickAndSwitchAgain();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  EventUtils.sendMouseEvent({ type: "click" }, gFilteredSources.visibleItems[0].target);
+}
+
+function clickAndSwitchAgain() {
+  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    is(gFilteredSources.totalItems, 3,
+      "The filtered sources view should have 3 items available.");
+    is(gFilteredSources.visibleItems.length, 3,
+      "The filtered sources view should have 3 items visible.");
+
+    is(gFilteredSources.selectedValue,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
+      "The correct item should be selected in the filtered sources view");
+    is(gFilteredSources.selectedLabel,
+       gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
+      "The correct item should be selected in the filtered sources view");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("test-script-switching-01.js") != -1) {
+      window.removeEventListener(aEvent.type, _onEvent);
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        switchFocusWithEscape();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  EventUtils.sendMouseEvent({ type: "click" }, gFilteredSources.visibleItems[2].target);
+}
+
+function switchFocusWithEscape() {
+  window.addEventListener("popuphidden", function _onEvent(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent);
+
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("test-script-switching-01.js") != -1) {
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        focusAgainAfterEscape();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  EventUtils.sendKey("ESCAPE");
+}
+
+function focusAgainAfterEscape() {
+  window.addEventListener("popupshown", function _onEvent(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent);
+
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("test-script-switching-01.js") != -1) {
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 1,
+          "Not all the correct scripts are shown after the search.");
+
+        switchFocusWithReturn();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  append("0");
+}
+
+function switchFocusWithReturn() {
+  window.addEventListener("popuphidden", function _onEvent(aEvent) {
+    window.removeEventListener(aEvent.type, _onEvent);
+
+    info("Current script url:\n" + gScripts.selectedValue + "\n");
+    info("Debugger editor text:\n" + gEditor.getText() + "\n");
+
+    let url = gScripts.selectedValue;
+    if (url.indexOf("test-script-switching-01.js") != -1) {
+
+      executeSoon(function() {
+        info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+        ok(gEditor.getCaretPosition().line == 0 &&
+           gEditor.getCaretPosition().col == 0,
+          "The editor didn't jump to the correct line.");
+        is(gScripts.visibleItems.length, 3,
+          "Not all the correct scripts are shown after the search.");
+
+        closeDebuggerAndFinish();
+      });
+    } else {
+      ok(false, "How did you get here?");
+    }
+  });
+  EventUtils.sendKey("RETURN");
+}
+
+function clear() {
+  gSearchBox.focus();
+  gSearchBox.value = "";
+}
+
+function write(text) {
+  clear();
+  append(text);
+}
+
+function backspace(times) {
+  for (let i = 0; i < times; i++) {
+    EventUtils.sendKey("BACK_SPACE")
+  }
+}
+
+function append(text) {
+  gSearchBox.focus();
+
+  for (let i = 0; i < text.length; i++) {
+    EventUtils.sendChar(text[i]);
+  }
+  info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
+}
+
+registerCleanupFunction(function() {
+  removeTab(gTab);
+  gPane = null;
+  gTab = null;
+  gDebuggee = null;
+  gDebugger = null;
+  gEditor = null;
+  gScripts = null;
+  gSearchBox = null;
+  gFilteredSources = null;
+  gMenulist = null;
+});
--- a/browser/themes/gnomestripe/devtools/debugger.css
+++ b/browser/themes/gnomestripe/devtools/debugger.css
@@ -8,17 +8,17 @@
   background-color: white;
 }
 
 /**
  * Debugger content
  */
 
 #chrome-globals, #sources {
-  min-width: 150px;
+  width: 200px;
 }
 
 /**
  * Lists and headers
  */
 
 .list-item {
   padding: 2px;
@@ -39,16 +39,21 @@
 }
 
 /**
  * Sources searching
  */
 
 #globalsearch {
   background-color: white;
+  min-height: 10px;
+}
+
+#globalsearch > vbox:not(:empty) {
+  min-height: 10px;
   max-height: 150px;
 }
 
 .dbg-source-results:not(:last-child) {
   border-bottom: 1px dotted #aaa;
 }
 
 .dbg-results-header {
@@ -96,16 +101,20 @@
   transition: transform 0.25s ease-in-out;
 }
 
 .dbg-results-container .line-contents > .string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
+/**
+ * Searchbox panel
+ */
+
 #searchbox-panel .description {
   margin: -6px 0 8px 0;
 }
 
 #searchbox-panel button.operator {
   min-width: 0;
   margin: 0;
   padding: 0;
@@ -113,16 +122,60 @@
 }
 
 #searchbox-panel label.operator {
   -moz-padding-start: 6px;
   padding-bottom: 1px;
 }
 
 /**
+ * Filtered sources panel
+ */
+
+#filtered-sources-panel {
+  padding: 4px;
+}
+
+.dbg-source-item {
+  background: #f4f4f4;
+  border: 1px solid #ddd;
+  border-top-color: #fff;
+}
+
+.dbg-source-item.selected {
+  background: #cddae5;
+}
+
+.dbg-source-item:first-of-type {
+  border-top-color: #ddd;
+  border-radius: 4px 4px 0 0;
+}
+
+.dbg-source-item:last-of-type {
+  border-radius: 0 0 4px 4px;
+}
+
+.dbg-source-item:only-of-type {
+  border-radius: 4px 4px 4px 4px;
+}
+
+.dbg-source-item:not(:hover) {
+  text-shadow: 0 1px #fff;
+}
+
+.dbg-source-item-name {
+  color: #333;
+  font-weight: 600;
+}
+
+.dbg-source-item-details {
+  color: #888;
+}
+
+/**
  * Stack frames and breakpoints pane
  */
 
 #stackframes\+breakpoints {
   background-color: white;
   min-width: 50px;
 }
 
--- a/browser/themes/pinstripe/devtools/debugger.css
+++ b/browser/themes/pinstripe/devtools/debugger.css
@@ -10,17 +10,17 @@
   background-color: white;
 }
 
 /**
  * Debugger content
  */
 
 #chrome-globals, #sources {
-  min-width: 150px;
+  width: 200px;
 }
 
 /**
  * Lists and headers
  */
 
 .list-item {
   padding: 2px;
@@ -41,16 +41,21 @@
 }
 
 /**
  * Sources searching
  */
 
 #globalsearch {
   background-color: white;
+  min-height: 10px;
+}
+
+#globalsearch > vbox:not(:empty) {
+  min-height: 10px;
   max-height: 150px;
 }
 
 .dbg-source-results:not(:last-child) {
   border-bottom: 1px dotted #aaa;
 }
 
 .dbg-results-header {
@@ -98,16 +103,20 @@
   transition: transform 0.25s ease-in-out;
 }
 
 .dbg-results-container .line-contents > .string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
+/**
+ * Searchbox panel
+ */
+
 #searchbox-panel .description {
   margin: -6px 0 8px 0;
 }
 
 #searchbox-panel button.operator {
   min-width: 0;
   margin: 0;
   padding: 0;
@@ -115,16 +124,60 @@
 }
 
 #searchbox-panel label.operator {
   -moz-padding-start: 6px;
   padding-bottom: 1px;
 }
 
 /**
+ * Filtered sources panel
+ */
+
+#filtered-sources-panel {
+  padding: 4px;
+}
+
+.dbg-source-item {
+  background: #f4f4f4;
+  border: 1px solid #ddd;
+  border-top-color: #fff;
+}
+
+.dbg-source-item.selected {
+  background: #cddae5;
+}
+
+.dbg-source-item:first-of-type {
+  border-top-color: #ddd;
+  border-radius: 4px 4px 0 0;
+}
+
+.dbg-source-item:last-of-type {
+  border-radius: 0 0 4px 4px;
+}
+
+.dbg-source-item:only-of-type {
+  border-radius: 4px 4px 4px 4px;
+}
+
+.dbg-source-item:not(.selected):not(:hover) {
+  text-shadow: 0 1px #fff;
+}
+
+.dbg-source-item-name {
+  color: #333;
+  font-weight: 600;
+}
+
+.dbg-source-item-details {
+  color: #888;
+}
+
+/**
  * Stack frames and breakpoints pane
  */
 
 #stackframes\+breakpoints {
   background-color: white;
   min-width: 50px;
 }
 
--- a/browser/themes/winstripe/devtools/debugger.css
+++ b/browser/themes/winstripe/devtools/debugger.css
@@ -8,17 +8,17 @@
   background-color: white;
 }
 
 /**
  * Debugger content
  */
 
 #chrome-globals, #sources {
-  min-width: 150px;
+  width: 200px;
 }
 
 /**
  * This hardcoded width likely due to a toolkit Windows specific bug.
  * See http://hg.mozilla.org/mozilla-central/annotate/f38d6df93cad/toolkit/themes/winstripe/global/textbox-aero.css#l7
  */
 #searchbox {
   width: 200px;
@@ -47,16 +47,21 @@
 }
 
 /**
  * Sources searching
  */
 
 #globalsearch {
   background-color: white;
+  min-height: 10px;
+}
+
+#globalsearch > vbox:not(:empty) {
+  min-height: 10px;
   max-height: 150px;
 }
 
 .dbg-source-results:not(:last-child) {
   border-bottom: 1px dotted #aaa;
 }
 
 .dbg-results-header {
@@ -104,16 +109,20 @@
   transition: transform 0.25s ease-in-out;
 }
 
 .dbg-results-container .line-contents > .string[match=true][focused] {
   transition-duration: 0.1s;
   transform: scale(1.75, 1.75);
 }
 
+/**
+ * Searchbox panel
+ */
+
 #searchbox-panel .description {
   margin: -6px 0 8px 0;
 }
 
 #searchbox-panel button.operator {
   min-width: 0;
   margin: 0;
   padding: 0;
@@ -121,16 +130,60 @@
 }
 
 #searchbox-panel label.operator {
   -moz-padding-start: 6px;
   padding-bottom: 1px;
 }
 
 /**
+ * Filtered sources panel
+ */
+
+#filtered-sources-panel {
+  padding: 4px;
+}
+
+.dbg-source-item {
+  background: #f4f4f4;
+  border: 1px solid #ddd;
+  border-top-color: #fff;
+}
+
+.dbg-source-item.selected {
+  background: #cddae5;
+}
+
+.dbg-source-item:first-of-type {
+  border-top-color: #ddd;
+  border-radius: 4px 4px 0 0;
+}
+
+.dbg-source-item:last-of-type {
+  border-radius: 0 0 4px 4px;
+}
+
+.dbg-source-item:only-of-type {
+  border-radius: 4px 4px 4px 4px;
+}
+
+.dbg-source-item:not(:hover) {
+  text-shadow: 0 1px #fff;
+}
+
+.dbg-source-item-name {
+  color: #333;
+  font-weight: 600;
+}
+
+.dbg-source-item-details {
+  color: #888;
+}
+
+/**
  * Stack frames and breakpoints pane
  */
 
 #stackframes\+breakpoints {
   background-color: white;
   min-width: 50px;
 }