Bug 1482406 - Update Debugger Frontend v81. r=dwalsh
authorJason Laster <jason.laster.11@gmail.com>
Fri, 10 Aug 2018 17:54:51 -0400
changeset 486181 8d873104914ee117bc19c749f22b1ee29989e6b4
parent 486180 cde3d3c1697f8436237b3fcceb50175e4aaaa4f2
child 486182 7311e3026a473a4e4329257e6fd50b38bba6f614
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdwalsh
bugs1482406
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1482406 - Update Debugger Frontend v81. r=dwalsh
devtools/client/debugger/new/README.mozilla
devtools/client/debugger/new/src/actions/index.js
devtools/client/debugger/new/src/actions/moz.build
devtools/client/debugger/new/src/actions/sources/index.js
devtools/client/debugger/new/src/actions/sources/moz.build
devtools/client/debugger/new/src/actions/sources/newSources.js
devtools/client/debugger/new/src/actions/sources/select.js
devtools/client/debugger/new/src/actions/tabs.js
devtools/client/debugger/new/src/components/Editor/Tab.js
devtools/client/debugger/new/src/components/Editor/index.js
devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
devtools/client/debugger/new/src/reducers/index.js
devtools/client/debugger/new/src/reducers/moz.build
devtools/client/debugger/new/src/reducers/sources.js
devtools/client/debugger/new/src/reducers/tabs.js
devtools/client/debugger/new/src/selectors/index.js
devtools/client/debugger/new/src/utils/editor/index.js
devtools/client/debugger/new/test/mochitest/browser_dbg-console-async.js
--- 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 80
+Version 81
 
-Comparison: https://github.com/devtools-html/debugger.html/compare/release-79...release-80
+Comparison: https://github.com/devtools-html/debugger.html/compare/release-80...release-81
 
 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/src/actions/index.js
