Merge inbound to mozilla-central. a=merge
authorCiure Andrei <aciure@mozilla.com>
Fri, 04 Jan 2019 05:45:10 +0200
changeset 509603 ce9872f98d6274a8db6592cc8f0ad19ea332fafd
parent 509590 7be73b4e5299792d47667c8587d56a5b2e36c71e (current diff)
parent 509602 167735f407393b851817d77915c5d0be05523f23 (diff)
child 509606 a10fd3e50ab84f5d82ec1fb61e8c5053c4fcc0c3
child 509636 81804f8f55d0b7971a16e7da807799f8de04d01b
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone66.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
Merge inbound to mozilla-central. a=merge
--- a/accessible/xul/XULSelectControlAccessible.cpp
+++ b/accessible/xul/XULSelectControlAccessible.cpp
@@ -40,17 +40,17 @@ void XULSelectControlAccessible::Shutdow
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // XULSelectControlAccessible: SelectAccessible
 
 void XULSelectControlAccessible::SelectedItems(nsTArray<Accessible*>* aItems) {
   // For XUL multi-select control
   nsCOMPtr<nsIDOMXULMultiSelectControlElement> xulMultiSelect =
-      do_QueryInterface(mSelectControl);
+      mSelectControl->AsXULMultiSelectControl();
   if (xulMultiSelect) {
     int32_t length = 0;
     xulMultiSelect->GetSelectedCount(&length);
     for (int32_t index = 0; index < length; index++) {
       RefPtr<Element> element;
       xulMultiSelect->MultiGetSelectedItem(index, getter_AddRefs(element));
       Accessible* item = mDoc->GetAccessible(element);
       if (item) aItems->AppendElement(item);
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -356,17 +356,17 @@ toolbarpaletteitem {
 :root[inDOMFullscreen] #sidebar-box,
 :root[inDOMFullscreen] #sidebar-splitter,
 :root[inFullscreen]:not([OSXLionFullscreen]) toolbar:not([fullscreentoolbar=true]),
 :root[inFullscreen] .global-notificationbox {
   visibility: collapse;
 }
 
 #navigator-toolbox[fullscreenShouldAnimate] {
-  transition: 1.5s margin-top ease-out;
+  transition: 0.8s margin-top ease-out;
 }
 
 /* Rules to help integrate WebExtension buttons */
 
 .webextension-browser-action > .toolbarbutton-badge-stack > .toolbarbutton-icon {
   height: 16px;
   width: 16px;
 }
--- a/devtools/client/debugger/new/README.mozilla
+++ b/devtools/client/debugger/new/README.mozilla
@@ -1,13 +1,13 @@
 This is the debugger.html project output.
 See https://github.com/devtools-html/debugger.html
 
-Version 115
+Version 116
 
-Comparison: https://github.com/devtools-html/debugger.html/compare/release-114...release-115
+Comparison: https://github.com/devtools-html/debugger.html/compare/release-115...release-116
 
 Packages:
 - babel-plugin-transform-es2015-modules-commonjs @6.26.2
 - babel-preset-react @6.24.1
 - react @16.4.1
 - react-dom @16.4.1
 - webpack @3.12.0
--- a/devtools/client/debugger/new/dist/debugger.css
+++ b/devtools/client/debugger/new/dist/debugger.css
@@ -1014,20 +1014,21 @@ menuseparator {
 .arrow,
 .worker,
 .refresh,
 .shortcut,
 .add-button {
   fill: var(--theme-splitter-color);
 }
 
-.folder,
-.domain,
-.file,
-.extension {
+.img.folder,
+.img.domain,
+.img.file,
+.img.extension,
+.img.worker {
   background-color: var(--theme-comment);
 }
 
 .worker,
 .file,
 .folder,
 .sources-list .source-icon,
 .extension {
@@ -1051,17 +1052,18 @@ menuseparator {
 }
 
 .img.domain,
 .img.folder {
   width: 15px;
   height: 15px;
 }
 
-.img.extension {
+.img.extension,
+.img.worker {
   width: 13px;
   height: 13px;
   margin-inline-start: 2px;
 }
 
 .img.result-item-icon {
   height: 18px;
   width: 18px;
@@ -1100,21 +1102,26 @@ menuseparator {
 }
 
 .img.file {
   mask: url("resource://devtools/client/debugger/new/images/file.svg") no-repeat;
   width: 13px;
   height: 13px;
 }
 
+.img.worker {
+  mask: url("resource://devtools/client/debugger/new/images/worker.svg") no-repeat;
+}
+
 .img.domain,
 .img.folder,
 .img.file,
 .sources-list .img.source-icon,
-.img.extension {
+.img.extension,
+.img.worker {
   mask-size: 100%;
   margin-inline-end: 5px;
   display: inline-block;
 }
 
 .img.result-item-icon {
   mask-size: 100%;
   margin-inline-end: 15px;
@@ -1714,272 +1721,16 @@ html .toggle-button.end.vertical svg {
 .container {
   display: flex;
   list-style: none;
   margin: 0;
   padding: 0;
 }
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
-
-.sources-panel {
-  background-color: var(--theme-sidebar-background);
-  display: flex;
-  flex: 1;
-  flex-direction: column;
-  overflow: hidden;
-  position: relative;
-}
-
-.sources-panel * {
-  -moz-user-select: none;
-  user-select: none;
-}
-
-.sources-clear-root {
-  padding: 4px 3px 4px 3px;
-  width: 100%;
-  text-align: start;
-  white-space: nowrap;
-  color: inherit;
-  display: block;
-  position: absolute;
-  top: 0;
-  left: 0;
-  border-bottom: 1px solid var(--theme-splitter-color);
-}
-
-.sources-clear-root i {
-  margin-right: 5px;
-  position: relative;
-}
-
-.sources-clear-root svg {
-  width: 13px;
-  height: 13px;
-}
-
-.theme-dark .sources-clear-root svg {
-  fill: var(--theme-body-color);
-}
-
-.sources-clear-root .home {
-  opacity: 0.5;
-}
-
-.sources-clear-root .breadcrumb svg {
-  width: 5px;
-  top: 2px;
-  position: absolute;
-  margin-right: 5px;
-}
-
-.sources-clear-root-label {
-  margin-left: 5px;
-}
-
-.sources-pane {
-  display: flex;
-  flex: 1;
-  height: 100%;
-}
-
-.sources-list {
-  flex: 1;
-  display: flex;
-}
-
-.sources-list .managed-tree {
-  flex: 1;
-  display: flex;
-}
-
-.sources-list .managed-tree .tree-node:first-child {
-  margin-top: 4px;
-}
-
-.sources-list .managed-tree .tree .node {
-  padding: 0 10px 0 3px;
-  width: 100%;
-}
-
-.sources-list .tree .img.arrow {
-  margin-right: 5px;
-}
-
-.sources-list .tree .focused .img:not(.vue):not(.angular),
-.sources-list .managed-tree .tree .node.focused .img.blackBox {
-  background: #ffffff;
-}
-
-.sources-list .tree .label .suffix {
-  font-style: italic;
-  font-size: 0.9em;
-  color: var(--theme-comment);
-}
-
-.sources-list .tree .focused .label .suffix {
-  color: inherit;
-}
-
-.sources-list .tree .img.arrow.expanded {
-  transform: rotate(0deg);
-}
-
-.theme-dark .source-list .tree .node.focused {
-  background-color: var(--theme-tab-toolbar-background);
-}
-
-.sources-list .tree .focused .label {
-  background-color: var(--theme-selection-background);
-}
-
-.sources-list .tree .label {
-  padding: 3px 0px 3px 0px;
-  display: inline-block;
-}
-
-.sources-list .tree .node .no-arrow {
-  width: 10px;
-  display: inline-block;
-}
-
-.no-sources-message {
-  width: 100%;
-  font-style: italic;
-  text-align: center;
-  padding: 0.5em;
-  font-size: 12px;
-  user-select: none;
-  justify-content: center;
-  align-items: center;
-}
-
-.sources-panel .outline {
-  display: flex;
-  height: 100%;
-}
-
-.tree-indent {
-  border-inline-start: 0 none;
-}
-
-.source-outline-tabs {
-  font-size: 12px;
-  width: 100%;
-  background: var(--theme-body-background);
-  border-top: 1px solid var(--theme-splitter-color);
-  display: flex;
-  -moz-user-select: none;
-  user-select: none;
-  box-sizing: border-box;
-  height: 29px;
-  margin: 0;
-  padding: 0;
-}
-
-.source-outline-tabs .tab {
-  background-color: var(--theme-toolbar-background);
-  border-top: 2px solid transparent;
-  border-bottom: 1px solid var(--theme-splitter-color);
-  color: var(--theme-toolbar-color);
-  cursor: default;
-  display: inline-flex;
-  flex: 1;
-  justify-content: center;
-  margin-bottom: 0px;
-  margin-top: -1px;
-  overflow: hidden;
-  padding: 5px 8px 7px 8px;
-  position: relative;
-  transition: all 0.25s ease;
-}
-
-.source-outline-tabs .tab:not(.active):hover {
-  background-color: var(--theme-toolbar-hover);
-  border-top: 2px solid rgba(0, 0, 0, 0.2);
-}
-
-.theme-dark .source-outline-tabs .tab:hover {
-  border-top: 2px solid var(--tab-line-hover-color);
-}
-
-.source-outline-tabs .tab.active {
-  border-top: 2px solid var(--tab-line-selected-color);
-  color: var(--theme-toolbar-selected-color);
-}
-
-.theme-dark .source-outline-tabs .tab.active {
-  color: var(--theme-toolbar-selected-color);
-}
-
-.theme-dark .source-outline-tabs .tab.active:hover {
-  border-top: 2px solid var(--tab-line-selected-color);
-}
-
-.source-outline-tabs .tab.active path,
-.source-outline-tabs .tab:hover path {
-  fill: var(--theme-body-color);
-}
-
-.source-outline-panel {
-  flex: 1;
-  overflow: auto;
-}
-
-.sources-list .managed-tree .tree .node .img.blackBox {
-  mask: url("resource://devtools/client/debugger/new/images/blackBox.svg") no-repeat;
-  mask-size: 100%;
-  background-color: var(--theme-highlight-blue);
-  width: 13px;
-  height: 13px;
-  display: inline-block;
-  margin-inline-end: 6px;
-  margin-inline-start: 1px;
-  margin-top: 2px;
-}
-
-.theme-dark .sources-list .managed-tree .tree .node .img.blackBox {
-  background-color: var(--theme-body-color);
-}
-
-/*
-  Custom root styles
-*/
-.sources-pane.sources-list-custom-root {
-  display: block;
-  position: relative;
-}
-
-.sources-list-custom-root .sources-pane {
-  display: block;
-}
-
-.sources-list-custom-root .sources-list,
-.sources-list-custom-root .no-sources-message {
-  position: absolute;
-  top: 26px;
-  right: 0;
-  bottom: 0;
-  left: 0;
-}
-
-/* Removes start margin when a custom root is used */
-.sources-list-custom-root
-  .tree
-  > .tree-node[data-expandable="false"][aria-level="0"] {
-  padding-inline-start: 4px;
-}
-
-.sources-list .tree-node[data-expandable="false"] .tree-indent:last-of-type {
-  margin-inline-end: 0;
-}
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 menu {
   display: inline;
   padding: 0;
 }
 
 menu > menuitem::after {
@@ -2267,16 +2018,281 @@ menuseparator {
 .source-icon.react {
   mask-size: 100%;
   background: var(--theme-highlight-bluegrey);
 }
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
+.sources-panel {
+  background-color: var(--theme-sidebar-background);
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  overflow: hidden;
+  position: relative;
+}
+
+.sources-panel * {
+  -moz-user-select: none;
+  user-select: none;
+}
+
+.sources-clear-root {
+  padding: 4px 3px 4px 3px;
+  width: 100%;
+  text-align: start;
+  white-space: nowrap;
+  color: inherit;
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.sources-clear-root i {
+  margin-right: 5px;
+  position: relative;
+}
+
+.sources-clear-root svg {
+  width: 13px;
+  height: 13px;
+}
+
+.theme-dark .sources-clear-root svg {
+  fill: var(--theme-body-color);
+}
+
+.sources-clear-root .home {
+  opacity: 0.5;
+}
+
+.sources-clear-root .breadcrumb svg {
+  width: 5px;
+  top: 2px;
+  position: absolute;
+  margin-right: 5px;
+}
+
+.sources-clear-root-label {
+  margin-left: 5px;
+}
+
+.sources-pane {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  height: 100%;
+}
+
+.sources-list {
+  flex: 1;
+  display: flex;
+}
+
+.sources-list .managed-tree {
+  flex: 1;
+  display: flex;
+}
+
+.sources-list .managed-tree .tree-node:first-child {
+  margin-top: 4px;
+}
+
+.sources-list .managed-tree .tree .node {
+  padding: 0 10px 0 3px;
+  width: 100%;
+}
+
+.sources-list .tree .img.arrow {
+  margin-right: 5px;
+}
+
+.sources-list .tree .focused .img:not(.vue):not(.angular),
+.sources-list .managed-tree .tree .node.focused .img.blackBox {
+  background: #ffffff;
+}
+
+.sources-list .tree .label .suffix {
+  font-style: italic;
+  font-size: 0.9em;
+  color: var(--theme-comment);
+}
+
+.sources-list .tree .focused .label .suffix {
+  color: inherit;
+}
+
+.sources-list .tree .img.arrow.expanded {
+  transform: rotate(0deg);
+}
+
+.theme-dark .source-list .tree .node.focused {
+  background-color: var(--theme-tab-toolbar-background);
+}
+
+.sources-list .tree .focused .label {
+  background-color: var(--theme-selection-background);
+}
+
+.sources-list .tree .label {
+  padding: 3px 0px 3px 0px;
+  display: inline-block;
+}
+
+.sources-list .tree .node .no-arrow {
+  width: 10px;
+  display: inline-block;
+}
+
+.no-sources-message {
+  width: 100%;
+  font-style: italic;
+  text-align: center;
+  padding: 0.5em;
+  font-size: 12px;
+  user-select: none;
+  justify-content: center;
+  align-items: center;
+}
+
+.sources-panel .outline {
+  display: flex;
+  height: 100%;
+}
+
+.tree-indent {
+  border-inline-start: 0 none;
+}
+
+.source-outline-tabs {
+  font-size: 12px;
+  width: 100%;
+  background: var(--theme-body-background);
+  border-top: 1px solid var(--theme-splitter-color);
+  display: flex;
+  -moz-user-select: none;
+  user-select: none;
+  box-sizing: border-box;
+  height: 29px;
+  margin: 0;
+  padding: 0;
+}
+
+.source-outline-tabs .tab {
+  background-color: var(--theme-toolbar-background);
+  border-top: 2px solid transparent;
+  border-bottom: 1px solid var(--theme-splitter-color);
+  color: var(--theme-toolbar-color);
+  cursor: default;
+  display: inline-flex;
+  flex: 1;
+  justify-content: center;
+  margin-bottom: 0px;
+  margin-top: -1px;
+  overflow: hidden;
+  padding: 5px 8px 7px 8px;
+  position: relative;
+  transition: all 0.25s ease;
+}
+
+.source-outline-tabs .tab:not(.active):hover {
+  background-color: var(--theme-toolbar-hover);
+  border-top: 2px solid rgba(0, 0, 0, 0.2);
+}
+
+.theme-dark .source-outline-tabs .tab:hover {
+  border-top: 2px solid var(--tab-line-hover-color);
+}
+
+.source-outline-tabs .tab.active {
+  border-top: 2px solid var(--tab-line-selected-color);
+  color: var(--theme-toolbar-selected-color);
+}
+
+.theme-dark .source-outline-tabs .tab.active {
+  color: var(--theme-toolbar-selected-color);
+}
+
+.theme-dark .source-outline-tabs .tab.active:hover {
+  border-top: 2px solid var(--tab-line-selected-color);
+}
+
+.source-outline-tabs .tab.active path,
+.source-outline-tabs .tab:hover path {
+  fill: var(--theme-body-color);
+}
+
+.source-outline-panel {
+  flex: 1;
+  overflow: auto;
+}
+
+.sources-list .managed-tree .tree .node .img.blackBox {
+  mask: url("resource://devtools/client/debugger/new/images/blackBox.svg") no-repeat;
+  mask-size: 100%;
+  background-color: var(--theme-highlight-blue);
+  width: 13px;
+  height: 13px;
+  display: inline-block;
+  margin-inline-end: 6px;
+  margin-inline-start: 1px;
+  margin-top: 2px;
+}
+
+.theme-dark .sources-list .managed-tree .tree .node .img.blackBox {
+  background-color: var(--theme-body-color);
+}
+
+/*
+  Custom root styles
+*/
+.sources-pane.sources-list-custom-root {
+  display: block;
+  position: relative;
+}
+
+.sources-list-custom-root .sources-pane {
+  display: block;
+}
+
+.sources-list-custom-root .sources-list,
+.sources-list-custom-root .no-sources-message {
+  position: absolute;
+  top: 26px;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+/* Removes start margin when a custom root is used */
+.sources-list-custom-root
+  .tree
+  > .tree-node[data-expandable="false"][aria-level="0"] {
+  padding-inline-start: 4px;
+}
+
+.sources-list .tree-node[data-expandable="false"] .tree-indent:last-of-type {
+  margin-inline-end: 0;
+}
+
+.thread-header {
+  margin-top: 10px;
+}
+
+.thread-header .label {
+  line-height: 15px;
+}
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
 .source-footer {
   background: var(--theme-body-background);
   border-top: 1px solid var(--theme-splitter-color);
   position: absolute;
   display: flex;
   bottom: 0;
   left: 0;
   right: 0;
@@ -3718,33 +3734,58 @@ html[dir="rtl"] .breakpoints-list .break
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 .workers-list * {
   user-select: none;
 }
 
-.workers-list .worker {
+.workers-list > .worker {
   font-size: 1rem;
   color: var(--theme-content-color1);
-  padding: 0 1em;
-  line-height: 31px;
+  padding: 0 0.5em;
+  line-height: 25px;
   position: relative;
   transition: all 0.25s ease;
   cursor: pointer;
-}
-
-.worker-list .worker:hover {
+  display: flex;
+}
+
+.workers-list .worker:hover {
   background-color: var(--search-overlays-semitransparent);
 }
 
-.worker-list .worker.selected {
+.workers-list .worker.selected {
   background-color: var(--tab-line-selected-color);
 }
+
+.workers-list .icon {
+  align-self: center;
+}
+
+.workers-list .label {
+  display: inline-block;
+  flex-grow: 1;
+}
+
+.workers-list .pause-badge {
+  align-self: center;
+}
+
+.workers-list .worker.selected {
+  background: var(--theme-selection-background);
+  color: white;
+}
+
+.workers-list .selected .img.file,
+.workers-list .selected .img.worker,
+.workers-list .selected .pause-badge .img.pause {
+  background: white;
+}
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 :root {
   --accordion-header-background: var(--theme-toolbar-background);
   --disclosure-arrow: #b2b2b2;
 }
@@ -3863,52 +3904,52 @@ html[dir="rtl"] .command-bar {
 .img.replay-previous,
 .img.replay-next,
 .img.resume,
 .img.shortcuts,
 .img.skipPausing {
   background-color: var(--theme-body-color);
 }
 
-.command-bar .img.pause {
+.img.pause {
   mask: url("resource://devtools/client/debugger/new/images/pause.svg") no-repeat;
 }
 
-.command-bar .img.stepOver {
+.img.stepOver {
   mask: url("resource://devtools/client/debugger/new/images/stepOver.svg") no-repeat;
 }
 
-.command-bar .img.stepIn {
+.img.stepIn {
   mask: url("resource://devtools/client/debugger/new/images/stepIn.svg") no-repeat;
 }
 
-.command-bar .img.stepOut {
+.img.stepOut {
   mask: url("resource://devtools/client/debugger/new/images/stepOut.svg") no-repeat;
 }
 
-.command-bar .img.resume {
+.img.resume {
   mask: url("resource://devtools/client/debugger/new/images/resume.svg") no-repeat;
 }
 
-.command-bar .img.rewind {
+.img.rewind {
   mask: url("resource://devtools/client/debugger/new/images/resume.svg") no-repeat;
   transform: scaleX(-1);
 }
 
-.command-bar .img.reverseStepOver {
+.img.reverseStepOver {
   mask: url("resource://devtools/client/debugger/new/images/stepOver.svg") no-repeat;
   transform: scaleX(-1);
 }
 
-.command-bar .img.reverseStepIn {
+.img.reverseStepIn {
   mask: url("resource://devtools/client/debugger/new/images/stepIn.svg") no-repeat;
   transform: scaleX(-1);
 }
 
-.command-bar .img.reverseStepOut {
+.img.reverseStepOut {
   mask: url("resource://devtools/client/debugger/new/images/stepOut.svg") no-repeat;
   transform: scaleX(-1);
 }
 
 .command-bar .filler {
   flex-grow: 1;
 }
 
--- a/devtools/client/debugger/new/dist/vendors.css
+++ b/devtools/client/debugger/new/dist/vendors.css
@@ -182,20 +182,21 @@ html[dir="rtl"] .tree-node button.arrow 
 .arrow,
 .worker,
 .refresh,
 .shortcut,
 .add-button {
   fill: var(--theme-splitter-color);
 }
 
-.folder,
-.domain,
-.file,
-.extension {
+.img.folder,
+.img.domain,
+.img.file,
+.img.extension,
+.img.worker {
   background-color: var(--theme-comment);
 }
 
 .worker,
 .file,
 .folder,
 .sources-list .source-icon,
 .extension {
@@ -219,17 +220,18 @@ html[dir="rtl"] .tree-node button.arrow 
 }
 
 .img.domain,
 .img.folder {
   width: 15px;
   height: 15px;
 }
 
-.img.extension {
+.img.extension,
+.img.worker {
   width: 13px;
   height: 13px;
   margin-inline-start: 2px;
 }
 
 .img.result-item-icon {
   height: 18px;
   width: 18px;
@@ -268,21 +270,26 @@ html[dir="rtl"] .tree-node button.arrow 
 }
 
 .img.file {
   mask: url("resource://devtools/client/debugger/new/images/file.svg") no-repeat;
   width: 13px;
   height: 13px;
 }
 
+.img.worker {
+  mask: url("resource://devtools/client/debugger/new/images/worker.svg") no-repeat;
+}
+
 .img.domain,
 .img.folder,
 .img.file,
 .sources-list .img.source-icon,
-.img.extension {
+.img.extension,
+.img.worker {
   mask-size: 100%;
   margin-inline-end: 5px;
   display: inline-block;
 }
 
 .img.result-item-icon {
   mask-size: 100%;
   margin-inline-end: 15px;
--- a/devtools/client/debugger/new/images/moz.build
+++ b/devtools/client/debugger/new/images/moz.build
@@ -25,9 +25,10 @@ DevToolsModules(
     'react.svg',
     'resume.svg',
     'stepIn.svg',
     'stepOut.svg',
     'stepOver.svg',
     'tab.svg',
     'typescript.svg',
     'vuejs.svg',
+    'worker.svg',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/images/worker.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+  <path fill-rule="evenodd" d="M8.5 8.793L5.854 6.146l-.04-.035L7.5 4.426c.2-.2.3-.4.3-.6 0-.2-.1-.4-.2-.6l-1-1c-.4-.3-.9-.3-1.2 0l-4.1 4.1c-.2.2-.3.4-.3.6 0 .2.1.4.2.6l1 1c.3.3.9.3 1.2 0l1.71-1.71.036.04L7.793 9.5l-3.647 3.646c-.195.196-.195.512 0 .708.196.195.512.195.708 0L8.5 10.207l3.646 3.647c.196.195.512.195.708 0 .195-.196.195-.512 0-.708L9.207 9.5l2.565-2.565L13.3 8.5c.1.1 2.3 1.1 2.7.7.4-.4-.3-2.7-.5-2.9l-1.1-1.1c.1-.1.2-.4.2-.6 0-.2-.1-.4-.2-.6l-.4-.4c-.3-.3-.8-.3-1.1 0l-1.5-1.4c-.2-.2-.3-.2-.5-.2s-.3.1-.5.2L9.2 3.4c-.2.1-.2.2-.2.4s.1.4.2.5l1.874 1.92L8.5 8.792z"/>
+</svg>
--- a/devtools/client/debugger/new/src/actions/debuggee.js
+++ b/devtools/client/debugger/new/src/actions/debuggee.js
@@ -4,14 +4,14 @@
 
 // @flow
 
 import type { Action, ThunkArgs } from "./types";
 import { closeTabsForMissingThreads } from "./tabs";
 
 export function updateWorkers() {
   return async function({ dispatch, getState, client }: ThunkArgs) {
-    const { workers } = await client.fetchWorkers();
+    const workers = await client.fetchWorkers();
     dispatch(({ type: "SET_WORKERS", workers }: Action));
 
     dispatch(closeTabsForMissingThreads(workers));
   };
 }
--- a/devtools/client/debugger/new/src/actions/navigation.js
+++ b/devtools/client/debugger/new/src/actions/navigation.js
@@ -15,17 +15,17 @@ import { updateWorkers } from "./debugge
 import {
   clearASTs,
   clearSymbols,
   clearScopes,
   clearSources
 } from "../workers/parser";
 
 import { clearWasmStates } from "../utils/wasm";
-
+import { getMainThread } from "../selectors";
 import type { Action, ThunkArgs } from "./types";
 
 /**
  * Redux actions for the navigation state
  * @module actions/navigation
  */
 
 /**
@@ -40,29 +40,38 @@ export function willNavigate(event: Obje
     clearSymbols();
     clearASTs();
     clearScopes();
     clearSources();
     dispatch(navigate(event.url));
   };
 }
 
-export function navigate(url: string): Action {
-  sourceQueue.clear();
+export function navigate(url: string) {
+  return async function({ dispatch, getState }: ThunkArgs) {
+    sourceQueue.clear();
+    const thread = getMainThread(getState());
 
-  return {
-    type: "NAVIGATE",
-    url
+    dispatch({
+      type: "NAVIGATE",
+      mainThread: { ...thread, url }
+    });
   };
 }
 
-export function connect(url: string, thread: string, canRewind: boolean) {
+export function connect(url: string, actor: string, canRewind: boolean) {
   return async function({ dispatch }: ThunkArgs) {
     await dispatch(updateWorkers());
-    dispatch(({ type: "CONNECT", url, thread, canRewind }: Action));
+    dispatch(
+      ({
+        type: "CONNECT",
+        mainThread: { url, actor, type: -1 },
+        canRewind
+      }: Action)
+    );
   };
 }
 
 /**
  * @memberof actions/navigation
  * @static
  */
 export function navigated() {
--- a/devtools/client/debugger/new/src/actions/pause/mapFrames.js
+++ b/devtools/client/debugger/new/src/actions/pause/mapFrames.js
@@ -13,17 +13,16 @@ import {
 } from "../../selectors";
 
 import assert from "../../utils/assert";
 import { findClosestFunction } from "../../utils/ast";
 
 import type { Frame } from "../../types";
 import type { State } from "../../reducers/types";
 import type { ThunkArgs } from "../types";
-import { features } from "../../utils/prefs";
 
 import { isGeneratedId } from "devtools-source-map";
 
 function isFrameBlackboxed(state, frame) {
   const source = getSource(state, frame.location.sourceId);
   return source && source.isBlackBoxed;
 }
 
--- a/devtools/client/debugger/new/src/actions/project-text-search.js
+++ b/devtools/client/debugger/new/src/actions/project-text-search.js
@@ -8,24 +8,33 @@
  * Redux actions for the search state
  * @module actions/search
  */
 
 import { findSourceMatches } from "../workers/search";
 import { getSource, hasPrettySource, getSourceList } from "../selectors";
 import { isThirdParty } from "../utils/source";
 import { loadSourceText } from "./sources/loadSourceText";
-import { statusType } from "../reducers/project-text-search";
+import {
+  statusType,
+  getTextSearchOperation,
+  getTextSearchStatus
+} from "../reducers/project-text-search";
 
 import type { Action, ThunkArgs } from "./types";
+import type { SearchOperation } from "../reducers/project-text-search";
 
 export function addSearchQuery(query: string): Action {
   return { type: "ADD_QUERY", query };
 }
 
+export function addOngoingSearch(ongoingSearch: SearchOperation): Action {
+  return { type: "ADD_ONGOING_SEARCH", ongoingSearch };
+}
+
 export function clearSearchQuery(): Action {
   return { type: "CLEAR_QUERY" };
 }
 
 export function addSearchResult(
   sourceId: string,
   filepath: string,
   matches: Object[]
@@ -43,34 +52,62 @@ export function clearSearchResults(): Ac
 export function clearSearch(): Action {
   return { type: "CLEAR_SEARCH" };
 }
 
 export function updateSearchStatus(status: string): Action {
   return { type: "UPDATE_STATUS", status };
 }
 
-export function closeProjectSearch(): Action {
-  return { type: "CLOSE_PROJECT_SEARCH" };
+export function closeProjectSearch() {
+  return ({ dispatch, getState }: ThunkArgs) => {
+    dispatch(stopOngoingSearch());
+    dispatch({ type: "CLOSE_PROJECT_SEARCH" });
+  };
+}
+
+export function stopOngoingSearch() {
+  return ({ dispatch, getState }: ThunkArgs) => {
+    const state = getState();
+    const ongoingSearch = getTextSearchOperation(state);
+    const status = getTextSearchStatus(state);
+    if (ongoingSearch && status !== statusType.done) {
+      ongoingSearch.cancel();
+      dispatch(updateSearchStatus(statusType.cancelled));
+    }
+  };
 }
 
 export function searchSources(query: string) {
-  return async ({ dispatch, getState }: ThunkArgs) => {
+  let cancelled = false;
+
+  const search = async ({ dispatch, getState }: ThunkArgs) => {
+    dispatch(stopOngoingSearch());
+    await dispatch(addOngoingSearch(search));
     await dispatch(clearSearchResults());
     await dispatch(addSearchQuery(query));
     dispatch(updateSearchStatus(statusType.fetching));
     const validSources = getSourceList(getState()).filter(
       source => !hasPrettySource(getState(), source.id) && !isThirdParty(source)
     );
     for (const source of validSources) {
+      if (cancelled) {
+        return;
+      }
       await dispatch(loadSourceText(source));
       await dispatch(searchSource(source.id, query));
     }
     dispatch(updateSearchStatus(statusType.done));
   };
+
+  search.cancel = () => {
+    cancelled = true;
+  };
+
+  return search;
 }
 
 export function searchSource(sourceId: string, query: string) {
   return async ({ dispatch, getState }: ThunkArgs) => {
     const source = getSource(getState(), sourceId);
     if (!source) {
       return;
     }
--- a/devtools/client/debugger/new/src/actions/tabs.js
+++ b/devtools/client/debugger/new/src/actions/tabs.js
@@ -18,17 +18,17 @@ import {
   getSourcesByURLs,
   getSourceTabs,
   getSourceFromId,
   getNewSelectedSourceId,
   removeSourceFromTabList,
   removeSourcesFromTabList
 } from "../selectors";
 
-import { getMainThread } from "../reducers/pause";
+import { getMainThread } from "../reducers/debuggee";
 
 import type { Action, ThunkArgs } from "./types";
 import type { Source, Worker } from "../types";
 
 export function updateTab(source: Source, framework: string): Action {
   const { url, id: sourceId, thread } = source;
   const isOriginal = isOriginalId(source.id);
 
@@ -101,17 +101,17 @@ export function closeTabsForMissingThrea
   return ({ dispatch, getState }: ThunkArgs) => {
     const oldTabs = getSourceTabs(getState());
     const mainThread = getMainThread(getState());
     const removed = [];
     for (const { sourceId } of oldTabs) {
       if (sourceId) {
         const source = getSourceFromId(getState(), sourceId);
         if (
-          source.thread != mainThread &&
+          source.thread != mainThread.actor &&
           !workers.some(({ actor }) => actor == source.thread)
         ) {
           removed.push(source);
         }
       }
     }
     const tabs = removeSourcesFromTabList(oldTabs, removed);
     dispatch({ type: "CLOSE_TABS", removed, tabs });
--- a/devtools/client/debugger/new/src/client/firefox/commands.js
+++ b/devtools/client/debugger/new/src/client/firefox/commands.js
@@ -411,43 +411,42 @@ async function fetchSources(): Promise<a
   // NOTE: this happens when we fetch sources and then immediately navigate
   if (!sources) {
     return [];
   }
 
   return sources;
 }
 
-async function fetchWorkers(): Promise<{ workers: Worker[] }> {
+async function fetchWorkers(): Promise<Worker[]> {
   if (features.windowlessWorkers) {
     workerClients = await updateWorkerClients({
       tabTarget,
       debuggerClient,
       threadClient,
       workerClients
     });
 
     const workerNames = Object.getOwnPropertyNames(workerClients);
 
     workerNames.forEach(actor => {
       createSources(workerClients[actor].thread);
     });
 
-    return {
-      workers: workerNames.map(actor =>
-        createWorker(actor, workerClients[actor].url)
-      )
-    };
+    return workerNames.map(actor =>
+      createWorker(actor, workerClients[actor].url)
+    );
   }
 
   if (!supportsWorkers(tabTarget)) {
-    return Promise.resolve({ workers: [] });
+    return Promise.resolve([]);
   }
 
-  return tabTarget.activeTab.listWorkers();
+  const { workers } = await tabTarget.activeTab.listWorkers();
+  return workers;
 }
 
 const clientCommands = {
   autocomplete,
   blackBox,
   createObjectClient,
   releaseActor,
   interrupt,
--- a/devtools/client/debugger/new/src/components/Editor/GutterMenu.js
+++ b/devtools/client/debugger/new/src/components/Editor/GutterMenu.js
@@ -46,29 +46,29 @@ export function gutterMenu({
       label: L10N.getStr("editor.addBreakpoint")
     },
     addLogPoint: {
       id: "node-menu-add-log-point",
       label: L10N.getStr("editor.addLogPoint")
     },
     addConditional: {
       id: "node-menu-add-conditional-breakpoint",
-      label: L10N.getStr("editor.addConditionalBreakpoint")
+      label: L10N.getStr("editor.addConditionBreakpoint")
     },
     removeBreakpoint: {
       id: "node-menu-remove-breakpoint",
       label: L10N.getStr("editor.removeBreakpoint")
     },
     editLogPoint: {
       id: "node-menu-edit-log-point",
       label: L10N.getStr("editor.editLogPoint")
     },
     editConditional: {
       id: "node-menu-edit-conditional-breakpoint",
-      label: L10N.getStr("editor.editConditionalBreakpoint")
+      label: L10N.getStr("editor.editConditionBreakpoint")
     },
     enableBreakpoint: {
       id: "node-menu-enable-breakpoint",
       label: L10N.getStr("editor.enableBreakpoint")
     },
     disableBreakpoint: {
       id: "node-menu-disable-breakpoint",
       label: L10N.getStr("editor.disableBreakpoint")
@@ -102,17 +102,17 @@ export function gutterMenu({
       ),
     accelerator: L10N.getStr("toggleCondPanel.key"),
     ...(breakpoint && breakpoint.condition
       ? gutterItems.editLogPoint
       : gutterItems.addLogPoint)
   };
 
   const conditionalBreakpoint = {
-    accesskey: L10N.getStr("editor.addConditionalBreakpoint.accesskey"),
+    accesskey: L10N.getStr("editor.addConditionBreakpoint.accesskey"),
     disabled: false,
     // Leaving column undefined so pause points can be detected
     click: () =>
       openConditionalPanel(
         breakpoint ? breakpoint.location : { line, column, sourceId }
       ),
     accelerator: L10N.getStr("toggleCondPanel.key"),
     ...(breakpoint && breakpoint.condition
--- a/devtools/client/debugger/new/src/components/Editor/Tab.js
+++ b/devtools/client/debugger/new/src/components/Editor/Tab.js
@@ -28,18 +28,17 @@ import {
 import { shouldShowPrettyPrint } from "../../utils/editor";
 import { copyToTheClipboard } from "../../utils/clipboard";
 import { getTabMenuItems } from "../../utils/tabs";
 
 import {
   getSelectedSource,
   getActiveSearch,
   getSourcesForTabs,
-  getHasSiblingOfSameName,
-  getWorkerDisplayName
+  getHasSiblingOfSameName
 } from "../../selectors";
 import type { ActiveSearchType } from "../../selectors";
 
 import classnames from "classnames";
 
 type SourcesList = List<Source>;
 
 type Props = {
@@ -47,18 +46,17 @@ type Props = {
   selectedSource: Source,
   source: Source,
   activeSearch: ActiveSearchType,
   hasSiblingOfSameName: boolean,
   selectSource: typeof actions.selectSource,
   closeTab: typeof actions.closeTab,
   closeTabs: typeof actions.closeTabs,
   togglePrettyPrint: typeof actions.togglePrettyPrint,
-  showSource: typeof actions.showSource,
-  threadName: string
+  showSource: typeof actions.showSource
 };
 
 class Tab extends PureComponent<Props> {
   onTabContextMenu = (event, tab: string) => {
     event.preventDefault();
     this.showContextMenu(event, tab);
   };
 
@@ -158,18 +156,17 @@ class Tab extends PureComponent<Props> {
 
   render() {
     const {
       selectedSource,
       selectSource,
       closeTab,
       source,
       tabSources,
-      hasSiblingOfSameName,
-      threadName
+      hasSiblingOfSameName
     } = this.props;
     const sourceId = source.id;
     const active =
       selectedSource &&
       sourceId == selectedSource.id &&
       (!this.isProjectSearchEnabled() && !this.isSourceSearchEnabled());
     const isPrettyCode = isPretty(source);
 
@@ -186,34 +183,33 @@ class Tab extends PureComponent<Props> {
 
     const className = classnames("source-tab", {
       active,
       pretty: isPrettyCode
     });
 
     const path = getDisplayPath(source, tabSources);
     const query = hasSiblingOfSameName ? getSourceQueryString(source) : "";
-    const threadNamePrefix = `${threadName}${threadName ? ": " : ""}`;
 
     return (
       <div
         className={className}
         key={sourceId}
         onClick={handleTabClick}
         // Accommodate middle click to close tab
         onMouseUp={e => e.button === 1 && closeTab(source)}
         onContextMenu={e => this.onTabContextMenu(e, sourceId)}
         title={getFileURL(source, false)}
       >
         <SourceIcon
           source={source}
           shouldHide={icon => ["file", "javascript"].includes(icon)}
         />
         <div className="filename">
-          {`${threadNamePrefix}${getTruncatedFileName(source, query)}`}
+          {getTruncatedFileName(source, query)}
           {path && <span>{`../${path}/..`}</span>}
         </div>
         <CloseButton
           handleClick={onClickClose}
           tooltip={L10N.getStr("sourceTabs.closeTabButtonTooltip")}
         />
       </div>
     );
@@ -222,18 +218,17 @@ class Tab extends PureComponent<Props> {
 
 const mapStateToProps = (state, { source }) => {
   const selectedSource = getSelectedSource(state);
 
   return {
     tabSources: getSourcesForTabs(state),
     selectedSource: selectedSource,
     activeSearch: getActiveSearch(state),
-    hasSiblingOfSameName: getHasSiblingOfSameName(state, source),
-    threadName: getWorkerDisplayName(state, source.thread)
+    hasSiblingOfSameName: getHasSiblingOfSameName(state, source)
   };
 };
 
 export default connect(
   mapStateToProps,
   {
     selectSource: actions.selectSource,
     closeTab: actions.closeTab,
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
@@ -14,64 +14,70 @@ import {
   getShownSource,
   getSelectedSource,
   getDebuggeeUrl,
   getExpandedState,
   getProjectDirectoryRoot,
   getRelativeSourcesForThread,
   getSourceCount,
   getFocusedSourceItem,
-  getWorkerDisplayName
+  getWorkerByThread,
+  getWorkerCount
 } from "../../selectors";
 
 import { getGeneratedSourceByURL } from "../../reducers/sources";
 
 // Actions
 import actions from "../../actions";
 
 // Components
+import AccessibleImage from "../shared/AccessibleImage";
 import SourcesTreeItem from "./SourcesTreeItem";
 import ManagedTree from "../shared/ManagedTree";
 import Svg from "../shared/Svg";
 
 // Utils
 import {
   createTree,
   getDirectories,
   isDirectory,
   getSourceFromNode,
   nodeHasChildren,
   updateTree
 } from "../../utils/sources-tree";
 import { getRawSourceURL } from "../../utils/source";
 
+import { getDisplayName } from "../../utils/workers";
+import { features } from "../../utils/prefs";
+
 import type {
   TreeNode,
   TreeDirectory,
   ParentMap
 } from "../../utils/sources-tree/types";
-import type { Source } from "../../types";
+import type { Worker, Source } from "../../types";
 import type { SourcesMap, State as AppState } from "../../reducers/types";
 import type { Item } from "../shared/ManagedTree";
 
 type Props = {
   thread: string,
+  worker: Worker,
   sources: SourcesMap,
   sourceCount: number,
   shownSource?: Source,
   selectedSource?: Source,
   debuggeeUrl: string,
   projectRoot: string,
   expanded: Set<string>,
   selectSource: typeof actions.selectSource,
   setExpandedState: typeof actions.setExpandedState,
   clearProjectDirectoryRoot: typeof actions.clearProjectDirectoryRoot,
   focusItem: typeof actions.focusItem,
   focused: TreeNode,
-  workerDisplayName: string
+  workerCount: number
 };
 
 type State = {
   parentMap: ParentMap,
   sourceTree: TreeDirectory,
   uncollapsedTree: TreeDirectory,
   listItems?: any,
   highlightItems?: any
@@ -319,51 +325,68 @@ class SourcesTree extends Component<Prop
           "sources-list-custom-root": projectRoot
         })}
       >
         {children}
       </div>
     );
   }
 
-  renderContents() {
-    const { projectRoot } = this.props;
+  renderThreadHeader() {
+    const { worker, workerCount } = this.props;
+
+    if (!features.windowlessWorkers || workerCount == 0) {
+      return null;
+    }
+
+    if (worker) {
+      return (
+        <div className="node thread-header">
+          <AccessibleImage className="worker" />
+          <span className="label">{getDisplayName(worker)}</span>
+        </div>
+      );
+    }
+
+    return (
+      <div className="node thread-header">
+        <AccessibleImage className={"file"} />
+        <span className="label">{L10N.getStr("mainThread")}</span>
+      </div>
+    );
+  }
+
+  render() {
+    const { projectRoot, worker } = this.props;
+
+    if (!features.windowlessWorkers && worker) {
+      return null;
+    }
 
     if (this.isEmpty()) {
       if (projectRoot) {
         return this.renderPane(
           this.renderProjectRootHeader(),
           this.renderEmptyElement(L10N.getStr("sources.noSourcesAvailableRoot"))
         );
       }
 
       return this.renderPane(
         this.renderEmptyElement(L10N.getStr("sources.noSourcesAvailable"))
       );
     }
 
     return this.renderPane(
+      this.renderThreadHeader(),
       this.renderProjectRootHeader(),
       <div key="tree" className="sources-list" onKeyDown={this.onKeyDown}>
         {this.renderTree()}
       </div>
     );
   }
-
-  render() {
-    if (this.props.workerDisplayName) {
-      return (
-        <div>
-          {this.props.workerDisplayName}
-          {this.renderContents()}
-        </div>
-      );
-    }
-    return this.renderContents();
-  }
 }
 
 function getSourceForTree(
   state: AppState,
   source: ?Source,
   thread: ?string
 ): ?Source {
   if (!source || !source.isPrettyPrinted) {
@@ -387,17 +410,18 @@ const mapStateToProps = (state, props) =
     shownSource: getSourceForTree(state, shownSource, thread),
     selectedSource: getSourceForTree(state, selectedSource, thread),
     debuggeeUrl: getDebuggeeUrl(state),
     expanded: getExpandedState(state, props.thread),
     focused: focused && focused.thread == props.thread ? focused.item : null,
     projectRoot: getProjectDirectoryRoot(state),
     sources: getRelativeSourcesForThread(state, thread),
     sourceCount: getSourceCount(state, props.thread),
-    workerDisplayName: getWorkerDisplayName(state, thread)
+    worker: getWorkerByThread(state, thread),
+    workerCount: getWorkerCount(state)
   };
 };
 
 export default connect(
   mapStateToProps,
   {
     selectSource: actions.selectSource,
     setExpandedState: actions.setExpandedState,
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/index.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/index.js
@@ -1,51 +1,51 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import React, { Component } from "react";
-import { sortBy } from "lodash";
-import { connect } from "../../utils/connect";
+import classnames from "classnames";
 import { Tab, Tabs, TabList, TabPanels } from "react-aria-components/src/tabs";
-import { formatKeyShortcut } from "../../utils/text";
+
 import actions from "../../actions";
 import {
   getRelativeSources,
   getActiveSearch,
   getSelectedPrimaryPaneTab,
-  getWorkerDisplayName,
-  isValidThread
+  getThreads
 } from "../../selectors";
 import { features, prefs } from "../../utils/prefs";
-import "./Sources.css";
-import classnames from "classnames";
+import { connect } from "../../utils/connect";
+import { formatKeyShortcut } from "../../utils/text";
 
 import Outline from "./Outline";
 import SourcesTree from "./SourcesTree";
 
 import type { SourcesMapByThread } from "../../reducers/types";
 import type { SelectedPrimaryPaneTabType } from "../../selectors";
+import type { Thread } from "../../types";
+
+import "./Sources.css";
 
 type State = {
   alphabetizeOutline: boolean
 };
 
 type Props = {
   selectedTab: SelectedPrimaryPaneTabType,
   sources: SourcesMapByThread,
   horizontal: boolean,
   sourceSearchOn: boolean,
   setPrimaryPaneTab: typeof actions.setPrimaryPaneTab,
   setActiveSearch: typeof actions.setActiveSearch,
   closeActiveSearch: typeof actions.closeActiveSearch,
-  getWorkerDisplayName: string => string,
-  isValidThread: string => boolean
+  threads: Thread[]
 };
 
 class PrimaryPanes extends Component<Props, State> {
   constructor(props: Props) {
     super(props);
 
     this.state = {
       alphabetizeOutline: prefs.alphabetizeOutline
@@ -92,24 +92,19 @@ class PrimaryPanes extends Component<Pro
         key="outline-tab"
       >
         {outline}
       </Tab>
     ];
   }
 
   renderThreadSources() {
-    const threads = sortBy(
-      Object.getOwnPropertyNames(this.props.sources).filter(
-        this.props.isValidThread
-      ),
-      this.props.getWorkerDisplayName
-    );
-
-    return threads.map(thread => <SourcesTree thread={thread} key={thread} />);
+    return this.props.threads.map(({ actor }) => (
+      <SourcesTree thread={actor} key={actor} />
+    ));
   }
 
   render() {
     const { selectedTab } = this.props;
     const activeIndex = selectedTab === "sources" ? 0 : 1;
 
     return (
       <Tabs
@@ -131,18 +126,17 @@ class PrimaryPanes extends Component<Pro
     );
   }
 }
 
 const mapStateToProps = state => ({
   selectedTab: getSelectedPrimaryPaneTab(state),
   sources: getRelativeSources(state),
   sourceSearchOn: getActiveSearch(state) === "source",
-  getWorkerDisplayName: thread => getWorkerDisplayName(state, thread),
-  isValidThread: thread => isValidThread(state, thread)
+  threads: getThreads(state)
 });
 
 const connector = connect(
   mapStateToProps,
   {
     setPrimaryPaneTab: actions.setPrimaryPaneTab,
     setActiveSearch: actions.setActiveSearch,
     closeActiveSearch: actions.closeActiveSearch
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js
@@ -9,80 +9,69 @@ import actions from "../../../actions";
 import {
   getTruncatedFileName,
   getDisplayPath,
   getSourceQueryString,
   getFileURL
 } from "../../../utils/source";
 import {
   getHasSiblingOfSameName,
-  getBreakpointsForSource,
-  getWorkerDisplayName
+  getBreakpointsForSource
 } from "../../../selectors";
 
 import SourceIcon from "../../shared/SourceIcon";
 
 import type { Source, Breakpoint } from "../../../types";
 import showContextMenu from "./BreakpointHeadingsContextMenu";
 
 type Props = {
   sources: Source[],
   source: Source,
   hasSiblingOfSameName: boolean,
   breakpointsForSource: Breakpoint[],
   disableBreakpointsInSource: typeof actions.disableBreakpointsInSource,
   enableBreakpointsInSource: typeof actions.enableBreakpointsInSource,
   removeBreakpointsInSource: typeof actions.removeBreakpointsInSource,
-  selectSource: typeof actions.selectSource,
-  threadName: string
+  selectSource: typeof actions.selectSource
 };
 
 class BreakpointHeading extends PureComponent<Props> {
   onContextMenu = e => {
     showContextMenu({ ...this.props, contextMenuEvent: e });
   };
 
   render() {
-    const {
-      sources,
-      source,
-      hasSiblingOfSameName,
-      selectSource,
-      threadName
-    } = this.props;
+    const { sources, source, hasSiblingOfSameName, selectSource } = this.props;
 
     const path = getDisplayPath(source, sources);
     const query = hasSiblingOfSameName ? getSourceQueryString(source) : "";
 
     return (
       <div
         className="breakpoint-heading"
         title={getFileURL(source, false)}
         onClick={() => selectSource(source.id)}
         onContextMenu={this.onContextMenu}
       >
         <SourceIcon
           source={source}
           shouldHide={icon => ["file", "javascript"].includes(icon)}
         />
         <div className="filename">
-          {threadName +
-            (threadName ? ": " : "") +
-            getTruncatedFileName(source, query)}
+          {getTruncatedFileName(source, query)}
           {path && <span>{`../${path}/..`}</span>}
         </div>
       </div>
     );
   }
 }
 
 const mapStateToProps = (state, { source }) => ({
   hasSiblingOfSameName: getHasSiblingOfSameName(state, source),
-  breakpointsForSource: getBreakpointsForSource(state, source.id),
-  threadName: getWorkerDisplayName(state, source.thread)
+  breakpointsForSource: getBreakpointsForSource(state, source.id)
 });
 
 export default connect(
   mapStateToProps,
   {
     selectSource: actions.selectSource,
     enableBreakpointsInSource: actions.enableBreakpointsInSource,
     disableBreakpointsInSource: actions.disableBreakpointsInSource,
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/CommandBar.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/CommandBar.js
@@ -43,18 +43,18 @@ const KEYS = {
     stepOver: "Cmd+'",
     stepIn: "Cmd+;",
     stepOut: "Cmd+Shift+:",
     stepOutDisplay: "Cmd+Shift+;"
   },
   Linux: {
     resume: "F8",
     stepOver: "F10",
-    stepIn: "Ctrl+F11",
-    stepOut: "Ctrl+Shift+F11"
+    stepIn: "F11",
+    stepOut: "Shift+F11"
   }
 };
 
 function getKey(action) {
   return getKeyForOS(appinfo.OS, action);
 }
 
 function getKeyForOS(os, action) {
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Group.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Frames/Group.js
@@ -122,23 +122,24 @@ export default class Group extends Compo
             disableContextMenu={disableContextMenu}
           />
         ))}
       </div>
     );
   }
 
   renderDescription() {
+    const { l10n } = this.context;
+
     const frame = this.props.group[0];
-    const displayName = formatDisplayName(frame);
+    const displayName = formatDisplayName(frame, undefined, l10n);
 
     const l10NEntry = this.state.expanded
       ? "callStack.group.collapseTooltip"
       : "callStack.group.expandTooltip";
-    const { l10n } = this.context;
     const title = l10n.getFormatStr(l10NEntry, frame.library);
 
     return (
       <li
         key={frame.id}
         className={classNames("group")}
         onClick={this.toggleFrames}
         tabIndex={0}
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Worker.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import React, { Component } from "react";
+import { connect } from "../../utils/connect";
+import classnames from "classnames";
+
+import actions from "../../actions";
+import { getCurrentThread, getThreadIsPaused } from "../../selectors";
+import { getDisplayName, isWorker } from "../../utils/workers";
+import AccessibleImage from "../shared/AccessibleImage";
+
+import type { Thread } from "../../types";
+
+type Props = {
+  selectThread: typeof actions.selectThread,
+  isPaused: boolean,
+  thread: Thread,
+  currentThread: string
+};
+
+export class Worker extends Component<Props> {
+  render() {
+    const { currentThread, isPaused, thread } = this.props;
+
+    const label = isWorker(thread)
+      ? getDisplayName(thread)
+      : L10N.getStr("mainThread");
+
+    return (
+      <div
+        className={classnames(
+          "worker",
+          thread.actor == currentThread && "selected"
+        )}
+        key={thread.actor}
+        onClick={() => this.props.selectThread(thread.actor)}
+      >
+        <div clasName="icon">
+          <AccessibleImage className={isWorker ? "worker" : "file"} />
+        </div>
+        <div className="label">{label}</div>
+        {isPaused ? (
+          <div className="pause-badge">
+            <AccessibleImage className="pause" />
+          </div>
+        ) : null}
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state, props: Props) => ({
+  currentThread: getCurrentThread(state),
+  isPaused: getThreadIsPaused(state, props.thread.actor)
+});
+
+export default connect(
+  mapStateToProps,
+  {
+    selectThread: actions.selectThread
+  }
+)(Worker);
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/Workers.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/Workers.js
@@ -1,101 +1,66 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
+// @flow
+
 import React, { Component } from "react";
 import { connect } from "../../utils/connect";
-import type { List } from "immutable";
+
+import actions from "../../actions";
+import { getThreads } from "../../selectors";
+import { getDisplayName } from "../../utils/workers";
+import { features } from "../../utils/prefs";
+import Worker from "./Worker";
+import AccessibleImage from "../shared/AccessibleImage";
+
+import type { Thread, Worker as WorkerType } from "../../types";
 
 import "./Workers.css";
 
-import actions from "../../actions";
-import {
-  getMainThread,
-  getCurrentThread,
-  threadIsPaused,
-  getWorkers,
-  getWorkerDisplayName
-} from "../../selectors";
-import { basename } from "../../utils/path";
-import { features } from "../../utils/prefs";
-import type { Worker } from "../../types";
-import AccessibleImage from "../shared/AccessibleImage";
-import classnames from "classnames";
-
 type Props = {
-  selectThread: string => void
+  threads: Thread[],
+  openWorkerToolbox: typeof actions.openWorkerToolbox
 };
 
 export class Workers extends Component<Props> {
-  props: {
-    workers: List<Worker>,
-    openWorkerToolbox: object => void,
-    mainThread: string,
-    currentThread: string
-  };
+  renderWorker(thread: WorkerType) {
+    const { openWorkerToolbox } = this.props;
 
-  renderWorkers(workers, mainThread, currentThread) {
-    if (features.windowlessWorkers) {
-      return [{ actor: mainThread }, ...workers].map(worker => (
-        <div
-          className={classnames(
-            "worker",
-            worker.actor == currentThread && "selected"
-          )}
-          key={worker.actor}
-          onClick={() => this.props.selectThread(worker.actor)}
-        >
-          <img className="domain" />
-          {(worker.url
-            ? `${this.props.getWorkerDisplayName(worker.actor)}: ${basename(
-                worker.url
-              )}`
-            : "Main Thread") +
-            (this.props.threadIsPaused(worker.actor) ? " PAUSED" : "")}
-        </div>
-      ));
-    }
-    const { openWorkerToolbox } = this.props;
-    return workers.map(worker => (
+    return (
       <div
         className="worker"
-        key={worker.actor}
-        onClick={() => openWorkerToolbox(worker)}
+        key={thread.actor}
+        onClick={() => openWorkerToolbox(thread)}
       >
-        <AccessibleImage className="domain" />
-        {basename(worker.url)}
+        <div className="icon">
+          <AccessibleImage className={"worker"} />
+        </div>
+        <div className="label">{getDisplayName(thread)}</div>
       </div>
-    ));
-  }
-
-  renderNoWorkersPlaceholder() {
-    return <div className="pane-info">{L10N.getStr("noWorkersText")}</div>;
+    );
   }
 
   render() {
-    const { workers, mainThread, currentThread } = this.props;
-    return (
-      <div className="pane workers-list">
-        {workers && workers.size > 0
-          ? this.renderWorkers(workers, mainThread, currentThread)
-          : this.renderNoWorkersPlaceholder()}
-      </div>
-    );
+    const { threads } = this.props;
+
+    const workerList = features.windowlessWorkers
+      ? threads.map(thread => <Worker thread={thread} key={thread.actor} />)
+      : threads
+          .filter((thread: any) => thread.actorID)
+          .map(worker => this.renderWorker((worker: any)));
+
+    return <div className="pane workers-list">{workerList}</div>;
   }
 }
 
 const mapStateToProps = state => ({
-  workers: getWorkers(state),
-  getWorkerDisplayName: thread => getWorkerDisplayName(state, thread),
-  mainThread: getMainThread(state),
-  currentThread: getCurrentThread(state),
-  threadIsPaused: thread => threadIsPaused(state, thread)
+  threads: getThreads(state)
 });
 
 export default connect(
   mapStateToProps,
   {
-    openWorkerToolbox: actions.openWorkerToolbox,
-    selectThread: actions.selectThread
+    openWorkerToolbox: actions.openWorkerToolbox
   }
 )(Workers);
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/index.js
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/index.js
@@ -33,18 +33,17 @@ import Accordion from "../shared/Accordi
 import CommandBar from "./CommandBar";
 import UtilsBar from "./UtilsBar";
 import XHRBreakpoints from "./XHRBreakpoints";
 
 import Scopes from "./Scopes";
 
 import "./SecondaryPanes.css";
 
-import type { Expression } from "../../types";
-import type { WorkersList } from "../../reducers/types";
+import type { Expression, WorkerList } from "../../types";
 
 type AccordionPaneItem = {
   header: string,
   component: any,
   opened?: boolean,
   onToggle?: () => void,
   shouldOpen?: () => boolean,
   buttons?: any
@@ -73,17 +72,17 @@ type Props = {
   hasFrames: boolean,
   horizontal: boolean,
   breakpoints: Object,
   breakpointsDisabled: boolean,
   breakpointsLoading: boolean,
   isWaitingOnBreak: boolean,
   shouldPauseOnExceptions: boolean,
   shouldPauseOnCaughtExceptions: boolean,
-  workers: WorkersList,
+  workers: WorkerList,
   toggleShortcutsModal: () => void,
   toggleAllBreakpoints: typeof actions.toggleAllBreakpoints,
   evaluateExpressions: typeof actions.evaluateExpressions,
   pauseOnExceptions: typeof actions.pauseOnExceptions,
   breakOnNext: typeof actions.breakOnNext
 };
 
 class SecondaryPanes extends Component<Props, State> {
@@ -258,17 +257,19 @@ class SecondaryPanes extends Component<P
       onToggle: opened => {
         prefs.callStackVisible = opened;
       }
     };
   }
 
   getWorkersItem(): AccordionPaneItem {
     return {
-      header: L10N.getStr("workersHeader"),
+      header: features.windowlessWorkers
+        ? L10N.getStr("threadsHeader")
+        : L10N.getStr("workersHeader"),
       className: "workers-pane",
       component: <Workers />,
       opened: prefs.workersVisible,
       onToggle: opened => {
         prefs.workersVisible = opened;
       }
     };
   }
@@ -298,17 +299,17 @@ class SecondaryPanes extends Component<P
     };
   }
 
   getStartItems() {
     const { workers } = this.props;
 
     const items: Array<AccordionPaneItem> = [];
     if (this.props.horizontal) {
-      if (features.workers && workers.size > 0) {
+      if (features.workers && workers.length > 0) {
         items.push(this.getWorkersItem());
       }
 
       items.push(this.getWatchItem());
     }
 
     items.push(this.getBreakpointsItem());
 
@@ -335,17 +336,17 @@ class SecondaryPanes extends Component<P
     const { workers } = this.props;
 
     let items: Array<AccordionPaneItem> = [];
 
     if (this.props.horizontal) {
       return [];
     }
 
-    if (features.workers && workers.size > 0) {
+    if (features.workers && workers.length > 0) {
       items.push(this.getWorkersItem());
     }
 
     items.push(this.getWatchItem());
 
     if (this.props.hasFrames) {
       items = [...items, this.getScopeItem()];
     }
--- a/devtools/client/debugger/new/src/components/SecondaryPanes/moz.build
+++ b/devtools/client/debugger/new/src/components/SecondaryPanes/moz.build
@@ -9,11 +9,12 @@ DIRS += [
 ]
 
 DebuggerModules(
     'CommandBar.js',
     'Expressions.js',
     'index.js',
     'Scopes.js',
     'UtilsBar.js',
+    'Worker.js',
     'Workers.js',
     'XHRBreakpoints.js',
 )
--- a/devtools/client/debugger/new/src/reducers/debuggee.js
+++ b/devtools/client/debugger/new/src/reducers/debuggee.js
@@ -4,63 +4,66 @@
 
 // @flow
 
 /**
  * Debuggee reducer
  * @module reducers/debuggee
  */
 
-import { List } from "immutable";
-import type { Record } from "../utils/makeRecord";
-import type { Worker } from "../types";
+import { sortBy } from "lodash";
+import { getDisplayName } from "../utils/workers";
+
+import type { MainThread, WorkerList } from "../types";
 import type { Action } from "../actions/types";
-import makeRecord from "../utils/makeRecord";
-import { getMainThread } from "./pause";
 
-export type WorkersList = List<Worker>;
-
-type DebuggeeState = {
-  workers: WorkersList
+export type DebuggeeState = {
+  workers: WorkerList,
+  mainThread: MainThread
 };
 
-export const createDebuggeeState: () => Record<DebuggeeState> = makeRecord({
-  workers: List()
-});
+export function initialDebuggeeState(): DebuggeeState {
+  return { workers: [], mainThread: { actor: "", url: "", type: -1 } };
+}
 
 export default function debuggee(
-  state: Record<DebuggeeState> = createDebuggeeState(),
+  state: DebuggeeState = initialDebuggeeState(),
   action: Action
-): Record<DebuggeeState> {
+): DebuggeeState {
   switch (action.type) {
+    case "CONNECT":
+      return {
+        ...state,
+        mainThread: action.mainThread
+      };
     case "SET_WORKERS":
-      return state.set("workers", List(action.workers));
+      return { ...state, workers: action.workers };
+    case "NAVIGATE":
+      return {
+        ...initialDebuggeeState(),
+        mainThread: action.mainThread
+      };
     default:
       return state;
   }
 }
 
 export const getWorkers = (state: OuterState) => state.debuggee.workers;
 
-export const getWorkerDisplayName = (state: OuterState, thread: string) => {
-  let index = 1;
-  for (const { actor } of state.debuggee.workers) {
-    if (actor == thread) {
-      return `Worker #${index}`;
-    }
-    index++;
-  }
-  return "";
-};
+export const getWorkerCount = (state: OuterState) => getWorkers(state).length;
+
+export function getWorkerByThread(state: OuterState, thread: string) {
+  return getWorkers(state).find(worker => worker.actor == thread);
+}
 
-export const isValidThread = (state: OuterState, thread: string) => {
-  if (thread == getMainThread((state: any))) {
-    return true;
-  }
-  for (const { actor } of state.debuggee.workers) {
-    if (actor == thread) {
-      return true;
-    }
-  }
-  return false;
-};
+export function getMainThread(state: OuterState): MainThread {
+  return state.debuggee.mainThread;
+}
+
+export function getDebuggeeUrl(state: OuterState): string {
+  return getMainThread(state).url;
+}
+
+export function getThreads(state: OuterState) {
+  return [getMainThread(state), ...sortBy(getWorkers(state), getDisplayName)];
+}
 
 type OuterState = { debuggee: DebuggeeState };
--- a/devtools/client/debugger/new/src/reducers/pause.js
+++ b/devtools/client/debugger/new/src/reducers/pause.js
@@ -70,29 +70,25 @@ type ThreadPauseState = {
   shouldPauseOnCaughtExceptions: boolean,
   command: Command,
   previousLocation: ?MappedLocation,
   skipPausing: boolean
 };
 
 // Pause state describing all threads.
 export type PauseState = {
-  mainThread: string,
   currentThread: string,
-  debuggeeUrl: string,
   canRewind: boolean,
   threads: { [string]: ThreadPauseState }
 };
 
 export const createPauseState = (): PauseState => ({
-  mainThread: "UnknownThread",
   currentThread: "UnknownThread",
   threads: {},
-  canRewind: false,
-  debuggeeUrl: ""
+  canRewind: false
 });
 
 const resumedPauseState = {
   frames: null,
   frameScopes: {
     generated: {},
     original: {},
     mappings: {}
@@ -103,17 +99,16 @@ const resumedPauseState = {
 };
 
 const createInitialPauseState = () => ({
   ...resumedPauseState,
   isWaitingOnBreak: false,
   shouldPauseOnExceptions: prefs.pauseOnExceptions,
   shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions,
   canRewind: false,
-  debuggeeUrl: "",
   command: null,
   previousLocation: null,
   skipPausing: prefs.skipPausing
 });
 
 function getThreadPauseState(state: PauseState, thread: string) {
   // Thread state is lazily initialized so that we don't have to keep track of
   // the current set of worker threads.
@@ -241,19 +236,17 @@ function update(
           [action.objectId]: action.properties
         }
       });
     }
 
     case "CONNECT":
       return {
         ...createPauseState(),
-        mainThread: action.thread,
-        currentThread: action.thread,
-        debuggeeUrl: action.url,
+        currentThread: action.mainThread.actor,
         canRewind: action.canRewind
       };
 
     case "PAUSE_ON_EXCEPTIONS": {
       const { shouldPauseOnExceptions, shouldPauseOnCaughtExceptions } = action;
 
       prefs.pauseOnExceptions = shouldPauseOnExceptions;
       prefs.pauseOnCaughtExceptions = shouldPauseOnCaughtExceptions;
@@ -287,24 +280,23 @@ function update(
     case "EVALUATE_EXPRESSION":
       return updateThreadState({
         command: action.status === "start" ? "expression" : null
       });
 
     case "NAVIGATE":
       return {
         ...state,
-        currentThread: state.mainThread,
+        currentThread: action.mainThread.actor,
         threads: {
-          [state.mainThread]: {
-            ...state.threads[state.mainThread],
+          [action.mainThread.actor]: {
+            ...state.threads[action.mainThread.actor],
             ...resumedPauseState
           }
-        },
-        debuggeeUrl: action.url
+        }
       };
 
     case "TOGGLE_SKIP_PAUSING": {
       const { skipPausing } = action;
       prefs.skipPausing = skipPausing;
 
       return updateThreadState({ skipPausing });
     }
@@ -360,25 +352,21 @@ export function getPauseReason(state: Ou
 export function getPauseCommand(state: OuterState): Command {
   return getCurrentPauseState(state).command;
 }
 
 export function isStepping(state: OuterState) {
   return ["stepIn", "stepOver", "stepOut"].includes(getPauseCommand(state));
 }
 
-export function getMainThread(state: OuterState) {
-  return state.pause.mainThread;
-}
-
 export function getCurrentThread(state: OuterState) {
   return state.pause.currentThread;
 }
 
-export function threadIsPaused(state: OuterState, thread: string) {
+export function getThreadIsPaused(state: OuterState, thread: string) {
   return !!getThreadPauseState(state.pause, thread).frames;
 }
 
 export function isPaused(state: OuterState) {
   return !!getFrames(state);
 }
 
 export function getIsPaused(state: OuterState) {
@@ -549,20 +537,16 @@ export const getSelectedFrame: Selector<
     if (!frames) {
       return null;
     }
 
     return frames.find(frame => frame.id == selectedFrameId);
   }
 );
 
-export function getDebuggeeUrl(state: OuterState) {
-  return state.pause.debuggeeUrl;
-}
-
 export function getSkipPausing(state: OuterState) {
   return getCurrentPauseState(state).skipPausing;
 }
 
 // NOTE: currently only used for chrome
 export function getChromeScopes(state: OuterState) {
   const frame: ?ChromeFrame = (getSelectedFrame(state): any);
   return frame ? frame.scopeChain : undefined;
--- a/devtools/client/debugger/new/src/reducers/project-text-search.js
+++ b/devtools/client/debugger/new/src/reducers/project-text-search.js
@@ -6,33 +6,44 @@
 // @format
 
 /**
  * Project text search reducer
  * @module reducers/project-text-search
  */
 
 import type { Action } from "../actions/types";
+import type { Cancellable } from "../types";
 
 export type Search = {
   +sourceId: string,
   +filepath: string,
   +matches: any[]
 };
-export type StatusType = "INITIAL" | "FETCHING" | "DONE" | "ERROR";
+
+export type SearchOperation = Cancellable;
+
+export type StatusType =
+  | "INITIAL"
+  | "FETCHING"
+  | "CANCELLED"
+  | "DONE"
+  | "ERROR";
 export const statusType = {
   initial: "INITIAL",
   fetching: "FETCHING",
+  cancelled: "CANCELLED",
   done: "DONE",
   error: "ERROR"
 };
 
 export type ResultList = Search[];
 export type ProjectTextSearchState = {
   +query: string,
+  +ongoingSearch?: SearchOperation,
   +results: ResultList,
   +status: string
 };
 
 export function initialProjectTextSearchState(): ProjectTextSearchState {
   return {
     query: "",
     results: [],
@@ -69,26 +80,33 @@ function update(
       return { ...state, results: [...results, result] };
 
     case "UPDATE_STATUS":
       return { ...state, status: action.status };
 
     case "CLEAR_SEARCH_RESULTS":
       return { ...state, results: [] };
 
+    case "ADD_ONGOING_SEARCH":
+      return { ...state, ongoingSearch: action.ongoingSearch };
+
     case "CLEAR_SEARCH":
     case "CLOSE_PROJECT_SEARCH":
     case "NAVIGATE":
       return initialProjectTextSearchState();
   }
   return state;
 }
 
 type OuterState = { projectTextSearch: ProjectTextSearchState };
 
+export function getTextSearchOperation(state: OuterState) {
+  return state.projectTextSearch.ongoingSearch;
+}
+
 export function getTextSearchResults(state: OuterState) {
   return state.projectTextSearch.results;
 }
 
 export function getTextSearchStatus(state: OuterState) {
   return state.projectTextSearch.status;
 }
 
--- a/devtools/client/debugger/new/src/reducers/sources.js
+++ b/devtools/client/debugger/new/src/reducers/sources.js
@@ -12,20 +12,21 @@
 import { createSelector } from "reselect";
 import {
   getPrettySourceURL,
   underRoot,
   getRelativeUrl,
   isGenerated,
   isOriginal as isOriginalSource
 } from "../utils/source";
+
 import { originalToGeneratedId } from "devtools-source-map";
 import { prefs } from "../utils/prefs";
 
-import type { Source, SourceId, SourceLocation } from "../types";
+import type { Source, SourceId, SourceLocation, Thread } from "../types";
 import type { PendingSelectedLocation, Selector } from "./types";
 import type { Action, DonePromiseAction, FocusItem } from "../actions/types";
 import type { LoadSourceAction } from "../actions/types/SourceAction";
 
 export type SourcesMap = { [string]: Source };
 export type SourcesMapByThread = { [string]: SourcesMap };
 
 type UrlsMap = { [string]: SourceId[] };
@@ -137,28 +138,25 @@ function update(
         updateBlackBoxList(url, isBlackBoxed);
         return updateSources(state, [{ id, isBlackBoxed }]);
       }
       break;
 
     case "SET_PROJECT_DIRECTORY_ROOT":
       return updateProjectDirectoryRoot(state, action.url);
 
-    case "NAVIGATE":
-      const source =
-        state.selectedLocation &&
-        state.sources[state.selectedLocation.sourceId];
+    case "SET_WORKERS":
+      return addRelativeSourceThreads(state, action.workers);
 
-      const url = source && source.url;
+    case "NAVIGATE":
+      const newState = initialSourcesState();
+      return addRelativeSourceThread(newState, action.mainThread);
 
-      if (!url) {
-        return initialSourcesState();
-      }
-
-      return { ...initialSourcesState(), url };
+    case "CONNECT":
+      return addRelativeSourceThread(state, action.mainThread);
 
     case "SET_FOCUSED_SOURCE_ITEM":
       return { ...state, focusedItem: action.item };
   }
 
   return state;
 }
 
@@ -258,16 +256,35 @@ function updateRelativeSource(
     relativeSources[source.thread] = {};
   }
 
   relativeSources[source.thread][source.id] = relativeSource;
 
   return relativeSources;
 }
 
+function addRelativeSourceThread(state: SourcesState, thread: Thread) {
+  if (getRelativeSourcesForThread({ sources: state }, thread.actor)) {
+    return state;
+  }
+  return {
+    ...state,
+    relativeSources: { ...state.relativeSources, [thread.actor]: {} }
+  };
+}
+
+function addRelativeSourceThreads(state: SourcesState, workers: Thread[]) {
+  let newState = state;
+  for (const worker of workers) {
+    newState = addRelativeSourceThread(newState, worker);
+  }
+
+  return newState;
+}
+
 function updateProjectDirectoryRoot(state: SourcesState, root: string) {
   prefs.projectDirectoryRoot = root;
 
   const relativeSources = getSourceList({ sources: state }).reduce(
     (sources, source: Source) => updateRelativeSource(sources, source, root),
     {}
   );
 
--- a/devtools/client/debugger/new/src/utils/moz.build
+++ b/devtools/client/debugger/new/src/utils/moz.build
@@ -44,9 +44,10 @@ DebuggerModules(
     'telemetry.js',
     'text.js',
     'timings.js',
     'ui.js',
     'url.js',
     'utils.js',
     'wasm.js',
     'worker.js',
+    'workers.js',
 )
--- a/devtools/client/debugger/new/src/utils/pause/frames/displayName.js
+++ b/devtools/client/debugger/new/src/utils/pause/frames/displayName.js
@@ -79,26 +79,26 @@ function getFrameDisplayName(frame: Loca
 }
 
 type formatDisplayNameParams = {
   shouldMapDisplayName: boolean
 };
 export function formatDisplayName(
   frame: LocalFrame,
   { shouldMapDisplayName = true }: formatDisplayNameParams = {},
-  l10n: Object
+  l10n: typeof L10N
 ): string {
   const { library } = frame;
   let displayName = getFrameDisplayName(frame);
   if (library && shouldMapDisplayName) {
     displayName = mapDisplayNames(frame, library);
   }
 
   return simplifyDisplayName(displayName) || l10n.getStr("anonymousFunction");
 }
 
-export function formatCopyName(frame: LocalFrame, l10n: Object): string {
+export function formatCopyName(frame: LocalFrame, l10n: typeof L10N): string {
   const displayName = formatDisplayName(frame, undefined, l10n);
   const fileName = getFilename(frame.source);
   const frameLocation = frame.location.line;
 
   return `${displayName} (${fileName}#${frameLocation})`;
 }
--- a/devtools/client/debugger/new/src/utils/pause/frames/index.js
+++ b/devtools/client/debugger/new/src/utils/pause/frames/index.js
@@ -1,9 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
+// @flow
+
 export * from "./annotateFrames";
 export * from "./collapseFrames";
 export * from "./displayName";
 export * from "./getFrameUrl";
 export * from "./getLibraryFromUrl";
--- a/devtools/client/debugger/new/src/utils/pause/scopes/getScope.js
+++ b/devtools/client/debugger/new/src/utils/pause/scopes/getScope.js
@@ -78,17 +78,17 @@ export function getScope(
       const this_ = getThisVariable(thisDesc_, key);
 
       if (this_) {
         vars.push(this_);
       }
     }
 
     if (vars && vars.length) {
-      const title = getScopeTitle(type, scope);
+      const title = getScopeTitle(type, scope) || "";
       vars.sort((a, b) => a.name.localeCompare(b.name));
       return {
         name: title,
         path: key,
         contents: vars,
         type: NODE_TYPES.BLOCK
       };
     }
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/utils/workers.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { basename } from "./path";
+import type { Thread } from "../types";
+
+export function getDisplayName(thread: Thread) {
+  return basename(thread.url);
+}
+
+export function isWorker(thread: Thread) {
+  return thread.actor.includes("workerTarget");
+}
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-call-stack.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-call-stack.js
@@ -12,16 +12,53 @@ function isFrameSelected(dbg, index, tit
   return elSelected && titleSelected;
 }
 
 function toggleButton(dbg) {
   const callStackBody = findElement(dbg, "callStackBody");
   return callStackBody.querySelector(".show-more");
 }
 
+// Create an HTTP server to simulate an angular app with anonymous functions
+// and return the URL.
+function createMockAngularPage() {
+  const httpServer = createTestHTTPServer();
+
+  httpServer.registerContentType("html", "text/html");
+  httpServer.registerContentType("js", "application/javascript");
+
+  const htmlFilename = "angular-mock.html"
+  httpServer.registerPathHandler(`/${htmlFilename}`, function(request, response) {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.write(`
+        <html>
+            <button class="pause">Click me</button>
+            <script type="text/javascript" src="angular.js"></script>
+        </html>`
+      );
+    }
+  );
+
+  // Register an angular.js file in order to create a Group with anonymous functions in
+  // the callstack panel.
+  httpServer.registerPathHandler("/angular.js", function(request, response) {
+    response.setHeader("Content-Type", "application/javascript");
+    response.write(`
+      document.querySelector("button.pause").addEventListener("click", () => {
+        (function() {
+          debugger;
+        })();
+      })
+    `);
+  });
+
+  const port = httpServer.identity.primaryPort;
+  return `http://localhost:${port}/${htmlFilename}`;
+}
+
 add_task(async function() {
   const dbg = await initDebugger("doc-script-switching.html");
 
   const found = findElement(dbg, "callStackBody");
   is(found, null, "Call stack is hidden");
 
   invokeInTab("firstCall");
   await waitForPaused(dbg);
@@ -47,8 +84,32 @@ add_task(async function() {
   button.click();
 
   button = toggleButton(dbg);
   frames = findAllElements(dbg, "frames");
   is(button.innerText, "Collapse rows", "toggle button should be collapsed");
   is(frames.length, 22, "All of the frames should be shown");
   await waitForSelectedSource(dbg, "frames.js");
 });
+
+
+add_task(async function() {
+  const url = createMockAngularPage();
+  const tab = await addTab(url);
+  info("Open debugger");
+  const toolbox = await openToolboxForTab(tab, "jsdebugger");
+  const dbg = createDebuggerContext(toolbox);
+
+  const found = findElement(dbg, "callStackBody");
+  is(found, null, "Call stack is hidden");
+
+  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+    content.document.querySelector("button.pause").click();
+  });
+
+  await waitForPaused(dbg);
+  const $group = findElementWithSelector(dbg, ".frames .frames-group");
+  is($group.querySelector(".title").textContent,
+    "<anonymous>", "Group has expected frame title");
+  is($group.querySelector(".badge").textContent, "2", "Group has expected badge");
+  is($group.querySelector(".location").textContent, "Angular",
+    "Group has expected location");
+});
--- a/devtools/client/debugger/new/test/mochitest/helpers.js
+++ b/devtools/client/debugger/new/test/mochitest/helpers.js
@@ -425,17 +425,17 @@ function assertPausedAtSourceAndLine(dbg
 async function getWorkers(dbg) {
   await dbg.actions.updateWorkers();
 
   const {
     selectors: { getWorkers },
     getState
   } = dbg;
 
-  return getWorkers(getState()).toJS();
+  return getWorkers(getState());
 }
 
 async function waitForLoadedScopes(dbg) {
   const scopes = await waitForElement(dbg, "scopes");
   // Since scopes auto-expand, we can assume they are loaded when there is a tree node
   // with the aria-level attribute equal to "2".
   await waitUntil(() => scopes.querySelector(`.tree-node[aria-level="2"]`));
 }
--- a/devtools/client/locales/en-US/debugger.properties
+++ b/devtools/client/locales/en-US/debugger.properties
@@ -109,16 +109,23 @@ ignoreCaughtExceptionsItem=Ignore caught
 # LOCALIZATION NOTE (pauseOnCaughtExceptionsItem): The pause on exceptions checkbox description
 # when the debugger should pause on caught exceptions
 pauseOnCaughtExceptionsItem=Pause on caught exceptions
 
 # LOCALIZATION NOTE (workersHeader): The text to display in the events
 # header.
 workersHeader=Workers
 
+# LOCALIZATION NOTE (threadsHeader): The text to describe the threads header
+threadsHeader=Threads
+
+# LOCALIZATION NOTE (mainThread): The text to describe the thread of the
+# program as opposed to worker threads.
+mainThread=Main Thread
+
 # LOCALIZATION NOTE (noWorkersText): The text to display in the workers list
 # when there are no workers.
 noWorkersText=This page has no workers.
 
 # LOCALIZATION NOTE (noSourcesText): The text to display in the sources list
 # when there are no sources.
 noSourcesText=This page has no sources.
 
@@ -483,36 +490,32 @@ editor.disableBreakpoint.accesskey=D
 # LOCALIZATION NOTE (editor.enableBreakpoint): Editor gutter context menu item
 # for enabling a breakpoint on a line.
 editor.enableBreakpoint=Enable breakpoint
 
 # LOCALIZATION NOTE (editor.removeBreakpoint): Editor gutter context menu item
 # for removing a breakpoint on a line.
 editor.removeBreakpoint=Remove breakpoint
 
-# LOCALIZATION NOTE (editor.editBreakpoint): Editor gutter context menu item
-# for setting a breakpoint condition on a line.
-editor.editBreakpoint=Edit breakpoint
-
-# LOCALIZATION NOTE (editor.addConditionalBreakpoint): Editor gutter context
+# LOCALIZATION NOTE (editor.addConditionBreakpoint): Editor gutter context
 # menu item for adding a breakpoint condition on a line.
-editor.addConditionalBreakpoint=Add condition
-editor.addConditionalBreakpoint.accesskey=c
+editor.addConditionBreakpoint=Add condition
+editor.addConditionBreakpoint.accesskey=c
 
-# LOCALIZATION NOTE (editor.editBreakpoint): Editor gutter context menu item
+# LOCALIZATION NOTE (editor.editConditionBreakpoint): Editor gutter context menu item
 # for setting a breakpoint condition on a line.
-editor.editConditionalBreakpoint=Edit condition
+editor.editConditionBreakpoint=Edit condition
 
 # LOCALIZATION NOTE (editor.addLogPoint): Editor gutter context
 # menu item for adding a log point on a line.
 editor.addLogPoint=Add log
 editor.addLogPoint.accesskey=l
 
-# LOCALIZATION NOTE (editor.editLogpoint): Editor gutter context menu item
-# for setting a log point on a line.
+# LOCALIZATION NOTE (editor.editLogPoint): Editor gutter context menu item
+# for editing a log point already set on a line.
 editor.editLogPoint=Edit log
 
 # LOCALIZATION NOTE (editor.conditionalPanel.placeholder): Placeholder text for
 # input element inside ConditionalPanel component
 editor.conditionalPanel.placeholder=This breakpoint will pause when the expression is true
 
 # LOCALIZATION NOTE (editor.conditionalPanel.logPoint.placeholder): Placeholder text for
 # input element inside ConditionalPanel component when a log point is set
--- a/dom/storage/LocalStorage.cpp
+++ b/dom/storage/LocalStorage.cpp
@@ -190,20 +190,16 @@ void LocalStorage::ApplyEvent(StorageEve
     mCache->RemoveItem(this, key, old, LocalStorageCache::E10sPropagated);
     return;
   }
 
   // Otherwise, we set the new value.
   mCache->SetItem(this, key, value, old, LocalStorageCache::E10sPropagated);
 }
 
-bool LocalStorage::PrincipalEquals(nsIPrincipal* aPrincipal) {
-  return StorageUtils::PrincipalsEqual(mPrincipal, aPrincipal);
-}
-
 void LocalStorage::GetSupportedNames(nsTArray<nsString>& aKeys) {
   if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) {
     // return just an empty array
     aKeys.Clear();
     return;
   }
 
   mCache->GetKeys(this, aKeys);
--- a/dom/storage/LocalStorage.h
+++ b/dom/storage/LocalStorage.h
@@ -25,18 +25,16 @@ class LocalStorage final : public Storag
   StorageType Type() const override { return eLocalStorage; }
 
   LocalStorageManager* GetManager() const { return mManager; }
 
   LocalStorageCache const* GetCache() const { return mCache; }
 
   const nsString& DocumentURI() const { return mDocumentURI; }
 
-  bool PrincipalEquals(nsIPrincipal* aPrincipal);
-
   LocalStorage(nsPIDOMWindowInner* aWindow, LocalStorageManager* aManager,
                LocalStorageCache* aCache, const nsAString& aDocumentURI,
                nsIPrincipal* aPrincipal, bool aIsPrivate);
 
   bool IsForkOf(const Storage* aOther) const override;
 
   // WebIDL
 
@@ -70,20 +68,16 @@ class LocalStorage final : public Storag
 
   friend class LocalStorageManager;
   friend class LocalStorageCache;
 
   RefPtr<LocalStorageManager> mManager;
   RefPtr<LocalStorageCache> mCache;
   nsString mDocumentURI;
 
-  // Principal this Storage (i.e. localStorage or sessionStorage) has
-  // been created for
-  nsCOMPtr<nsIPrincipal> mPrincipal;
-
   // Whether this storage is running in private-browsing window.
   bool mIsPrivate : 1;
 
   void OnChange(const nsAString& aKey, const nsAString& aOldValue,
                 const nsAString& aNewValue);
 };
 
 }  // namespace dom
--- a/dom/storage/LocalStorageManager.cpp
+++ b/dom/storage/LocalStorageManager.cpp
@@ -306,46 +306,23 @@ NS_IMETHODIMP
 LocalStorageManager::CloneStorage(Storage* aStorage) {
   // Cloning is supported only for sessionStorage
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
 LocalStorageManager::CheckStorage(nsIPrincipal* aPrincipal, Storage* aStorage,
                                   bool* aRetval) {
-  if (!aStorage || aStorage->Type() != Storage::eLocalStorage) {
-    return NS_ERROR_UNEXPECTED;
-  }
-
-  RefPtr<LocalStorage> storage = static_cast<LocalStorage*>(aStorage);
-
-  *aRetval = false;
-
-  if (!aPrincipal) {
-    return NS_ERROR_NOT_AVAILABLE;
-  }
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aPrincipal);
+  MOZ_ASSERT(aStorage);
+  MOZ_ASSERT(aRetval);
 
-  nsAutoCString suffix;
-  nsAutoCString origin;
-  nsresult rv = GenerateOriginKey(aPrincipal, suffix, origin);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
-  }
-
-  LocalStorageCache* cache = GetCache(suffix, origin);
-  if (cache != storage->GetCache()) {
-    return NS_OK;
-  }
-
-  if (!storage->PrincipalEquals(aPrincipal)) {
-    return NS_OK;
-  }
-
-  *aRetval = true;
-  return NS_OK;
+  // Only used by sessionStorage.
+  return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
 LocalStorageManager::GetNextGenLocalStorageEnabled(bool* aResult) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aResult);
 
   *aResult = NextGenLocalStorageEnabled();
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -301,16 +301,17 @@ if (!isDummyDocument) {
     "chrome://global/content/elements/tabbox.js",
     "chrome://global/content/elements/tree.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
   }
 
   for (let [tag, script] of [
     ["findbar", "chrome://global/content/elements/findbar.js"],
+    ["richlistbox", "chrome://global/content/elements/richlistbox.js"],
     ["stringbundle", "chrome://global/content/elements/stringbundle.js"],
     ["printpreview-toolbar", "chrome://global/content/printPreviewToolbar.js"],
     ["editor", "chrome://global/content/elements/editor.js"],
   ]) {
     customElements.setElementCreationCallback(tag, () => {
       Services.scriptloader.loadSubScript(script, window);
     });
   }
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -93,16 +93,17 @@ toolkit.jar:
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js          (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/notificationbox.js  (widgets/notificationbox.js)
    content/global/elements/pluginProblem.js    (widgets/pluginProblem.js)
    content/global/elements/radio.js            (widgets/radio.js)
+   content/global/elements/richlistbox.js      (widgets/richlistbox.js)
    content/global/elements/marquee.css         (widgets/marquee.css)
    content/global/elements/marquee.js          (widgets/marquee.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/tabbox.js           (widgets/tabbox.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
    content/global/elements/videocontrols.js    (widgets/videocontrols.js)
    content/global/elements/tree.js             (widgets/tree.js)
 #ifdef XP_MACOSX
copy from toolkit/content/widgets/richlistbox.xml
copy to toolkit/content/widgets/richlistbox.js
--- a/toolkit/content/widgets/richlistbox.xml
+++ b/toolkit/content/widgets/richlistbox.js
@@ -1,1097 +1,830 @@
-<?xml version="1.0"?>
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<!-- This file relies on these specific Chrome/XBL globals -->
-<!-- globals ChromeNodeList -->
+"use strict";
 
-<bindings id="richlistboxBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
 
-  <binding id="richlistbox"
-           extends="chrome://global/content/bindings/general.xml#basecontrol">
-    <content allowevents="true" orient="vertical"/>
+class MozRichListBox extends MozElements.BaseControl {
+  constructor() {
+    super();
 
-    <implementation implements="nsIDOMXULMultiSelectControlElement">
-      <constructor>
-        <![CDATA[
-          this._refreshSelection();
-        ]]>
-      </constructor>
+    this.addEventListener("keypress", event => {
+      if (event.altKey || event.metaKey) {
+        return;
+      }
 
-      <method name="_fireOnSelect">
-        <body>
-          <![CDATA[
-            // make sure not to modify last-selected when suppressing select events
-            // (otherwise we'll lose the selection when a template gets rebuilt)
-            if (this._suppressOnSelect || this.suppressOnSelect)
-              return;
+      switch (event.keyCode) {
+        case KeyEvent.DOM_VK_UP:
+          this._moveByOffsetFromUserEvent(-1, event);
+          break;
+        case KeyEvent.DOM_VK_DOWN:
+          this._moveByOffsetFromUserEvent(1, event);
+          break;
+        case KeyEvent.DOM_VK_HOME:
+          this._moveByOffsetFromUserEvent(-this.currentIndex, event);
+          break;
+        case KeyEvent.DOM_VK_END:
+          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
+          break;
+        case KeyEvent.DOM_VK_PAGE_UP:
+          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
+          break;
+        case KeyEvent.DOM_VK_PAGE_DOWN:
+          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
+          break;
+      }
+    }, { mozSystemGroup: true });
 
-            // remember the current item and all selected items with IDs
-            var state = this.currentItem ? this.currentItem.id : "";
-            if (this.selType == "multiple" && this.selectedCount) {
-              let getId = function getId(aItem) { return aItem.id; };
-              state += " " + [...this.selectedItems].filter(getId).map(getId).join(" ");
-            }
-            if (state)
-              this.setAttribute("last-selected", state);
-            else
-              this.removeAttribute("last-selected");
-
-            // preserve the index just in case no IDs are available
-            if (this.currentIndex > -1)
-              this._currentIndex = this.currentIndex + 1;
-
-            var event = document.createEvent("Events");
-            event.initEvent("select", true, true);
-            this.dispatchEvent(event);
-
-            // always call this (allows a commandupdater without controller)
-            document.commandDispatcher.updateCommands("richlistbox-select");
-          ]]>
-        </body>
-      </method>
+    this.addEventListener("keypress", event => {
+      if (event.target != this) {
+        return;
+      }
 
-      <method name="getNextItem">
-        <parameter name="aStartItem"/>
-        <parameter name="aDelta"/>
-        <body>
-        <![CDATA[
-          while (aStartItem) {
-            aStartItem = aStartItem.nextSibling;
-            if (aStartItem && aStartItem.localName == "richlistitem" &&
-                (!this._userSelecting || this._canUserSelect(aStartItem))) {
-              --aDelta;
-              if (aDelta == 0)
-                return aStartItem;
-            }
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
+      if (event.key == " " &&
+          event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey &&
+          this.currentItem && this.selType == "multiple") {
+        this.toggleItemSelection(this.currentItem);
+      }
+
+      if (!event.charCode || event.altKey || event.ctrlKey || event.metaKey) {
+        return;
+      }
+
+      if (event.timeStamp - this._lastKeyTime > 1000) {
+        this._incrementalString = "";
+      }
+
+      var key = String.fromCharCode(event.charCode).toLowerCase();
+      this._incrementalString += key;
+      this._lastKeyTime = event.timeStamp;
+
+      // If all letters in the incremental string are the same, just
+      // try to match the first one
+      var incrementalString = /^(.)\1+$/.test(this._incrementalString) ?
+        RegExp.$1 : this._incrementalString;
+      var length = incrementalString.length;
 
-      <method name="getPreviousItem">
-        <parameter name="aStartItem"/>
-        <parameter name="aDelta"/>
-        <body>
-        <![CDATA[
-          while (aStartItem) {
-            aStartItem = aStartItem.previousSibling;
-            if (aStartItem && aStartItem.localName == "richlistitem" &&
-                (!this._userSelecting || this._canUserSelect(aStartItem))) {
-              --aDelta;
-              if (aDelta == 0)
-                return aStartItem;
-            }
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
+      var rowCount = this.getRowCount();
+      var l = this.selectedItems.length;
+      var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
+      // start from the first element if none was selected or from the one
+      // following the selected one if it's a new or a repeated-letter search
+      if (start == -1 || length == 1) {
+        start++;
+      }
 
-      <method name="appendItem">
-        <parameter name="aLabel"/>
-        <parameter name="aValue"/>
-        <body>
-          var item =
-            this.ownerDocument.createXULElement("richlistitem");
-          item.setAttribute("value", aValue);
-
-          var label = this.ownerDocument.createXULElement("label");
-          label.setAttribute("value", aLabel);
-          label.setAttribute("flex", "1");
-          label.setAttribute("crop", "end");
-          item.appendChild(label);
-
-          this.appendChild(item);
-
-          return item;
-        </body>
-      </method>
-
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="selectedItem"
-                onset="this.selectItem(val);">
-        <getter>
-        <![CDATA[
-          return this.selectedItems.length > 0 ? this.selectedItems[0] : null;
-        ]]>
-        </getter>
-      </property>
+      for (var i = 0; i < rowCount; i++) {
+        var k = (start + i) % rowCount;
+        var listitem = this.getItemAtIndex(k);
+        if (!this._canUserSelect(listitem)) {
+          continue;
+        }
+        // allow richlistitems to specify the string being searched for
+        var searchText = "searchLabel" in listitem ? listitem.searchLabel :
+          listitem.getAttribute("label"); // (see also bug 250123)
+        searchText = searchText.substring(0, length).toLowerCase();
+        if (searchText == incrementalString) {
+          this.ensureIndexIsVisible(k);
+          this.timedSelect(listitem, this._selectDelay);
+          break;
+        }
+      }
+    });
 
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="selectedIndex">
-        <getter>
-        <![CDATA[
-          if (this.selectedItems.length > 0)
-            return this.getIndexOfItem(this.selectedItems[0]);
-          return -1;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val >= 0) {
-            // This is a micro-optimization so that a call to getIndexOfItem or
-            // getItemAtIndex caused by _fireOnSelect (especially for derived
-            // widgets) won't loop the children.
-            this._selecting = {
-              item: this.getItemAtIndex(val),
-              index: val,
-            };
-            this.selectItem(this._selecting.item);
-            delete this._selecting;
-          } else {
-            this.clearSelection();
-            this.currentItem = null;
+    this.addEventListener("focus", event => {
+      if (this.getRowCount() > 0) {
+        if (this.currentIndex == -1) {
+          this.currentIndex = this.getIndexOfFirstVisibleRow();
+          let currentItem = this.getItemAtIndex(this.currentIndex);
+          if (currentItem) {
+            this.selectItem(currentItem);
           }
-        ]]>
-        </setter>
-      </property>
+        } else {
+          this.currentItem._fireEvent("DOMMenuItemActive");
+        }
+      }
+      this._lastKeyTime = 0;
+    });
+
+    this.addEventListener("click", event => {
+      // clicking into nothing should unselect
+      if (event.originalTarget == this) {
+        this.clearSelection();
+        this.currentItem = null;
+      }
+    });
 
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="value">
-        <getter>
-        <![CDATA[
-          if (this.selectedItems.length > 0)
-            return this.selectedItem.value;
-          return null;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          var kids = this.getElementsByAttribute("value", val);
-          if (kids && kids.item(0))
-            this.selectItem(kids[0]);
-          return val;
-        ]]>
-        </setter>
-      </property>
+    this.addEventListener("MozSwipeGesture", event => {
+      // Only handle swipe gestures up and down
+      switch (event.direction) {
+        case event.DIRECTION_DOWN:
+          this.scrollTop = this.scrollHeight;
+          break;
+        case event.DIRECTION_UP:
+          this.scrollTop = 0;
+          break;
+      }
+    });
+  }
 
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="itemCount" readonly="true"
-                onget="return this.itemChildren.length"/>
+  connectedCallback() {
+    if (this.delayConnectedCallback()) {
+      return;
+    }
+
+    this.setAttribute("allowevents", "true");
 
-      <!-- nsIDOMXULSelectControlElement -->
-      <method name="getIndexOfItem">
-        <parameter name="aItem"/>
-        <body>
-          <![CDATA[
-            // don't search the children, if we're looking for none of them
-            if (aItem == null)
-              return -1;
-            if (this._selecting && this._selecting.item == aItem)
-              return this._selecting.index;
-            return this.itemChildren.indexOf(aItem);
-          ]]>
-        </body>
-      </method>
+    this.selectedItems = new ChromeNodeList();
+    this._currentIndex = null;
+    this._lastKeyTime = 0;
+    this._incrementalString = "";
+    this._suppressOnSelect = false;
+    this._userSelecting = false;
+    this._selectTimeout = null;
+    this._currentItem = null;
+    this._selectionStart = null;
 
-      <!-- nsIDOMXULSelectControlElement -->
-      <method name="getItemAtIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            if (this._selecting && this._selecting.index == aIndex)
-              return this._selecting.item;
-            return this.itemChildren[aIndex] || null;
-          ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="selType"
-                onget="return this.getAttribute('seltype');"
-                onset="this.setAttribute('seltype', val); return val;"/>
+    this._refreshSelection();
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="currentItem" onget="return this._currentItem;">
-        <setter>
-          if (this._currentItem == val)
-            return val;
-
-          if (this._currentItem)
-            this._currentItem.current = false;
-          this._currentItem = val;
-
-          if (val)
-            val.current = true;
-
-          return val;
-        </setter>
-      </property>
+  // nsIDOMXULSelectControlElement
+  set selectedItem(val) {
+    this.selectItem(val);
+  }
+  get selectedItem() {
+    return this.selectedItems.length > 0 ? this.selectedItems[0] : null;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="currentIndex">
-        <getter>
-          return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val >= 0)
-            this.currentItem = this.getItemAtIndex(val);
-          else
-            this.currentItem = null;
-        ]]>
-        </setter>
-      </property>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <field name="selectedItems">new ChromeNodeList()</field>
+  // nsIDOMXULSelectControlElement
+  set selectedIndex(val) {
+    if (val >= 0) {
+      // This is a micro-optimization so that a call to getIndexOfItem or
+      // getItemAtIndex caused by _fireOnSelect (especially for derived
+      // widgets) won't loop the children.
+      this._selecting = {
+        item: this.getItemAtIndex(val),
+        index: val,
+      };
+      this.selectItem(this._selecting.item);
+      delete this._selecting;
+    } else {
+      this.clearSelection();
+      this.currentItem = null;
+    }
+  }
+  get selectedIndex() {
+    if (this.selectedItems.length > 0) {
+      return this.getIndexOfItem(this.selectedItems[0]);
+    }
+    return -1;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="addItemToSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (this.selType != "multiple" && this.selectedCount)
-            return;
+  // nsIDOMXULSelectControlElement
+  set value(val) {
+    var kids = this.getElementsByAttribute("value", val);
+    if (kids && kids.item(0)) {
+      this.selectItem(kids[0]);
+    }
+    return val;
+  }
+  get value() {
+    if (this.selectedItems.length > 0) {
+      return this.selectedItem.value;
+    }
+    return null;
+  }
 
-          if (aItem.selected)
-            return;
-
-          this.selectedItems.append(aItem);
-          aItem.selected = true;
-
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  get itemCount() {
+    return this.itemChildren.length;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="removeItemFromSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (!aItem.selected)
-            return;
+  // nsIDOMXULSelectControlElement
+  set selType(val) {
+    this.setAttribute("seltype", val);
+    return val;
+  }
+  get selType() {
+    return this.getAttribute("seltype");
+  }
 
-          this.selectedItems.remove(aItem);
-          aItem.selected = false;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  set currentItem(val) {
+    if (this._currentItem == val) {
+      return val;
+    }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="toggleItemSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (aItem.selected)
-            this.removeItemFromSelection(aItem);
-          else
-            this.addItemToSelection(aItem);
-        ]]>
-        </body>
-      </method>
+    if (this._currentItem) {
+      this._currentItem.current = false;
+    }
+    this._currentItem = val;
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="selectItem">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (!aItem)
-            return;
+    if (val) {
+      val.current = true;
+    }
 
-          if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem)
-            return;
-
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
+    return val;
+  }
+  get currentItem() {
+    return this._currentItem;
+  }
 
-          this.clearSelection();
-          this.addItemToSelection(aItem);
-          this.currentItem = aItem;
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  set currentIndex(val) {
+    if (val >= 0) {
+      this.currentItem = this.getItemAtIndex(val);
+    } else {
+      this.currentItem = null;
+    }
+  }
+  get currentIndex() {
+    return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="selectItemRange">
-        <parameter name="aStartItem"/>
-        <parameter name="aEndItem"/>
-        <body>
-        <![CDATA[
-          if (this.selType != "multiple")
-            return;
-
-          if (!aStartItem)
-            aStartItem = this._selectionStart ?
-              this._selectionStart : this.currentItem;
+  // nsIDOMXULSelectControlElement
+  get selectedCount() {
+    return this.selectedItems.length;
+  }
 
-          if (!aStartItem)
-            aStartItem = aEndItem;
-
-          var suppressSelect = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          this._selectionStart = aStartItem;
+  get itemChildren() {
+    let children = Array.from(this.children)
+      .filter(node => node.localName == "richlistitem");
+    return children;
+  }
 
-          var currentItem;
-          var startIndex = this.getIndexOfItem(aStartItem);
-          var endIndex = this.getIndexOfItem(aEndItem);
-          if (endIndex < startIndex) {
-            currentItem = aEndItem;
-            aEndItem = aStartItem;
-            aStartItem = currentItem;
-          } else {
-            currentItem = aStartItem;
-          }
+  set suppressOnSelect(val) {
+    this.setAttribute("suppressonselect", val);
+  }
+  get suppressOnSelect() {
+    return this.getAttribute("suppressonselect") == "true";
+  }
 
-          while (currentItem) {
-            this.addItemToSelection(currentItem);
-            if (currentItem == aEndItem) {
-              currentItem = this.getNextItem(currentItem, 1);
-              break;
-            }
-            currentItem = this.getNextItem(currentItem, 1);
-          }
+  set _selectDelay(val) {
+    this.setAttribute("_selectDelay", val);
+  }
+  get _selectDelay() {
+    return this.getAttribute("_selectDelay") || 50;
+  }
 
-          // Clear around new selection
-          // Don't use clearSelection() because it causes a lot of noise
-          // with respect to selection removed notifications used by the
-          // accessibility API support.
-          var userSelecting = this._userSelecting;
-          this._userSelecting = false; // that's US automatically unselecting
-          for (; currentItem; currentItem = this.getNextItem(currentItem, 1))
-            this.removeItemFromSelection(currentItem);
-
-          for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem;
-               currentItem = this.getNextItem(currentItem, 1))
-            this.removeItemFromSelection(currentItem);
-          this._userSelecting = userSelecting;
-
-          this._suppressOnSelect = suppressSelect;
+  _fireOnSelect() {
+    // make sure not to modify last-selected when suppressing select events
+    // (otherwise we'll lose the selection when a template gets rebuilt)
+    if (this._suppressOnSelect || this.suppressOnSelect) {
+      return;
+    }
 
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="selectAll">
-        <body>
-          this._selectionStart = null;
+    // remember the current item and all selected items with IDs
+    var state = this.currentItem ? this.currentItem.id : "";
+    if (this.selType == "multiple" && this.selectedCount) {
+      let getId = function getId(aItem) { return aItem.id; };
+      state += " " + [...this.selectedItems].filter(getId).map(getId).join(" ");
+    }
+    if (state) {
+      this.setAttribute("last-selected", state);
+    } else {
+      this.removeAttribute("last-selected");
+    }
 
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
+    // preserve the index just in case no IDs are available
+    if (this.currentIndex > -1) {
+      this._currentIndex = this.currentIndex + 1;
+    }
 
-          var item = this.getItemAtIndex(0);
-          while (item) {
-            this.addItemToSelection(item);
-            item = this.getNextItem(item, 1);
-          }
+    var event = document.createEvent("Events");
+    event.initEvent("select", true, true);
+    this.dispatchEvent(event);
 
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        </body>
-      </method>
+    // always call this (allows a commandupdater without controller)
+    document.commandDispatcher.updateCommands("richlistbox-select");
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="invertSelection">
-        <body>
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          var item = this.getItemAtIndex(0);
-          while (item) {
-            if (item.selected)
-              this.removeItemFromSelection(item);
-            else
-              this.addItemToSelection(item);
-            item = this.getNextItem(item, 1);
-          }
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        </body>
-      </method>
+  getNextItem(aStartItem, aDelta) {
+    while (aStartItem) {
+      aStartItem = aStartItem.nextSibling;
+      if (aStartItem && aStartItem.localName == "richlistitem" &&
+        (!this._userSelecting || this._canUserSelect(aStartItem))) {
+        --aDelta;
+        if (aDelta == 0) {
+          return aStartItem;
+        }
+      }
+    }
+    return null;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="clearSelection">
-        <body>
-        <![CDATA[
-          if (this.selectedItems) {
-            while (this.selectedItems.length > 0) {
-              let item = this.selectedItems[0];
-              item.selected = false;
-              this.selectedItems.remove(item);
-            }
-          }
-
-          this._selectionStart = null;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
+  getPreviousItem(aStartItem, aDelta) {
+    while (aStartItem) {
+      aStartItem = aStartItem.previousSibling;
+      if (aStartItem && aStartItem.localName == "richlistitem" &&
+        (!this._userSelecting || this._canUserSelect(aStartItem))) {
+        --aDelta;
+        if (aDelta == 0) {
+          return aStartItem;
+        }
+      }
+    }
+    return null;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="selectedCount" readonly="true"
-                onget="return this.selectedItems.length;"/>
+  appendItem(aLabel, aValue) {
+    var item =
+      this.ownerDocument.createXULElement("richlistitem");
+    item.setAttribute("value", aValue);
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="getSelectedItem">
-        <parameter name="aIndex"/>
-        <body>
-        <![CDATA[
-          return aIndex < this.selectedItems.length ?
-            this.selectedItems[aIndex] : null;
-        ]]>
-        </body>
-      </method>
+    var label = this.ownerDocument.createXULElement("label");
+    label.setAttribute("value", aLabel);
+    label.setAttribute("flex", "1");
+    label.setAttribute("crop", "end");
+    item.appendChild(label);
 
-      <method name="ensureIndexIsVisible">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
-          ]]>
-        </body>
-      </method>
+    this.appendChild(item);
+
+    return item;
+  }
 
-      <method name="ensureElementIsVisible">
-        <parameter name="aElement"/>
-        <parameter name="aAlignToTop"/>
-        <body>
-          <![CDATA[
-            if (!aElement) {
-              return;
-            }
+  // nsIDOMXULSelectControlElement
+  getIndexOfItem(aItem) {
+    // don't search the children, if we're looking for none of them
+    if (aItem == null) {
+      return -1;
+    }
+    if (this._selecting && this._selecting.item == aItem) {
+      return this._selecting.index;
+    }
+    return this.itemChildren.indexOf(aItem);
+  }
 
-            // These calculations assume that there is no padding on the
-            // "richlistbox" element, although there might be a margin.
-            var targetRect = aElement.getBoundingClientRect();
-            var scrollRect = this.getBoundingClientRect();
-            var offset = targetRect.top - scrollRect.top;
-            if (!aAlignToTop && offset >= 0) {
-              // scrollRect.bottom wouldn't take a horizontal scroll bar into account
-              let scrollRectBottom = scrollRect.top + this.clientHeight;
-              offset = targetRect.bottom - scrollRectBottom;
-              if (offset <= 0)
-                return;
-            }
-            this.scrollTop += offset;
-          ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  getItemAtIndex(aIndex) {
+    if (this._selecting && this._selecting.index == aIndex) {
+      return this._selecting.item;
+    }
+    return this.itemChildren[aIndex] || null;
+  }
 
-      <method name="scrollToIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            var item = this.getItemAtIndex(aIndex);
-            if (item) {
-              this.ensureElementIsVisible(item, true);
-            }
-          ]]>
-        </body>
-      </method>
+  // nsIDOMXULMultiSelectControlElement
+  addItemToSelection(aItem) {
+    if (this.selType != "multiple" && this.selectedCount) {
+      return;
+    }
+
+    if (aItem.selected) {
+      return;
+    }
+
+    this.selectedItems.append(aItem);
+    aItem.selected = true;
 
-      <method name="getIndexOfFirstVisibleRow">
-        <body>
-          <![CDATA[
-            var children = this.itemChildren;
+    this._fireOnSelect();
+  }
 
-            for (var ix = 0; ix < children.length; ix++)
-              if (this._isItemVisible(children[ix]))
-                return ix;
+  // nsIDOMXULMultiSelectControlElement
+  removeItemFromSelection(aItem) {
+    if (!aItem.selected) {
+      return;
+    }
 
-            return -1;
-          ]]>
-        </body>
-      </method>
+    this.selectedItems.remove(aItem);
+    aItem.selected = false;
+    this._fireOnSelect();
+  }
 
-      <method name="getRowCount">
-        <body>
-          <![CDATA[
-            return this.itemChildren.length;
-          ]]>
-        </body>
-      </method>
-
-      <method name="scrollOnePage">
-        <parameter name="aDirection"/> <!-- Must be -1 or 1 -->
-        <body>
-          <![CDATA[
-            var children = this.itemChildren;
+  // nsIDOMXULMultiSelectControlElement
+  toggleItemSelection(aItem) {
+    if (aItem.selected) {
+      this.removeItemFromSelection(aItem);
+    } else {
+      this.addItemToSelection(aItem);
+    }
+  }
 
-            if (children.length == 0)
-              return 0;
+  // nsIDOMXULMultiSelectControlElement
+  selectItem(aItem) {
+    if (!aItem) {
+      return;
+    }
 
-            // If nothing is selected, we just select the first element
-            // at the extreme we're moving away from
-            if (!this.currentItem)
-              return aDirection == -1 ? children.length : 0;
-
-            // If the current item is visible, scroll by one page so that
-            // the new current item is at approximately the same position as
-            // the existing current item.
-            if (this._isItemVisible(this.currentItem))
-              this.scrollBy(0, this.clientHeight * aDirection);
+    if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem) {
+      return;
+    }
 
-            // Figure out, how many items fully fit into the view port
-            // (including the currently selected one), and determine
-            // the index of the first one lying (partially) outside
-            var height = this.clientHeight;
-            var startBorder = this.currentItem.boxObject.y;
-            if (aDirection == -1)
-              startBorder += this.currentItem.clientHeight;
+    this._selectionStart = null;
+
+    var suppress = this._suppressOnSelect;
+    this._suppressOnSelect = true;
+
+    this.clearSelection();
+    this.addItemToSelection(aItem);
+    this.currentItem = aItem;
 
-            var index = this.currentIndex;
-            for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
-              var boxObject = children[ix].boxObject;
-              if (boxObject.height == 0)
-                continue; // hidden children have a y of 0
-              var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0);
-              if ((endBorder - startBorder) * aDirection > height)
-                break; // we've reached the desired distance
-              index = ix;
-            }
+    this._suppressOnSelect = suppress;
+    this._fireOnSelect();
+  }
 
-            return index != this.currentIndex ? index - this.currentIndex : aDirection;
-          ]]>
-        </body>
-      </method>
+  // nsIDOMXULMultiSelectControlElement
+  selectItemRange(aStartItem, aEndItem) {
+    if (this.selType != "multiple") {
+      return;
+    }
 
-      <property name="itemChildren" readonly="true">
-        <getter>
-          <![CDATA[
-            let children = Array.from(this.children)
-                                .filter(node => node.localName == "richlistitem");
-            return children;
-          ]]>
-        </getter>
-      </property>
+    if (!aStartItem) {
+      aStartItem = this._selectionStart ? this._selectionStart
+                                        : this.currentItem;
+    }
 
-      <method name="_refreshSelection">
-        <body>
-          <![CDATA[
-            // when this method is called, we know that either the currentItem
-            // and selectedItems we have are null (ctor) or a reference to an
-            // element no longer in the DOM (template).
+    if (!aStartItem) {
+      aStartItem = aEndItem;
+    }
+
+    var suppressSelect = this._suppressOnSelect;
+    this._suppressOnSelect = true;
 
-            // first look for the last-selected attribute
-            var state = this.getAttribute("last-selected");
-            if (state) {
-              var ids = state.split(" ");
+    this._selectionStart = aStartItem;
 
-              var suppressSelect = this._suppressOnSelect;
-              this._suppressOnSelect = true;
-              this.clearSelection();
-              for (let i = 1; i < ids.length; i++) {
-                var selectedItem = document.getElementById(ids[i]);
-                if (selectedItem)
-                  this.addItemToSelection(selectedItem);
-              }
-
-              var currentItem = document.getElementById(ids[0]);
-              if (!currentItem && this._currentIndex)
-                currentItem = this.getItemAtIndex(Math.min(
-                  this._currentIndex - 1, this.getRowCount()));
-              if (currentItem) {
-                this.currentItem = currentItem;
-                if (this.selType != "multiple" && this.selectedCount == 0)
-                  this.selectedItem = currentItem;
+    var currentItem;
+    var startIndex = this.getIndexOfItem(aStartItem);
+    var endIndex = this.getIndexOfItem(aEndItem);
+    if (endIndex < startIndex) {
+      currentItem = aEndItem;
+      aEndItem = aStartItem;
+      aStartItem = currentItem;
+    } else {
+      currentItem = aStartItem;
+    }
 
-                if (this.clientHeight) {
-                  this.ensureElementIsVisible(currentItem);
-                } else {
-                  // XXX hack around a bug in ensureElementIsVisible as it will
-                  // scroll beyond the last element, bug 493645.
-                  this.ensureElementIsVisible(currentItem.previousElementSibling);
-                }
-              }
-              this._suppressOnSelect = suppressSelect;
-              // XXX actually it's just a refresh, but at least
-              // the Extensions manager expects this:
-              this._fireOnSelect();
-              return;
-            }
+    while (currentItem) {
+      this.addItemToSelection(currentItem);
+      if (currentItem == aEndItem) {
+        currentItem = this.getNextItem(currentItem, 1);
+        break;
+      }
+      currentItem = this.getNextItem(currentItem, 1);
+    }
 
-            // try to restore the selected items according to their IDs
-            // (applies after a template rebuild, if last-selected was not set)
-            if (this.selectedItems) {
-              let itemIds = [];
-              for (let i = this.selectedCount - 1; i >= 0; i--) {
-                let selectedItem = this.selectedItems[i];
-                itemIds.push(selectedItem.id);
-                this.selectedItems.remove(selectedItem);
-              }
-              for (let i = 0; i < itemIds.length; i++) {
-                let selectedItem = document.getElementById(itemIds[i]);
-                if (selectedItem) {
-                  this.selectedItems.append(selectedItem);
-                }
-              }
-            }
-            if (this.currentItem && this.currentItem.id)
-              this.currentItem = document.getElementById(this.currentItem.id);
-            else
-              this.currentItem = null;
+    // Clear around new selection
+    // Don't use clearSelection() because it causes a lot of noise
+    // with respect to selection removed notifications used by the
+    // accessibility API support.
+    var userSelecting = this._userSelecting;
+    this._userSelecting = false; // that's US automatically unselecting
+    for (; currentItem; currentItem = this.getNextItem(currentItem, 1)) {
+      this.removeItemFromSelection(currentItem);
+    }
+
+    for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem;
+         currentItem = this.getNextItem(currentItem, 1)) {
+      this.removeItemFromSelection(currentItem);
+    }
+    this._userSelecting = userSelecting;
+
+    this._suppressOnSelect = suppressSelect;
 
-            // if we have no previously current item or if the above check fails to
-            // find the previous nodes (which causes it to clear selection)
-            if (!this.currentItem && this.selectedCount == 0) {
-              this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0;
+    this._fireOnSelect();
+  }
 
-              // cf. listbox constructor:
-              // select items according to their attributes
-              var children = this.itemChildren;
-              for (let i = 0; i < children.length; ++i) {
-                if (children[i].getAttribute("selected") == "true")
-                  this.selectedItems.append(children[i]);
-              }
-            }
+  // nsIDOMXULMultiSelectControlElement
+  selectAll() {
+    this._selectionStart = null;
+
+    var suppress = this._suppressOnSelect;
+    this._suppressOnSelect = true;
 
-            if (this.selType != "multiple" && this.selectedCount == 0)
-              this.selectedItem = this.currentItem;
-          ]]>
-        </body>
-      </method>
+    var item = this.getItemAtIndex(0);
+    while (item) {
+      this.addItemToSelection(item);
+      item = this.getNextItem(item, 1);
+    }
 
-      <method name="_isItemVisible">
-        <parameter name="aItem"/>
-        <body>
-          <![CDATA[
-            if (!aItem)
-              return false;
+    this._suppressOnSelect = suppress;
+    this._fireOnSelect();
+  }
 
-            var y = this.scrollTop + this.boxObject.y;
+  // nsIDOMXULMultiSelectControlElement
+  invertSelection() {
+    this._selectionStart = null;
 
-            // Partially visible items are also considered visible
-            return (aItem.boxObject.y + aItem.clientHeight > y) &&
-                   (aItem.boxObject.y < y + this.clientHeight);
-          ]]>
-        </body>
-      </method>
+    var suppress = this._suppressOnSelect;
+    this._suppressOnSelect = true;
 
-      <property name="suppressOnSelect"
-                onget="return this.getAttribute('suppressonselect') == 'true';"
-                onset="this.setAttribute('suppressonselect', val);"/>
-
-      <property name="_selectDelay"
-                onset="this.setAttribute('_selectDelay', val);"
-                onget="return this.getAttribute('_selectDelay') || 50;"/>
+    var item = this.getItemAtIndex(0);
+    while (item) {
+      if (item.selected) {
+        this.removeItemFromSelection(item);
+      } else {
+        this.addItemToSelection(item);
+      }
+      item = this.getNextItem(item, 1);
+    }
 
-      <method name="moveByOffset">
-        <parameter name="aOffset"/>
-        <parameter name="aIsSelecting"/>
-        <parameter name="aIsSelectingRange"/>
-        <body>
-        <![CDATA[
-          if ((aIsSelectingRange || !aIsSelecting) &&
-              this.selType != "multiple")
-            return;
-
-          var newIndex = this.currentIndex + aOffset;
-          if (newIndex < 0)
-            newIndex = 0;
-
-          var numItems = this.getRowCount();
-          if (newIndex > numItems - 1)
-            newIndex = numItems - 1;
+    this._suppressOnSelect = suppress;
+    this._fireOnSelect();
+  }
 
-          var newItem = this.getItemAtIndex(newIndex);
-          // make sure that the item is actually visible/selectable
-          if (this._userSelecting && newItem && !this._canUserSelect(newItem))
-            newItem =
-              aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) :
-                            this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
-          if (newItem) {
-            this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
-            if (aIsSelectingRange)
-              this.selectItemRange(null, newItem);
-            else if (aIsSelecting)
-              this.selectItem(newItem);
+  // nsIDOMXULMultiSelectControlElement
+  clearSelection() {
+    if (this.selectedItems) {
+      while (this.selectedItems.length > 0) {
+        let item = this.selectedItems[0];
+        item.selected = false;
+        this.selectedItems.remove(item);
+      }
+    }
+
+    this._selectionStart = null;
+    this._fireOnSelect();
+  }
 
-            this.currentItem = newItem;
-          }
-        ]]>
-        </body>
-      </method>
+  // nsIDOMXULMultiSelectControlElement
+  getSelectedItem(aIndex) {
+    return aIndex < this.selectedItems.length ?
+      this.selectedItems[aIndex] : null;
+  }
 
-      <method name="_moveByOffsetFromUserEvent">
-        <parameter name="aOffset"/>
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (!aEvent.defaultPrevented) {
-            this._userSelecting = true;
-            this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
-            this._userSelecting = false;
-            aEvent.preventDefault();
-          }
-        ]]>
-        </body>
-      </method>
+  ensureIndexIsVisible(aIndex) {
+    return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
+  }
+
+  ensureElementIsVisible(aElement, aAlignToTop) {
+    if (!aElement) {
+      return;
+    }
 
-      <method name="_canUserSelect">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          var style = document.defaultView.getComputedStyle(aItem);
-          return style.display != "none" && style.visibility == "visible" &&
-                 style.MozUserInput != "none";
-        ]]>
-        </body>
-      </method>
+    // These calculations assume that there is no padding on the
+    // "richlistbox" element, although there might be a margin.
+    var targetRect = aElement.getBoundingClientRect();
+    var scrollRect = this.getBoundingClientRect();
+    var offset = targetRect.top - scrollRect.top;
+    if (!aAlignToTop && offset >= 0) {
+      // scrollRect.bottom wouldn't take a horizontal scroll bar into account
+      let scrollRectBottom = scrollRect.top + this.clientHeight;
+      offset = targetRect.bottom - scrollRectBottom;
+      if (offset <= 0) {
+        return;
+      }
+    }
+    this.scrollTop += offset;
+  }
 
-      <method name="_selectTimeoutHandler">
-        <parameter name="aMe"/>
-        <body>
-          aMe._fireOnSelect();
-          aMe._selectTimeout = null;
-        </body>
-      </method>
+  scrollToIndex(aIndex) {
+    var item = this.getItemAtIndex(aIndex);
+    if (item) {
+      this.ensureElementIsVisible(item, true);
+    }
+  }
 
-      <method name="timedSelect">
-        <parameter name="aItem"/>
-        <parameter name="aTimeout"/>
-        <body>
-        <![CDATA[
-          var suppress = this._suppressOnSelect;
-          if (aTimeout != -1)
-            this._suppressOnSelect = true;
-
-          this.selectItem(aItem);
-
-          this._suppressOnSelect = suppress;
+  getIndexOfFirstVisibleRow() {
+    var children = this.itemChildren;
 
-          if (aTimeout != -1) {
-            if (this._selectTimeout)
-              window.clearTimeout(this._selectTimeout);
-            this._selectTimeout =
-              window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
-          }
-        ]]>
-        </body>
-      </method>
+    for (var ix = 0; ix < children.length; ix++) {
+      if (this._isItemVisible(children[ix])) {
+        return ix;
+      }
+    }
 
-      <field name="_currentIndex">null</field>
-      <field name="_lastKeyTime">0</field>
-      <field name="_incrementalString">""</field>
-      <field name="_suppressOnSelect">false</field>
-      <field name="_userSelecting">false</field>
-      <field name="_selectTimeout">null</field>
-      <field name="_currentItem">null</field>
-      <field name="_selectionStart">null</field>
+    return -1;
+  }
+
+  getRowCount() {
+    return this.itemChildren.length;
+  }
+
+  scrollOnePage(aDirection) {
+    var children = this.itemChildren;
 
-      <!-- For backwards-compatibility and for convenience.
-        Use ensureElementIsVisible instead -->
-      <method name="ensureSelectedElementIsVisible">
-        <body>
-          <![CDATA[
-            return this.ensureElementIsVisible(this.selectedItem);
-          ]]>
-        </body>
-      </method>
-    </implementation>
+    if (children.length == 0) {
+      return 0;
+    }
 
-    <handlers>
-      <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
-               action="this._moveByOffsetFromUserEvent(-1, event);"
-               group="system"/>
+    // If nothing is selected, we just select the first element
+    // at the extreme we're moving away from
+    if (!this.currentItem) {
+      return aDirection == -1 ? children.length : 0;
+    }
 
-      <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any"
-               action="this._moveByOffsetFromUserEvent(1, event);"
-               group="system"/>
-
-      <handler event="keypress" keycode="VK_HOME" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(-this.currentIndex, event);
-        ]]>
-      </handler>
+    // If the current item is visible, scroll by one page so that
+    // the new current item is at approximately the same position as
+    // the existing current item.
+    if (this._isItemVisible(this.currentItem)) {
+      this.scrollBy(0, this.clientHeight * aDirection);
+    }
 
-      <handler event="keypress" keycode="VK_END" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
-        ]]>
-      </handler>
+    // Figure out, how many items fully fit into the view port
+    // (including the currently selected one), and determine
+    // the index of the first one lying (partially) outside
+    var height = this.clientHeight;
+    var startBorder = this.currentItem.boxObject.y;
+    if (aDirection == -1) {
+      startBorder += this.currentItem.clientHeight;
+    }
 
-      <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" key=" " modifiers="control" phase="target">
-        <![CDATA[
-          if (this.currentItem && this.selType == "multiple")
-            this.toggleItemSelection(this.currentItem);
-        ]]>
-      </handler>
+    var index = this.currentIndex;
+    for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
+      var boxObject = children[ix].boxObject;
+      if (boxObject.height == 0) {
+        continue; // hidden children have a y of 0
+      }
+      var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0);
+      if ((endBorder - startBorder) * aDirection > height) {
+        break; // we've reached the desired distance
+      }
+      index = ix;
+    }
 
-      <handler event="focus">
-        <![CDATA[
-          if (this.getRowCount() > 0) {
-            if (this.currentIndex == -1) {
-              this.currentIndex = this.getIndexOfFirstVisibleRow();
-              let currentItem = this.getItemAtIndex(this.currentIndex);
-              if (currentItem) {
-                this.selectItem(currentItem);
-              }
-            } else {
-              this.currentItem._fireEvent("DOMMenuItemActive");
-            }
-          }
-          this._lastKeyTime = 0;
-        ]]>
-      </handler>
+    return index != this.currentIndex ? index - this.currentIndex : aDirection;
+  }
+
+  _refreshSelection() {
+    // when this method is called, we know that either the currentItem
+    // and selectedItems we have are null (ctor) or a reference to an
+    // element no longer in the DOM (template).
 
-      <handler event="keypress" phase="target">
-        <![CDATA[
-          if (!event.charCode || event.altKey || event.ctrlKey || event.metaKey)
-            return;
-
-          if (event.timeStamp - this._lastKeyTime > 1000)
-            this._incrementalString = "";
+    // first look for the last-selected attribute
+    var state = this.getAttribute("last-selected");
+    if (state) {
+      var ids = state.split(" ");
 
-          var key = String.fromCharCode(event.charCode).toLowerCase();
-          this._incrementalString += key;
-          this._lastKeyTime = event.timeStamp;
-
-          // If all letters in the incremental string are the same, just
-          // try to match the first one
-          var incrementalString = /^(.)\1+$/.test(this._incrementalString) ?
-                                  RegExp.$1 : this._incrementalString;
-          var length = incrementalString.length;
+      var suppressSelect = this._suppressOnSelect;
+      this._suppressOnSelect = true;
+      this.clearSelection();
+      for (let i = 1; i < ids.length; i++) {
+        var selectedItem = document.getElementById(ids[i]);
+        if (selectedItem) {
+          this.addItemToSelection(selectedItem);
+        }
+      }
 
-          var rowCount = this.getRowCount();
-          var l = this.selectedItems.length;
-          var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
-          // start from the first element if none was selected or from the one
-          // following the selected one if it's a new or a repeated-letter search
-          if (start == -1 || length == 1)
-            start++;
+      var currentItem = document.getElementById(ids[0]);
+      if (!currentItem && this._currentIndex) {
+        currentItem = this.getItemAtIndex(Math.min(
+          this._currentIndex - 1, this.getRowCount()));
+      }
+      if (currentItem) {
+        this.currentItem = currentItem;
+        if (this.selType != "multiple" && this.selectedCount == 0) {
+          this.selectedItem = currentItem;
+        }
 
-          for (var i = 0; i < rowCount; i++) {
-            var k = (start + i) % rowCount;
-            var listitem = this.getItemAtIndex(k);
-            if (!this._canUserSelect(listitem))
-              continue;
-            // allow richlistitems to specify the string being searched for
-            var searchText = "searchLabel" in listitem ? listitem.searchLabel :
-                             listitem.getAttribute("label"); // (see also bug 250123)
-            searchText = searchText.substring(0, length).toLowerCase();
-            if (searchText == incrementalString) {
-              this.ensureIndexIsVisible(k);
-              this.timedSelect(listitem, this._selectDelay);
-              break;
-            }
-          }
-        ]]>
-      </handler>
-
-      <handler event="click">
-        <![CDATA[
-          // clicking into nothing should unselect
-          if (event.originalTarget == this) {
-            this.clearSelection();
-            this.currentItem = null;
-          }
-        ]]>
-      </handler>
+        if (this.clientHeight) {
+          this.ensureElementIsVisible(currentItem);
+        } else {
+          // XXX hack around a bug in ensureElementIsVisible as it will
+          // scroll beyond the last element, bug 493645.
+          this.ensureElementIsVisible(currentItem.previousElementSibling);
+        }
+      }
+      this._suppressOnSelect = suppressSelect;
+      // XXX actually it's just a refresh, but at least
+      // the Extensions manager expects this:
+      this._fireOnSelect();
+      return;
+    }
 
-      <handler event="MozSwipeGesture">
-        <![CDATA[
-          // Only handle swipe gestures up and down
-          switch (event.direction) {
-            case event.DIRECTION_DOWN:
-              this.scrollTop = this.scrollHeight;
-              break;
-            case event.DIRECTION_UP:
-              this.scrollTop = 0;
-              break;
-          }
-        ]]>
-      </handler>
-    </handlers>
-  </binding>
+    // try to restore the selected items according to their IDs
+    // (applies after a template rebuild, if last-selected was not set)
+    if (this.selectedItems) {
+      let itemIds = [];
+      for (let i = this.selectedCount - 1; i >= 0; i--) {
+        let selectedItem = this.selectedItems[i];
+        itemIds.push(selectedItem.id);
+        this.selectedItems.remove(selectedItem);
+      }
+      for (let i = 0; i < itemIds.length; i++) {
+        let selectedItem = document.getElementById(itemIds[i]);
+        if (selectedItem) {
+          this.selectedItems.append(selectedItem);
+        }
+      }
+    }
+    if (this.currentItem && this.currentItem.id) {
+      this.currentItem = document.getElementById(this.currentItem.id);
+    } else {
+      this.currentItem = null;
+    }
 
-  <binding id="richlistitem"
-           extends="chrome://global/content/bindings/general.xml#basetext">
-    <implementation implements="nsIDOMXULSelectControlItemElement">
-      <field name="selectedByMouseOver">false</field>
+    // if we have no previously current item or if the above check fails to
+    // find the previous nodes (which causes it to clear selection)
+    if (!this.currentItem && this.selectedCount == 0) {
+      this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0;
 
-      <destructor>
-        <![CDATA[
-          var control = this.control;
-          if (!control)
-            return;
-          // When we are destructed and we are current or selected, unselect ourselves
-          // so that richlistbox's selection doesn't point to something not in the DOM.
-          // We don't want to reset last-selected, so we set _suppressOnSelect.
-          if (this.selected) {
-            var suppressSelect = control._suppressOnSelect;
-            control._suppressOnSelect = true;
-            control.removeItemFromSelection(this);
-            control._suppressOnSelect = suppressSelect;
-          }
-          if (this.current)
-            control.currentItem = null;
-        ]]>
-      </destructor>
+      // cf. listbox constructor:
+      // select items according to their attributes
+      var children = this.itemChildren;
+      for (let i = 0; i < children.length; ++i) {
+        if (children[i].getAttribute("selected") == "true") {
+          this.selectedItems.append(children[i]);
+        }
+      }
+    }
 
-      <!-- nsIDOMXULSelectControlItemElement -->
-      <property name="label" readonly="true">
-        <!-- Setter purposely not implemented; the getter returns a
-             concatentation of label text to expose via accessibility APIs -->
-        <getter>
-          <![CDATA[
-            const XULNS =
-              "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-            return Array.map(this.getElementsByTagNameNS(XULNS, "label"),
-                             label => label.value)
-                        .join(" ");
-          ]]>
-        </getter>
-      </property>
+    if (this.selType != "multiple" && this.selectedCount == 0) {
+      this.selectedItem = this.currentItem;
+    }
+  }
+
+  _isItemVisible(aItem) {
+    if (!aItem) {
+      return false;
+    }
 
-      <property name="searchLabel">
-        <getter>
-          <![CDATA[
-            return this.hasAttribute("searchlabel") ?
-                   this.getAttribute("searchlabel") : this.label;
-          ]]>
-        </getter>
-        <setter>
-          <![CDATA[
-            if (val !== null)
-              this.setAttribute("searchlabel", val);
-            else
-              // fall back to the label property (default value)
-              this.removeAttribute("searchlabel");
-            return val;
-          ]]>
-        </setter>
-      </property>
+    var y = this.scrollTop + this.boxObject.y;
+
+    // Partially visible items are also considered visible
+    return (aItem.boxObject.y + aItem.clientHeight > y) &&
+      (aItem.boxObject.y < y + this.clientHeight);
+  }
+
+  moveByOffset(aOffset, aIsSelecting, aIsSelectingRange) {
+    if ((aIsSelectingRange || !aIsSelecting) &&
+      this.selType != "multiple") {
+      return;
+    }
 
-      <!-- nsIDOMXULSelectControlItemElement -->
-      <property name="value" onget="return this.getAttribute('value');"
-                             onset="this.setAttribute('value', val); return val;"/>
+    var newIndex = this.currentIndex + aOffset;
+    if (newIndex < 0) {
+      newIndex = 0;
+    }
 
-      <!-- nsIDOMXULSelectControlItemElement -->
-      <property name="selected" onget="return this.getAttribute('selected') == 'true';">
-        <setter><![CDATA[
-          if (val)
-            this.setAttribute("selected", "true");
-          else
-            this.removeAttribute("selected");
-
-          return val;
-        ]]></setter>
-      </property>
+    var numItems = this.getRowCount();
+    if (newIndex > numItems - 1) {
+      newIndex = numItems - 1;
+    }
 
-      <!-- nsIDOMXULSelectControlItemElement -->
-      <property name="control">
-        <getter><![CDATA[
-          var parent = this.parentNode;
-          while (parent) {
-            if (parent.localName == "richlistbox")
-              return parent;
-            parent = parent.parentNode;
-          }
-          return null;
-        ]]></getter>
-      </property>
+    var newItem = this.getItemAtIndex(newIndex);
+    // make sure that the item is actually visible/selectable
+    if (this._userSelecting && newItem && !this._canUserSelect(newItem)) {
+      newItem =
+      aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) :
+      this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
+    }
+    if (newItem) {
+      this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
+      if (aIsSelectingRange) {
+        this.selectItemRange(null, newItem);
+      } else if (aIsSelecting) {
+        this.selectItem(newItem);
+      }
 
-      <property name="current" onget="return this.getAttribute('current') == 'true';">
-        <setter><![CDATA[
-          if (val)
-            this.setAttribute("current", "true");
-          else
-            this.removeAttribute("current");
+      this.currentItem = newItem;
+    }
+  }
 
-          let control = this.control;
-          if (!control || !control.suppressMenuItemEvent) {
-            this._fireEvent(val ? "DOMMenuItemActive" : "DOMMenuItemInactive");
-          }
-
-          return val;
-        ]]></setter>
-      </property>
+  _moveByOffsetFromUserEvent(aOffset, aEvent) {
+    if (!aEvent.defaultPrevented) {
+      this._userSelecting = true;
+      this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
+      this._userSelecting = false;
+      aEvent.preventDefault();
+    }
+  }
 
-      <method name="_fireEvent">
-        <parameter name="name"/>
-        <body>
-        <![CDATA[
-          var event = document.createEvent("Events");
-          event.initEvent(name, true, true);
-          this.dispatchEvent(event);
-        ]]>
-        </body>
-      </method>
-    </implementation>
+  _canUserSelect(aItem) {
+    var style = document.defaultView.getComputedStyle(aItem);
+    return style.display != "none" && style.visibility == "visible" &&
+      style.MozUserInput != "none";
+  }
+
+  _selectTimeoutHandler(aMe) {
+    aMe._fireOnSelect();
+    aMe._selectTimeout = null;
+  }
+
+  timedSelect(aItem, aTimeout) {
+    var suppress = this._suppressOnSelect;
+    if (aTimeout != -1) {
+      this._suppressOnSelect = true;
+    }
+
+    this.selectItem(aItem);
 
-    <handlers>
-      <!-- If there is no modifier key, we select on mousedown, not
-           click, so that drags work correctly. -->
-      <handler event="mousedown">
-        <![CDATA[
-          var control = this.control;
-          if (!control || control.disabled)
-            return;
-          if ((!event.ctrlKey || (/Mac/.test(navigator.platform) && event.button == 2)) &&
-              !event.shiftKey && !event.metaKey) {
-            if (!this.selected) {
-              control.selectItem(this);
-            }
-            control.currentItem = this;
-          }
-        ]]>
-      </handler>
+    this._suppressOnSelect = suppress;
+
+    if (aTimeout != -1) {
+      if (this._selectTimeout) {
+        window.clearTimeout(this._selectTimeout);
+      }
+      this._selectTimeout =
+        window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
+    }
+  }
 
-      <!-- On a click (up+down on the same item), deselect everything
-           except this item. -->
-      <handler event="click" button="0">
-        <![CDATA[
-          var control = this.control;
-          if (!control || control.disabled)
-            return;
-          control._userSelecting = true;
-          if (control.selType != "multiple") {
-            control.selectItem(this);
-          } else if (event.ctrlKey || event.metaKey) {
-            control.toggleItemSelection(this);
-            control.currentItem = this;
-          } else if (event.shiftKey) {
-            control.selectItemRange(null, this);
-            control.currentItem = this;
-          } else {
-            /* We want to deselect all the selected items except what was
-              clicked, UNLESS it was a right-click.  We have to do this
-              in click rather than mousedown so that you can drag a
-              selected group of items */
+  /**
+   * For backwards-compatibility and for convenience.
+   * Use ensureElementIsVisible instead
+   */
+  ensureSelectedElementIsVisible() {
+    return this.ensureElementIsVisible(this.selectedItem);
+  }
+}
 
-            // use selectItemRange instead of selectItem, because this
-            // doesn't de- and reselect this item if it is selected
-            control.selectItemRange(this, this);
-          }
-          control._userSelecting = false;
-        ]]>
-      </handler>
-    </handlers>
-  </binding>
-</bindings>
+MozXULElement.implementCustomInterface(MozRichListBox, [
+  Ci.nsIDOMXULSelectControlElement,
+  Ci.nsIDOMXULMultiSelectControlElement,
+]);
+
+customElements.define("richlistbox", MozRichListBox);
+
+}
--- a/toolkit/content/widgets/richlistbox.xml
+++ b/toolkit/content/widgets/richlistbox.xml
@@ -7,934 +7,16 @@
 <!-- This file relies on these specific Chrome/XBL globals -->
 <!-- globals ChromeNodeList -->
 
 <bindings id="richlistboxBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
-  <binding id="richlistbox"
-           extends="chrome://global/content/bindings/general.xml#basecontrol">
-    <content allowevents="true" orient="vertical"/>
-
-    <implementation implements="nsIDOMXULMultiSelectControlElement">
-      <constructor>
-        <![CDATA[
-          this._refreshSelection();
-        ]]>
-      </constructor>
-
-      <method name="_fireOnSelect">
-        <body>
-          <![CDATA[
-            // make sure not to modify last-selected when suppressing select events
-            // (otherwise we'll lose the selection when a template gets rebuilt)
-            if (this._suppressOnSelect || this.suppressOnSelect)
-              return;
-
-            // remember the current item and all selected items with IDs
-            var state = this.currentItem ? this.currentItem.id : "";
-            if (this.selType == "multiple" && this.selectedCount) {
-              let getId = function getId(aItem) { return aItem.id; };
-              state += " " + [...this.selectedItems].filter(getId).map(getId).join(" ");
-            }
-            if (state)
-              this.setAttribute("last-selected", state);
-            else
-              this.removeAttribute("last-selected");
-
-            // preserve the index just in case no IDs are available
-            if (this.currentIndex > -1)
-              this._currentIndex = this.currentIndex + 1;
-
-            var event = document.createEvent("Events");
-            event.initEvent("select", true, true);
-            this.dispatchEvent(event);
-
-            // always call this (allows a commandupdater without controller)
-            document.commandDispatcher.updateCommands("richlistbox-select");
-          ]]>
-        </body>
-      </method>
-
-      <method name="getNextItem">
-        <parameter name="aStartItem"/>
-        <parameter name="aDelta"/>
-        <body>
-        <![CDATA[
-          while (aStartItem) {
-            aStartItem = aStartItem.nextSibling;
-            if (aStartItem && aStartItem.localName == "richlistitem" &&
-                (!this._userSelecting || this._canUserSelect(aStartItem))) {
-              --aDelta;
-              if (aDelta == 0)
-                return aStartItem;
-            }
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="getPreviousItem">
-        <parameter name="aStartItem"/>
-        <parameter name="aDelta"/>
-        <body>
-        <![CDATA[
-          while (aStartItem) {
-            aStartItem = aStartItem.previousSibling;
-            if (aStartItem && aStartItem.localName == "richlistitem" &&
-                (!this._userSelecting || this._canUserSelect(aStartItem))) {
-              --aDelta;
-              if (aDelta == 0)
-                return aStartItem;
-            }
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="appendItem">
-        <parameter name="aLabel"/>
-        <parameter name="aValue"/>
-        <body>
-          var item =
-            this.ownerDocument.createXULElement("richlistitem");
-          item.setAttribute("value", aValue);
-
-          var label = this.ownerDocument.createXULElement("label");
-          label.setAttribute("value", aLabel);
-          label.setAttribute("flex", "1");
-          label.setAttribute("crop", "end");
-          item.appendChild(label);
-
-          this.appendChild(item);
-
-          return item;
-        </body>
-      </method>
-
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="selectedItem"
-                onset="this.selectItem(val);">
-        <getter>
-        <![CDATA[
-          return this.selectedItems.length > 0 ? this.selectedItems[0] : null;
-        ]]>
-        </getter>
-      </property>
-
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="selectedIndex">
-        <getter>
-        <![CDATA[
-          if (this.selectedItems.length > 0)
-            return this.getIndexOfItem(this.selectedItems[0]);
-          return -1;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val >= 0) {
-            // This is a micro-optimization so that a call to getIndexOfItem or
-            // getItemAtIndex caused by _fireOnSelect (especially for derived
-            // widgets) won't loop the children.
-            this._selecting = {
-              item: this.getItemAtIndex(val),
-              index: val,
-            };
-            this.selectItem(this._selecting.item);
-            delete this._selecting;
-          } else {
-            this.clearSelection();
-            this.currentItem = null;
-          }
-        ]]>
-        </setter>
-      </property>
-
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="value">
-        <getter>
-        <![CDATA[
-          if (this.selectedItems.length > 0)
-            return this.selectedItem.value;
-          return null;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          var kids = this.getElementsByAttribute("value", val);
-          if (kids && kids.item(0))
-            this.selectItem(kids[0]);
-          return val;
-        ]]>
-        </setter>
-      </property>
-
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="itemCount" readonly="true"
-                onget="return this.itemChildren.length"/>
-
-      <!-- nsIDOMXULSelectControlElement -->
-      <method name="getIndexOfItem">
-        <parameter name="aItem"/>
-        <body>
-          <![CDATA[
-            // don't search the children, if we're looking for none of them
-            if (aItem == null)
-              return -1;
-            if (this._selecting && this._selecting.item == aItem)
-              return this._selecting.index;
-            return this.itemChildren.indexOf(aItem);
-          ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULSelectControlElement -->
-      <method name="getItemAtIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            if (this._selecting && this._selecting.index == aIndex)
-              return this._selecting.item;
-            return this.itemChildren[aIndex] || null;
-          ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="selType"
-                onget="return this.getAttribute('seltype');"
-                onset="this.setAttribute('seltype', val); return val;"/>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="currentItem" onget="return this._currentItem;">
-        <setter>
-          if (this._currentItem == val)
-            return val;
-
-          if (this._currentItem)
-            this._currentItem.current = false;
-          this._currentItem = val;
-
-          if (val)
-            val.current = true;
-
-          return val;
-        </setter>
-      </property>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="currentIndex">
-        <getter>
-          return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val >= 0)
-            this.currentItem = this.getItemAtIndex(val);
-          else
-            this.currentItem = null;
-        ]]>
-        </setter>
-      </property>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <field name="selectedItems">new ChromeNodeList()</field>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="addItemToSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (this.selType != "multiple" && this.selectedCount)
-            return;
-
-          if (aItem.selected)
-            return;
-
-          this.selectedItems.append(aItem);
-          aItem.selected = true;
-
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="removeItemFromSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (!aItem.selected)
-            return;
-
-          this.selectedItems.remove(aItem);
-          aItem.selected = false;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="toggleItemSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (aItem.selected)
-            this.removeItemFromSelection(aItem);
-          else
-            this.addItemToSelection(aItem);
-        ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="selectItem">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (!aItem)
-            return;
-
-          if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem)
-            return;
-
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          this.clearSelection();
-          this.addItemToSelection(aItem);
-          this.currentItem = aItem;
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="selectItemRange">
-        <parameter name="aStartItem"/>
-        <parameter name="aEndItem"/>
-        <body>
-        <![CDATA[
-          if (this.selType != "multiple")
-            return;
-
-          if (!aStartItem)
-            aStartItem = this._selectionStart ?
-              this._selectionStart : this.currentItem;
-
-          if (!aStartItem)
-            aStartItem = aEndItem;
-
-          var suppressSelect = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          this._selectionStart = aStartItem;
-
-          var currentItem;
-          var startIndex = this.getIndexOfItem(aStartItem);
-          var endIndex = this.getIndexOfItem(aEndItem);
-          if (endIndex < startIndex) {
-            currentItem = aEndItem;
-            aEndItem = aStartItem;
-            aStartItem = currentItem;
-          } else {
-            currentItem = aStartItem;
-          }
-
-          while (currentItem) {
-            this.addItemToSelection(currentItem);
-            if (currentItem == aEndItem) {
-              currentItem = this.getNextItem(currentItem, 1);
-              break;
-            }
-            currentItem = this.getNextItem(currentItem, 1);
-          }
-
-          // Clear around new selection
-          // Don't use clearSelection() because it causes a lot of noise
-          // with respect to selection removed notifications used by the
-          // accessibility API support.
-          var userSelecting = this._userSelecting;
-          this._userSelecting = false; // that's US automatically unselecting
-          for (; currentItem; currentItem = this.getNextItem(currentItem, 1))
-            this.removeItemFromSelection(currentItem);
-
-          for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem;
-               currentItem = this.getNextItem(currentItem, 1))
-            this.removeItemFromSelection(currentItem);
-          this._userSelecting = userSelecting;
-
-          this._suppressOnSelect = suppressSelect;
-
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="selectAll">
-        <body>
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          var item = this.getItemAtIndex(0);
-          while (item) {
-            this.addItemToSelection(item);
-            item = this.getNextItem(item, 1);
-          }
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="invertSelection">
-        <body>
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          var item = this.getItemAtIndex(0);
-          while (item) {
-            if (item.selected)
-              this.removeItemFromSelection(item);
-            else
-              this.addItemToSelection(item);
-            item = this.getNextItem(item, 1);
-          }
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="clearSelection">
-        <body>
-        <![CDATA[
-          if (this.selectedItems) {
-            while (this.selectedItems.length > 0) {
-              let item = this.selectedItems[0];
-              item.selected = false;
-              this.selectedItems.remove(item);
-            }
-          }
-
-          this._selectionStart = null;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="selectedCount" readonly="true"
-                onget="return this.selectedItems.length;"/>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="getSelectedItem">
-        <parameter name="aIndex"/>
-        <body>
-        <![CDATA[
-          return aIndex < this.selectedItems.length ?
-            this.selectedItems[aIndex] : null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="ensureIndexIsVisible">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
-          ]]>
-        </body>
-      </method>
-
-      <method name="ensureElementIsVisible">
-        <parameter name="aElement"/>
-        <parameter name="aAlignToTop"/>
-        <body>
-          <![CDATA[
-            if (!aElement) {
-              return;
-            }
-
-            // These calculations assume that there is no padding on the
-            // "richlistbox" element, although there might be a margin.
-            var targetRect = aElement.getBoundingClientRect();
-            var scrollRect = this.getBoundingClientRect();
-            var offset = targetRect.top - scrollRect.top;
-            if (!aAlignToTop && offset >= 0) {
-              // scrollRect.bottom wouldn't take a horizontal scroll bar into account
-              let scrollRectBottom = scrollRect.top + this.clientHeight;
-              offset = targetRect.bottom - scrollRectBottom;
-              if (offset <= 0)
-                return;
-            }
-            this.scrollTop += offset;
-          ]]>
-        </body>
-      </method>
-
-      <method name="scrollToIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            var item = this.getItemAtIndex(aIndex);
-            if (item) {
-              this.ensureElementIsVisible(item, true);
-            }
-          ]]>
-        </body>
-      </method>
-
-      <method name="getIndexOfFirstVisibleRow">
-        <body>
-          <![CDATA[
-            var children = this.itemChildren;
-
-            for (var ix = 0; ix < children.length; ix++)
-              if (this._isItemVisible(children[ix]))
-                return ix;
-
-            return -1;
-          ]]>
-        </body>
-      </method>
-
-      <method name="getRowCount">
-        <body>
-          <![CDATA[
-            return this.itemChildren.length;
-          ]]>
-        </body>
-      </method>
-
-      <method name="scrollOnePage">
-        <parameter name="aDirection"/> <!-- Must be -1 or 1 -->
-        <body>
-          <![CDATA[
-            var children = this.itemChildren;
-
-            if (children.length == 0)
-              return 0;
-
-            // If nothing is selected, we just select the first element
-            // at the extreme we're moving away from
-            if (!this.currentItem)
-              return aDirection == -1 ? children.length : 0;
-
-            // If the current item is visible, scroll by one page so that
-            // the new current item is at approximately the same position as
-            // the existing current item.
-            if (this._isItemVisible(this.currentItem))
-              this.scrollBy(0, this.clientHeight * aDirection);
-
-            // Figure out, how many items fully fit into the view port
-            // (including the currently selected one), and determine
-            // the index of the first one lying (partially) outside
-            var height = this.clientHeight;
-            var startBorder = this.currentItem.boxObject.y;
-            if (aDirection == -1)
-              startBorder += this.currentItem.clientHeight;
-
-            var index = this.currentIndex;
-            for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
-              var boxObject = children[ix].boxObject;
-              if (boxObject.height == 0)
-                continue; // hidden children have a y of 0
-              var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0);
-              if ((endBorder - startBorder) * aDirection > height)
-                break; // we've reached the desired distance
-              index = ix;
-            }
-
-            return index != this.currentIndex ? index - this.currentIndex : aDirection;
-          ]]>
-        </body>
-      </method>
-
-      <property name="itemChildren" readonly="true">
-        <getter>
-          <![CDATA[
-            let children = Array.from(this.children)
-                                .filter(node => node.localName == "richlistitem");
-            return children;
-          ]]>
-        </getter>
-      </property>
-
-      <method name="_refreshSelection">
-        <body>
-          <![CDATA[
-            // when this method is called, we know that either the currentItem
-            // and selectedItems we have are null (ctor) or a reference to an
-            // element no longer in the DOM (template).
-
-            // first look for the last-selected attribute
-            var state = this.getAttribute("last-selected");
-            if (state) {
-              var ids = state.split(" ");
-
-              var suppressSelect = this._suppressOnSelect;
-              this._suppressOnSelect = true;
-              this.clearSelection();
-              for (let i = 1; i < ids.length; i++) {
-                var selectedItem = document.getElementById(ids[i]);
-                if (selectedItem)
-                  this.addItemToSelection(selectedItem);
-              }
-
-              var currentItem = document.getElementById(ids[0]);
-              if (!currentItem && this._currentIndex)
-                currentItem = this.getItemAtIndex(Math.min(
-                  this._currentIndex - 1, this.getRowCount()));
-              if (currentItem) {
-                this.currentItem = currentItem;
-                if (this.selType != "multiple" && this.selectedCount == 0)
-                  this.selectedItem = currentItem;
-
-                if (this.clientHeight) {
-                  this.ensureElementIsVisible(currentItem);
-                } else {
-                  // XXX hack around a bug in ensureElementIsVisible as it will
-                  // scroll beyond the last element, bug 493645.
-                  this.ensureElementIsVisible(currentItem.previousElementSibling);
-                }
-              }
-              this._suppressOnSelect = suppressSelect;
-              // XXX actually it's just a refresh, but at least
-              // the Extensions manager expects this:
-              this._fireOnSelect();
-              return;
-            }
-
-            // try to restore the selected items according to their IDs
-            // (applies after a template rebuild, if last-selected was not set)
-            if (this.selectedItems) {
-              let itemIds = [];
-              for (let i = this.selectedCount - 1; i >= 0; i--) {
-                let selectedItem = this.selectedItems[i];
-                itemIds.push(selectedItem.id);
-                this.selectedItems.remove(selectedItem);
-              }
-              for (let i = 0; i < itemIds.length; i++) {
-                let selectedItem = document.getElementById(itemIds[i]);
-                if (selectedItem) {
-                  this.selectedItems.append(selectedItem);
-                }
-              }
-            }
-            if (this.currentItem && this.currentItem.id)
-              this.currentItem = document.getElementById(this.currentItem.id);
-            else
-              this.currentItem = null;
-
-            // if we have no previously current item or if the above check fails to
-            // find the previous nodes (which causes it to clear selection)
-            if (!this.currentItem && this.selectedCount == 0) {
-              this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0;
-
-              // cf. listbox constructor:
-              // select items according to their attributes
-              var children = this.itemChildren;
-              for (let i = 0; i < children.length; ++i) {
-                if (children[i].getAttribute("selected") == "true")
-                  this.selectedItems.append(children[i]);
-              }
-            }
-
-            if (this.selType != "multiple" && this.selectedCount == 0)
-              this.selectedItem = this.currentItem;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_isItemVisible">
-        <parameter name="aItem"/>
-        <body>
-          <![CDATA[
-            if (!aItem)
-              return false;
-
-            var y = this.scrollTop + this.boxObject.y;
-
-            // Partially visible items are also considered visible
-            return (aItem.boxObject.y + aItem.clientHeight > y) &&
-                   (aItem.boxObject.y < y + this.clientHeight);
-          ]]>
-        </body>
-      </method>
-
-      <property name="suppressOnSelect"
-                onget="return this.getAttribute('suppressonselect') == 'true';"
-                onset="this.setAttribute('suppressonselect', val);"/>
-
-      <property name="_selectDelay"
-                onset="this.setAttribute('_selectDelay', val);"
-                onget="return this.getAttribute('_selectDelay') || 50;"/>
-
-      <method name="moveByOffset">
-        <parameter name="aOffset"/>
-        <parameter name="aIsSelecting"/>
-        <parameter name="aIsSelectingRange"/>
-        <body>
-        <![CDATA[
-          if ((aIsSelectingRange || !aIsSelecting) &&
-              this.selType != "multiple")
-            return;
-
-          var newIndex = this.currentIndex + aOffset;
-          if (newIndex < 0)
-            newIndex = 0;
-
-          var numItems = this.getRowCount();
-          if (newIndex > numItems - 1)
-            newIndex = numItems - 1;
-
-          var newItem = this.getItemAtIndex(newIndex);
-          // make sure that the item is actually visible/selectable
-          if (this._userSelecting && newItem && !this._canUserSelect(newItem))
-            newItem =
-              aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) :
-                            this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
-          if (newItem) {
-            this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
-            if (aIsSelectingRange)
-              this.selectItemRange(null, newItem);
-            else if (aIsSelecting)
-              this.selectItem(newItem);
-
-            this.currentItem = newItem;
-          }
-        ]]>
-        </body>
-      </method>
-
-      <method name="_moveByOffsetFromUserEvent">
-        <parameter name="aOffset"/>
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (!aEvent.defaultPrevented) {
-            this._userSelecting = true;
-            this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
-            this._userSelecting = false;
-            aEvent.preventDefault();
-          }
-        ]]>
-        </body>
-      </method>
-
-      <method name="_canUserSelect">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          var style = document.defaultView.getComputedStyle(aItem);
-          return style.display != "none" && style.visibility == "visible" &&
-                 style.MozUserInput != "none";
-        ]]>
-        </body>
-      </method>
-
-      <method name="_selectTimeoutHandler">
-        <parameter name="aMe"/>
-        <body>
-          aMe._fireOnSelect();
-          aMe._selectTimeout = null;
-        </body>
-      </method>
-
-      <method name="timedSelect">
-        <parameter name="aItem"/>
-        <parameter name="aTimeout"/>
-        <body>
-        <![CDATA[
-          var suppress = this._suppressOnSelect;
-          if (aTimeout != -1)
-            this._suppressOnSelect = true;
-
-          this.selectItem(aItem);
-
-          this._suppressOnSelect = suppress;
-
-          if (aTimeout != -1) {
-            if (this._selectTimeout)
-              window.clearTimeout(this._selectTimeout);
-            this._selectTimeout =
-              window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
-          }
-        ]]>
-        </body>
-      </method>
-
-      <field name="_currentIndex">null</field>
-      <field name="_lastKeyTime">0</field>
-      <field name="_incrementalString">""</field>
-      <field name="_suppressOnSelect">false</field>
-      <field name="_userSelecting">false</field>
-      <field name="_selectTimeout">null</field>
-      <field name="_currentItem">null</field>
-      <field name="_selectionStart">null</field>
-
-      <!-- For backwards-compatibility and for convenience.
-        Use ensureElementIsVisible instead -->
-      <method name="ensureSelectedElementIsVisible">
-        <body>
-          <![CDATA[
-            return this.ensureElementIsVisible(this.selectedItem);
-          ]]>
-        </body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
-               action="this._moveByOffsetFromUserEvent(-1, event);"
-               group="system"/>
-
-      <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any"
-               action="this._moveByOffsetFromUserEvent(1, event);"
-               group="system"/>
-
-      <handler event="keypress" keycode="VK_HOME" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(-this.currentIndex, event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" keycode="VK_END" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" key=" " modifiers="control" phase="target">
-        <![CDATA[
-          if (this.currentItem && this.selType == "multiple")
-            this.toggleItemSelection(this.currentItem);
-        ]]>
-      </handler>
-
-      <handler event="focus">
-        <![CDATA[
-          if (this.getRowCount() > 0) {
-            if (this.currentIndex == -1) {
-              this.currentIndex = this.getIndexOfFirstVisibleRow();
-              let currentItem = this.getItemAtIndex(this.currentIndex);
-              if (currentItem) {
-                this.selectItem(currentItem);
-              }
-            } else {
-              this.currentItem._fireEvent("DOMMenuItemActive");
-            }
-          }
-          this._lastKeyTime = 0;
-        ]]>
-      </handler>
-
-      <handler event="keypress" phase="target">
-        <![CDATA[
-          if (!event.charCode || event.altKey || event.ctrlKey || event.metaKey)
-            return;
-
-          if (event.timeStamp - this._lastKeyTime > 1000)
-            this._incrementalString = "";
-
-          var key = String.fromCharCode(event.charCode).toLowerCase();
-          this._incrementalString += key;
-          this._lastKeyTime = event.timeStamp;
-
-          // If all letters in the incremental string are the same, just
-          // try to match the first one
-          var incrementalString = /^(.)\1+$/.test(this._incrementalString) ?
-                                  RegExp.$1 : this._incrementalString;
-          var length = incrementalString.length;
-
-          var rowCount = this.getRowCount();
-          var l = this.selectedItems.length;
-          var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
-          // start from the first element if none was selected or from the one
-          // following the selected one if it's a new or a repeated-letter search
-          if (start == -1 || length == 1)
-            start++;
-
-          for (var i = 0; i < rowCount; i++) {
-            var k = (start + i) % rowCount;
-            var listitem = this.getItemAtIndex(k);
-            if (!this._canUserSelect(listitem))
-              continue;
-            // allow richlistitems to specify the string being searched for
-            var searchText = "searchLabel" in listitem ? listitem.searchLabel :
-                             listitem.getAttribute("label"); // (see also bug 250123)
-            searchText = searchText.substring(0, length).toLowerCase();
-            if (searchText == incrementalString) {
-              this.ensureIndexIsVisible(k);
-              this.timedSelect(listitem, this._selectDelay);
-              break;
-            }
-          }
-        ]]>
-      </handler>
-
-      <handler event="click">
-        <![CDATA[
-          // clicking into nothing should unselect
-          if (event.originalTarget == this) {
-            this.clearSelection();
-            this.currentItem = null;
-          }
-        ]]>
-      </handler>
-
-      <handler event="MozSwipeGesture">
-        <![CDATA[
-          // Only handle swipe gestures up and down
-          switch (event.direction) {
-            case event.DIRECTION_DOWN:
-              this.scrollTop = this.scrollHeight;
-              break;
-            case event.DIRECTION_UP:
-              this.scrollTop = 0;
-              break;
-          }
-        ]]>
-      </handler>
-    </handlers>
-  </binding>
-
   <binding id="richlistitem"
            extends="chrome://global/content/bindings/general.xml#basetext">
     <implementation implements="nsIDOMXULSelectControlItemElement">
       <field name="selectedByMouseOver">false</field>
 
       <destructor>
         <![CDATA[
           var control = this.control;
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -717,17 +717,16 @@ wizardpage {
 
 .wizard-buttons {
   -moz-binding: url("chrome://global/content/bindings/wizard.xml#wizard-buttons");
 }
 
 /********** Rich Listbox ********/
 
 richlistbox {
-  -moz-binding: url('chrome://global/content/bindings/richlistbox.xml#richlistbox');
   -moz-user-focus: normal;
   -moz-box-orient: vertical;
 }
 
 richlistitem {
   -moz-binding: url('chrome://global/content/bindings/richlistbox.xml#richlistitem');
 }