+++ b/devtools/client/debugger/new/src/actions/index.js
@@ -55,16 +55,20 @@ var quickOpen = _interopRequireWildcard(
 var _sourceTree = require("./source-tree");
 
 var sourceTree = _interopRequireWildcard(_sourceTree);
 
 var _sources = require("./sources/index");
 
 var sources = _interopRequireWildcard(_sources);
 
+var _tabs = require("./tabs");
+
+var tabs = _interopRequireWildcard(_tabs);
+
 var _debuggee = require("./debuggee");
 
 var debuggee = _interopRequireWildcard(_debuggee);
 
 var _toolbox = require("./toolbox");
 
 var toolbox = _interopRequireWildcard(_toolbox);
 
@@ -77,16 +81,17 @@ function _interopRequireWildcard(obj) { 
 /* 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/>. */
 exports.default = { ...navigation,
   ...breakpoints,
   ...expressions,
   ...eventListeners,
   ...sources,
+  ...tabs,
   ...pause,
   ...ui,
   ...fileSearch,
   ...ast,
   ...coverage,
   ...projectTextSearch,
   ...replay,
   ...quickOpen,
--- a/devtools/client/debugger/new/src/actions/moz.build
+++ b/devtools/client/debugger/new/src/actions/moz.build
@@ -20,11 +20,12 @@ DevToolsModules(
     'file-search.js',
     'index.js',
     'navigation.js',
     'preview.js',
     'project-text-search.js',
     'quick-open.js',
     'replay.js',
     'source-tree.js',
+    'tabs.js',
     'toolbox.js',
     'ui.js',
 )
--- a/devtools/client/debugger/new/src/actions/sources/index.js
+++ b/devtools/client/debugger/new/src/actions/sources/index.js
@@ -57,21 +57,9 @@ var _select = require("./select");
 Object.keys(_select).forEach(function (key) {
   if (key === "default" || key === "__esModule") return;
   Object.defineProperty(exports, key, {
     enumerable: true,
     get: function () {
       return _select[key];
     }
   });
-});
-
-var _tabs = require("./tabs");
-
-Object.keys(_tabs).forEach(function (key) {
-  if (key === "default" || key === "__esModule") return;
-  Object.defineProperty(exports, key, {
-    enumerable: true,
-    get: function () {
-      return _tabs[key];
-    }
-  });
 });
\ No newline at end of file
--- a/devtools/client/debugger/new/src/actions/sources/moz.build
+++ b/devtools/client/debugger/new/src/actions/sources/moz.build
@@ -9,10 +9,9 @@ DIRS += [
 
 DevToolsModules(
     'blackbox.js',
     'index.js',
     'loadSourceText.js',
     'newSources.js',
     'prettyPrint.js',
     'select.js',
-    'tabs.js',
 )
--- a/devtools/client/debugger/new/src/actions/sources/newSources.js
+++ b/devtools/client/debugger/new/src/actions/sources/newSources.js
@@ -48,18 +48,24 @@ function loadSourceMaps(sources) {
   return async function ({
     dispatch,
     sourceMaps
   }) {
     if (!sourceMaps) {
       return;
     }
 
-    const originalSources = await Promise.all(sources.map(source => dispatch(loadSourceMap(source.id))));
-    await dispatch(newSources((0, _lodash.flatten)(originalSources)));
+    let originalSources = await Promise.all(sources.map(({
+      id
+    }) => dispatch(loadSourceMap(id))));
+    originalSources = (0, _lodash.flatten)(originalSources).filter(Boolean);
+
+    if (originalSources.length > 0) {
+      await dispatch(newSources(originalSources));
+    }
   };
 }
 /**
  * @memberof actions/sources
  * @static
  */
 
 
@@ -180,30 +186,30 @@ function newSource(source) {
   };
 }
 
 function newSources(sources) {
   return async ({
     dispatch,
     getState
   }) => {
-    const filteredSources = sources.filter(source => source && !(0, _selectors.getSource)(getState(), source.id));
+    sources = sources.filter(source => !(0, _selectors.getSource)(getState(), source.id));
 
-    if (filteredSources.length == 0) {
+    if (sources.length == 0) {
       return;
     }
 
     dispatch({
       type: "ADD_SOURCES",
-      sources: filteredSources
+      sources: sources
     });
 
-    for (const source of filteredSources) {
+    for (const source of sources) {
       dispatch(checkSelectedSource(source.id));
       dispatch(checkPendingBreakpoints(source.id));
     }
 
-    await dispatch(loadSourceMaps(filteredSources)); // We would like to restore the blackboxed state
+    await dispatch(loadSourceMaps(sources)); // We would like to restore the blackboxed state
     // after loading all states to make sure the correctness.
 
-    await dispatch(restoreBlackBoxedSources(filteredSources));
+    await dispatch(restoreBlackBoxedSources(sources));
   };
 }
\ No newline at end of file
--- a/devtools/client/debugger/new/src/actions/sources/select.js
+++ b/devtools/client/debugger/new/src/actions/sources/select.js
@@ -15,17 +15,17 @@ exports.jumpToMappedSelectedLocation = j
 var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
 
 var _ast = require("../ast");
 
 var _ui = require("../ui");
 
 var _prettyPrint = require("./prettyPrint");
 
-var _tabs = require("./tabs");
+var _tabs = require("../tabs");
 
 var _loadSourceText = require("./loadSourceText");
 
 var _prefs = require("../../utils/prefs");
 
 var _source = require("../../utils/source");
 
 var _location = require("../../utils/location");
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/actions/tabs.js
@@ -0,0 +1,91 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.addTab = addTab;
+exports.moveTab = moveTab;
+exports.closeTab = closeTab;
+exports.closeTabs = closeTabs;
+
+var _editor = require("../utils/editor/index");
+
+var _sources = require("./sources/index");
+
+var _selectors = require("../selectors/index");
+
+/* 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/>. */
+
+/**
+ * Redux actions for the editor tabs
+ * @module actions/tabs
+ */
+function addTab(url, tabIndex) {
+  return {
+    type: "ADD_TAB",
+    url,
+    tabIndex
+  };
+}
+
+function moveTab(url, tabIndex) {
+  return {
+    type: "MOVE_TAB",
+    url,
+    tabIndex
+  };
+}
+/**
+ * @memberof actions/tabs
+ * @static
+ */
+
+
+function closeTab(url) {
+  return ({
+    dispatch,
+    getState,
+    client
+  }) => {
+    (0, _editor.removeDocument)(url);
+    const tabs = (0, _selectors.removeSourceFromTabList)((0, _selectors.getSourceTabs)(getState()), url);
+    const sourceId = (0, _selectors.getNewSelectedSourceId)(getState(), tabs);
+    dispatch({
+      type: "CLOSE_TAB",
+      url,
+      tabs
+    });
+    dispatch((0, _sources.selectSource)(sourceId));
+  };
+}
+/**
+ * @memberof actions/tabs
+ * @static
+ */
+
+
+function closeTabs(urls) {
+  return ({
+    dispatch,
+    getState,
+    client
+  }) => {
+    urls.forEach(url => {
+      const source = (0, _selectors.getSourceByURL)(getState(), url);
+
+      if (source) {
+        (0, _editor.removeDocument)(source.id);
+      }
+    });
+    const tabs = (0, _selectors.removeSourcesFromTabList)((0, _selectors.getSourceTabs)(getState()), urls);
+    dispatch({
+      type: "CLOSE_TABS",
+      urls,
+      tabs
+    });
+    const sourceId = (0, _selectors.getNewSelectedSourceId)(getState(), tabs);
+    dispatch((0, _sources.selectSource)(sourceId));
+  };
+}
\ No newline at end of file
--- a/devtools/client/debugger/new/src/components/Editor/Tab.js
+++ b/devtools/client/debugger/new/src/components/Editor/Tab.js
@@ -160,17 +160,17 @@ class Tab extends _react.PureComponent {
     const className = (0, _classnames2.default)("source-tab", {
       active,
       pretty: isPrettyCode
     });
     const path = (0, _source.getDisplayPath)(source, tabSources);
     return _react2.default.createElement("div", {
       className: className,
       key: sourceId,
-      onMouseUp: handleTabClick,
+      onClick: handleTabClick,
       onContextMenu: e => this.onTabContextMenu(e, sourceId),
       title: (0, _source.getFileURL)(source)
     }, _react2.default.createElement(_SourceIcon2.default, {
       source: source,
       shouldHide: icon => ["file", "javascript"].includes(icon)
     }), _react2.default.createElement("div", {
       className: "filename"
     }, (0, _source.getTruncatedFileName)(source), path && _react2.default.createElement("span", null, `../${path}/..`)), _react2.default.createElement(_Button.CloseButton, {
--- a/devtools/client/debugger/new/src/components/Editor/index.js
+++ b/devtools/client/debugger/new/src/components/Editor/index.js
@@ -177,17 +177,17 @@ class Editor extends _react.PureComponen
       }
 
       if (gutter === "CodeMirror-foldgutter") {
         return;
       }
 
       const sourceLine = (0, _editor.toSourceLine)(selectedSource.id, line);
 
-      if (ev.altKey) {
+      if (ev.metaKey) {
         return continueToHere(sourceLine);
       }
 
       if (ev.shiftKey) {
         return addOrToggleDisabledBreakpoint(sourceLine);
       }
 
       return toggleBreakpointsAtLine(sourceLine);
@@ -418,17 +418,18 @@ class Editor extends _react.PureComponen
     } = this.state;
 
     if (!editor || !nextProps.selectedSource || !nextProps.selectedLocation || !nextProps.selectedLocation.line || !(0, _source.isLoaded)(nextProps.selectedSource)) {
       return false;
     }
 
     const isFirstLoad = (!selectedSource || !(0, _source.isLoaded)(selectedSource)) && (0, _source.isLoaded)(nextProps.selectedSource);
     const locationChanged = selectedLocation !== nextProps.selectedLocation;
-    return isFirstLoad || locationChanged;
+    const symbolsChanged = nextProps.symbols != this.props.symbols;
+    return isFirstLoad || locationChanged || symbolsChanged;
   }
 
   scrollToLocation(nextProps) {
     const {
       editor
     } = this.state;
     const {
       selectedLocation,
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
@@ -157,18 +157,17 @@ class SourcesTree extends _react.Compone
       className: "sources-clear-root-container"
     }, _react2.default.createElement("button", {
       className: "sources-clear-root",
       onClick: () => this.props.clearProjectDirectoryRoot(),
       title: L10N.getStr("removeDirectoryRoot.label")
     }, _react2.default.createElement(_Svg2.default, {
       name: "home"
     }), _react2.default.createElement(_Svg2.default, {
-      name: "breadcrumb",
-      "class": true
+      name: "breadcrumb"
     }), _react2.default.createElement("span", {
       className: "sources-clear-root-label"
     }, rootLabel)));
   }
 
   renderTree() {
     const {
       expanded
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
@@ -120,17 +120,17 @@ class SourceTreeItem extends _react.Comp
     if (item.path === "webpack://") {
       return _react2.default.createElement(_Svg2.default, {
         name: "webpack"
       });
     } else if (item.path === "ng://") {
       return _react2.default.createElement(_Svg2.default, {
         name: "angular"
       });
-    } else if (item.path === "moz-extension://") {
+    } else if (item.path.startsWith("moz-extension://") && depth === 0) {
       return _react2.default.createElement("img", {
         className: "extension"
       });
     }
 
     if (depth === 0 && projectRoot === "") {
       return _react2.default.createElement("img", {
         className: (0, _classnames2.default)("domain", {
@@ -171,19 +171,16 @@ class SourceTreeItem extends _react.Comp
   renderItemName(name) {
     switch (name) {
       case "ng://":
         return "Angular";
 
       case "webpack://":
         return "Webpack";
 
-      case "moz-extension://":
-        return L10N.getStr("extensionsText");
-
       default:
         return name;
     }
   }
 
   render() {
     const {
       item,
--- a/devtools/client/debugger/new/src/reducers/index.js
+++ b/devtools/client/debugger/new/src/reducers/index.js
@@ -11,16 +11,20 @@ var _expressions2 = _interopRequireDefau
 var _eventListeners = require("./event-listeners");
 
 var _eventListeners2 = _interopRequireDefault(_eventListeners);
 
 var _sources = require("./sources");
 
 var _sources2 = _interopRequireDefault(_sources);
 
+var _tabs = require("./tabs");
+
+var _tabs2 = _interopRequireDefault(_tabs);
+
 var _breakpoints = require("./breakpoints");
 
 var _breakpoints2 = _interopRequireDefault(_breakpoints);
 
 var _pendingBreakpoints = require("./pending-breakpoints");
 
 var _pendingBreakpoints2 = _interopRequireDefault(_pendingBreakpoints);
 
@@ -77,16 +81,17 @@ function _interopRequireDefault(obj) { r
 /**
  * Reducer index
  * @module reducers/index
  */
 exports.default = {
   expressions: _expressions2.default,
   eventListeners: _eventListeners2.default,
   sources: _sources2.default,
+  tabs: _tabs2.default,
   breakpoints: _breakpoints2.default,
   pendingBreakpoints: _pendingBreakpoints2.default,
   asyncRequests: _asyncRequests2.default,
   pause: _pause2.default,
   ui: _ui2.default,
   fileSearch: _fileSearch2.default,
   ast: _ast2.default,
   coverage: _coverage2.default,
--- a/devtools/client/debugger/new/src/reducers/moz.build
+++ b/devtools/client/debugger/new/src/reducers/moz.build
@@ -19,10 +19,11 @@ DevToolsModules(
     'index.js',
     'pause.js',
     'pending-breakpoints.js',
     'project-text-search.js',
     'quick-open.js',
     'replay.js',
     'source-tree.js',
     'sources.js',
+    'tabs.js',
     'ui.js',
 )
--- a/devtools/client/debugger/new/src/reducers/sources.js
+++ b/devtools/client/debugger/new/src/reducers/sources.js
@@ -1,62 +1,52 @@
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.getSelectedSource = exports.getSelectedLocation = exports.getSourcesForTabs = exports.getSourceTabs = exports.getTabs = undefined;
+exports.getSelectedSource = exports.getSelectedLocation = exports.getSourceCount = undefined;
 exports.initialSourcesState = initialSourcesState;
 exports.createSource = createSource;
-exports.removeSourceFromTabList = removeSourceFromTabList;
-exports.removeSourcesFromTabList = removeSourcesFromTabList;
 exports.getBlackBoxList = getBlackBoxList;
-exports.getNewSelectedSourceId = getNewSelectedSourceId;
 exports.getSource = getSource;
 exports.getSourceFromId = getSourceFromId;
 exports.getSourceByURL = getSourceByURL;
 exports.getGeneratedSource = getGeneratedSource;
 exports.getPendingSelectedLocation = getPendingSelectedLocation;
 exports.getPrettySource = getPrettySource;
 exports.hasPrettySource = hasPrettySource;
+exports.getSourceByUrlInSources = getSourceByUrlInSources;
 exports.getSourceInSources = getSourceInSources;
 exports.getSources = getSources;
+exports.getUrls = getUrls;
 exports.getSourceList = getSourceList;
-exports.getSourceCount = getSourceCount;
 
 var _reselect = require("devtools/client/debugger/new/dist/vendors").vendored["reselect"];
 
-var _lodashMove = require("devtools/client/debugger/new/dist/vendors").vendored["lodash-move"];
-
-var _lodashMove2 = _interopRequireDefault(_lodashMove);
-
 var _source = require("../utils/source");
 
 var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
 
-var _lodash = require("devtools/client/shared/vendor/lodash");
-
 var _prefs = require("../utils/prefs");
 
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
 /* 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 reducer
  * @module reducers/sources
  */
 function initialSourcesState() {
   return {
     sources: {},
+    urls: {},
     selectedLocation: undefined,
-    pendingSelectedLocation: _prefs.prefs.pendingSelectedLocation,
-    tabs: restoreTabs()
+    pendingSelectedLocation: _prefs.prefs.pendingSelectedLocation
   };
 }
 
 function createSource(source) {
   return {
     id: undefined,
     url: undefined,
     sourceMapURL: undefined,
@@ -120,38 +110,16 @@ function update(state = initialSourcesSt
         url: action.url,
         line: action.line
       };
       _prefs.prefs.pendingSelectedLocation = location;
       return { ...state,
         pendingSelectedLocation: location
       };
 
-    case "ADD_TAB":
-      return { ...state,
-        tabs: updateTabList(state.tabs, action.url)
-      };
-
-    case "MOVE_TAB":
-      return { ...state,
-        tabs: updateTabList(state.tabs, action.url, action.tabIndex)
-      };
-
-    case "CLOSE_TAB":
-      _prefs.prefs.tabs = action.tabs;
-      return { ...state,
-        tabs: action.tabs
-      };
-
-    case "CLOSE_TABS":
-      _prefs.prefs.tabs = action.tabs;
-      return { ...state,
-        tabs: action.tabs
-      };
-
     case "LOAD_SOURCE_TEXT":
       return setSourceTextProps(state, action);
 
     case "BLACKBOX":
       if (action.status === "done") {
         const {
           id,
           url
@@ -223,55 +191,28 @@ function updateSource(state, source) {
   if (!source.id) {
     return state;
   }
 
   const existingSource = state.sources[source.id];
   const updatedSource = existingSource ? { ...existingSource,
     ...source
   } : createSource(source);
+  const existingUrls = state.urls[source.url];
+  const urls = existingUrls ? [...existingUrls, source.id] : [source.id];
   return { ...state,
     sources: { ...state.sources,
       [source.id]: updatedSource
+    },
+    urls: { ...state.urls,
+      [source.url]: urls
     }
   };
 }
 
-function removeSourceFromTabList(tabs, url) {
-  return tabs.filter(tab => tab !== url);
-}
-
-function removeSourcesFromTabList(tabs, urls) {
-  return urls.reduce((t, url) => removeSourceFromTabList(t, url), tabs);
-}
-
-function restoreTabs() {
-  const prefsTabs = _prefs.prefs.tabs || [];
-  return prefsTabs;
-}
-/**
- * Adds the new source to the tab list if it is not already there
- * @memberof reducers/sources
- * @static
- */
-
-
-function updateTabList(tabs, url, newIndex) {
-  const currentIndex = tabs.indexOf(url);
-
-  if (currentIndex === -1) {
-    tabs = [url, ...tabs];
-  } else if (newIndex !== undefined) {
-    tabs = (0, _lodashMove2.default)(tabs, currentIndex, newIndex);
-  }
-
-  _prefs.prefs.tabs = tabs;
-  return tabs;
-}
-
 function updateBlackBoxList(url, isBlackBoxed) {
   const tabs = getBlackBoxList();
   const i = tabs.indexOf(url);
 
   if (i >= 0) {
     if (!isBlackBoxed) {
       tabs.splice(i, 1);
     }
@@ -279,69 +220,16 @@ function updateBlackBoxList(url, isBlack
     tabs.push(url);
   }
 
   _prefs.prefs.tabsBlackBoxed = tabs;
 }
 
 function getBlackBoxList() {
   return _prefs.prefs.tabsBlackBoxed || [];
-}
-/**
- * Gets the next tab to select when a tab closes. Heuristics:
- * 1. if the selected tab is available, it remains selected
- * 2. if it is gone, the next available tab to the left should be active
- * 3. if the first tab is active and closed, select the second tab
- *
- * @memberof reducers/sources
- * @static
- */
-
-
-function getNewSelectedSourceId(state, availableTabs) {
-  const selectedLocation = state.sources.selectedLocation;
-
-  if (!selectedLocation) {
-    return "";
-  }
-
-  const selectedTab = getSource(state, selectedLocation.sourceId);
-
-  if (!selectedTab) {
-    return "";
-  }
-
-  if (availableTabs.includes(selectedTab.url)) {
-    const sources = state.sources.sources;
-
-    if (!sources) {
-      return "";
-    }
-
-    const selectedSource = getSourceByURL(state, selectedTab.url);
-
-    if (selectedSource) {
-      return selectedSource.id;
-    }
-
-    return "";
-  }
-
-  const tabUrls = state.sources.tabs;
-  const leftNeighborIndex = Math.max(tabUrls.indexOf(selectedTab.url) - 1, 0);
-  const lastAvailbleTabIndex = availableTabs.length - 1;
-  const newSelectedTabIndex = Math.min(leftNeighborIndex, lastAvailbleTabIndex);
-  const availableTab = availableTabs[newSelectedTabIndex];
-  const tabSource = getSourceByUrlInSources(state.sources.sources, availableTab);
-
-  if (tabSource) {
-    return tabSource.id;
-  }
-
-  return "";
 } // Selectors
 // Unfortunately, it's really hard to make these functions accept just
 // the state that we care about and still type it with Flow. The
 // problem is that we want to re-export all selectors from a single
 // module for the UI, and all of those selectors should take the
 // top-level app state, so we'd have to "wrap" them to automatically
 // pick off the piece of state we're interested in. It's impossible
 // (right now) to type those wrapped functions.
@@ -353,17 +241,17 @@ function getSource(state, id) {
   return getSourceInSources(getSources(state), id);
 }
 
 function getSourceFromId(state, id) {
   return getSourcesState(state).sources[id];
 }
 
 function getSourceByURL(state, url) {
-  return getSourceByUrlInSources(state.sources.sources, url);
+  return getSourceByUrlInSources(getSources(state), getUrls(state), url);
 }
 
 function getGeneratedSource(state, source) {
   if (!(0, _devtoolsSourceMap.isOriginalId)(source.id)) {
     return source;
   }
 
   return getSourceFromId(state, (0, _devtoolsSourceMap.originalToGeneratedId)(source.id));
@@ -382,45 +270,51 @@ function getPrettySource(state, id) {
 
   return getSourceByURL(state, (0, _source.getPrettySourceURL)(source.url));
 }
 
 function hasPrettySource(state, id) {
   return !!getPrettySource(state, id);
 }
 
-function getSourceByUrlInSources(sources, url) {
-  if (!url) {
+function getSourceByUrlInSources(sources, urls, url) {
+  const foundSources = getSourcesByUrlInSources(sources, urls, url);
+
+  if (!foundSources) {
     return null;
   }
 
-  return (0, _lodash.find)(sources, source => source.url === url);
+  return foundSources[0];
+}
+
+function getSourcesByUrlInSources(sources, urls, url) {
+  if (!url || !urls[url]) {
+    return [];
+  }
+
+  return urls[url].map(id => sources[id]);
 }
 
 function getSourceInSources(sources, id) {
   return sources[id];
 }
 
 function getSources(state) {
   return state.sources.sources;
 }
 
+function getUrls(state) {
+  return state.sources.urls;
+}
+
 function getSourceList(state) {
   return Object.values(getSources(state));
 }
 
-function getSourceCount(state) {
-  return Object.keys(getSources(state)).length;
-}
-
-const getTabs = exports.getTabs = (0, _reselect.createSelector)(getSourcesState, sources => sources.tabs);
-const getSourceTabs = exports.getSourceTabs = (0, _reselect.createSelector)(getTabs, getSources, (tabs, sources) => tabs.filter(tab => getSourceByUrlInSources(sources, tab)));
-const getSourcesForTabs = exports.getSourcesForTabs = (0, _reselect.createSelector)(getSourceTabs, getSources, (tabs, sources) => {
-  return tabs.map(tab => getSourceByUrlInSources(sources, tab)).filter(source => source);
-});
+const getSourceCount = exports.getSourceCount = (0, _reselect.createSelector)(getSources, sources => Object.keys(sources).length);
 const getSelectedLocation = exports.getSelectedLocation = (0, _reselect.createSelector)(getSourcesState, sources => sources.selectedLocation);
 const getSelectedSource = exports.getSelectedSource = (0, _reselect.createSelector)(getSelectedLocation, getSources, (selectedLocation, sources) => {
   if (!selectedLocation) {
     return;
   }
 
   return sources[selectedLocation.sourceId];
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/reducers/tabs.js
@@ -0,0 +1,143 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.getSourcesForTabs = exports.getSourceTabs = exports.getTabs = undefined;
+exports.removeSourceFromTabList = removeSourceFromTabList;
+exports.removeSourcesFromTabList = removeSourcesFromTabList;
+exports.getNewSelectedSourceId = getNewSelectedSourceId;
+
+var _reselect = require("devtools/client/debugger/new/dist/vendors").vendored["reselect"];
+
+var _lodashMove = require("devtools/client/debugger/new/dist/vendors").vendored["lodash-move"];
+
+var _lodashMove2 = _interopRequireDefault(_lodashMove);
+
+var _prefs = require("../utils/prefs");
+
+var _sources = require("./sources");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/* 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/>. */
+
+/**
+ * Tabs reducer
+ * @module reducers/tabs
+ */
+function update(state = _prefs.prefs.tabs || [], action) {
+  switch (action.type) {
+    case "ADD_TAB":
+      return updateTabList(state, action.url);
+
+    case "MOVE_TAB":
+      return updateTabList(state, action.url, action.tabIndex);
+
+    case "CLOSE_TAB":
+    case "CLOSE_TABS":
+      _prefs.prefs.tabs = action.tabs;
+      return action.tabs;
+
+    default:
+      return state;
+  }
+}
+
+function removeSourceFromTabList(tabs, url) {
+  return tabs.filter(tab => tab !== url);
+}
+
+function removeSourcesFromTabList(tabs, urls) {
+  return urls.reduce((t, url) => removeSourceFromTabList(t, url), tabs);
+}
+/**
+ * Adds the new source to the tab list if it is not already there
+ * @memberof reducers/tabs
+ * @static
+ */
+
+
+function updateTabList(tabs, url, newIndex) {
+  const currentIndex = tabs.indexOf(url);
+
+  if (currentIndex === -1) {
+    tabs = [url, ...tabs];
+  } else if (newIndex !== undefined) {
+    tabs = (0, _lodashMove2.default)(tabs, currentIndex, newIndex);
+  }
+
+  _prefs.prefs.tabs = tabs;
+  return tabs;
+}
+/**
+ * Gets the next tab to select when a tab closes. Heuristics:
+ * 1. if the selected tab is available, it remains selected
+ * 2. if it is gone, the next available tab to the left should be active
+ * 3. if the first tab is active and closed, select the second tab
+ *
+ * @memberof reducers/tabs
+ * @static
+ */
+
+
+function getNewSelectedSourceId(state, availableTabs) {
+  const selectedLocation = state.sources.selectedLocation;
+
+  if (!selectedLocation) {
+    return "";
+  }
+
+  const selectedTab = (0, _sources.getSource)(state, selectedLocation.sourceId);
+
+  if (!selectedTab) {
+    return "";
+  }
+
+  if (availableTabs.includes(selectedTab.url)) {
+    const sources = state.sources.sources;
+
+    if (!sources) {
+      return "";
+    }
+
+    const selectedSource = (0, _sources.getSourceByURL)(state, selectedTab.url);
+
+    if (selectedSource) {
+      return selectedSource.id;
+    }
+
+    return "";
+  }
+
+  const tabUrls = state.tabs;
+  const leftNeighborIndex = Math.max(tabUrls.indexOf(selectedTab.url) - 1, 0);
+  const lastAvailbleTabIndex = availableTabs.length - 1;
+  const newSelectedTabIndex = Math.min(leftNeighborIndex, lastAvailbleTabIndex);
+  const availableTab = availableTabs[newSelectedTabIndex];
+  const tabSource = (0, _sources.getSourceByUrlInSources)((0, _sources.getSources)(state), (0, _sources.getUrls)(state), availableTab);
+
+  if (tabSource) {
+    return tabSource.id;
+  }
+
+  return "";
+} // Selectors
+// Unfortunately, it's really hard to make these functions accept just
+// the state that we care about and still type it with Flow. The
+// problem is that we want to re-export all selectors from a single
+// module for the UI, and all of those selectors should take the
+// top-level app state, so we'd have to "wrap" them to automatically
+// pick off the piece of state we're interested in. It's impossible
+// (right now) to type those wrapped functions.
+
+
+const getTabs = exports.getTabs = state => state.tabs;
+
+const getSourceTabs = exports.getSourceTabs = (0, _reselect.createSelector)(getTabs, _sources.getSources, _sources.getUrls, (tabs, sources, urls) => tabs.filter(tab => (0, _sources.getSourceByUrlInSources)(sources, urls, tab)));
+const getSourcesForTabs = exports.getSourcesForTabs = (0, _reselect.createSelector)(getSourceTabs, _sources.getSources, _sources.getUrls, (tabs, sources, urls) => {
+  return tabs.map(tab => (0, _sources.getSourceByUrlInSources)(sources, urls, tab)).filter(source => source);
+});
+exports.default = update;
\ No newline at end of file
--- a/devtools/client/debugger/new/src/selectors/index.js
+++ b/devtools/client/debugger/new/src/selectors/index.js
@@ -23,16 +23,28 @@ Object.keys(_sources).forEach(function (
   Object.defineProperty(exports, key, {
     enumerable: true,
     get: function () {
       return _sources[key];
     }
   });
 });
 
+var _tabs = require("../reducers/tabs");
+
+Object.keys(_tabs).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _tabs[key];
+    }
+  });
+});
+
 var _pause = require("../reducers/pause");
 
 Object.keys(_pause).forEach(function (key) {
   if (key === "default" || key === "__esModule") return;
   Object.defineProperty(exports, key, {
     enumerable: true,
     get: function () {
       return _pause[key];
--- a/devtools/client/debugger/new/src/utils/editor/index.js
+++ b/devtools/client/debugger/new/src/utils/editor/index.js
@@ -188,19 +188,23 @@ function scrollToColumn(codeMirror, line
 
 function isVisible(codeMirror, top, left) {
   function withinBounds(x, min, max) {
     return x >= min && x <= max;
   }
 
   const scrollArea = codeMirror.getScrollInfo();
   const charWidth = codeMirror.defaultCharWidth();
-  const inXView = withinBounds(left, scrollArea.left, scrollArea.left + (scrollArea.clientWidth - 30) - charWidth);
   const fontHeight = codeMirror.defaultTextHeight();
-  const inYView = withinBounds(top, scrollArea.top, scrollArea.top + scrollArea.clientHeight - fontHeight);
+  const {
+    scrollTop,
+    scrollLeft
+  } = codeMirror.doc;
+  const inXView = withinBounds(left, scrollLeft, scrollLeft + (scrollArea.clientWidth - 30) - charWidth);
+  const inYView = withinBounds(top, scrollTop, scrollTop + scrollArea.clientHeight - fontHeight);
   return inXView && inYView;
 }
 
 function markText(_editor, className, {
   start,
   end
 }) {
   return _editor.codeMirror.markText({
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-console-async.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-console-async.js
@@ -23,30 +23,42 @@ function getSplitConsole(dbg) {
 
 function findMessages(win, query) {
   return Array.prototype.filter.call(
     win.document.querySelectorAll(".message"),
     e => e.innerText.includes(query)
   )
 }
 
+async function hasMessage(dbg, msg) {
+  const webConsole = await dbg.toolbox.getPanel("webconsole")
+  return waitFor(async () => findMessages(
+    webConsole._frameWindow,
+    msg
+  ).length > 0)
+}
+
 add_task(async function() {
   Services.prefs.setBoolPref("devtools.toolbox.splitconsoleEnabled", true);
+  Services.prefs.setBoolPref("devtools.map-await-expression", true);
+
   const dbg = await initDebugger("doc-script-switching.html");
 
   await selectSource(dbg, "switching-01");
 
   // open the console
   await getSplitConsole(dbg);
   ok(dbg.toolbox.splitConsole, "Split console is shown.");
 
   const webConsole = await dbg.toolbox.getPanel("webconsole")
   const jsterm = webConsole.hud.jsterm;
 
-  await jsterm.execute(`let sleep = async (time, v) => new Promise(
-    res => setTimeout(() => res(v+'!!!'), time)
-  )`);
+  await jsterm.execute(`
+    let sleep = async (time, v) => new Promise(
+      res => setTimeout(() => res(v+'!!!'), time)
+    )
+  `);
 
-  await jsterm.execute(`await sleep(200, "DONE")`)
+  await hasMessage(dbg, "sleep");
 
-  await waitFor(async () => findMessages(webConsole._frameWindow, "DONE!!!").length > 0)
-
+  await jsterm.execute(`await sleep(200, "DONE")`);
+  await hasMessage(dbg, "DONE!!!");
 });