Bug 910955 - Implement a live WebGL shader editor, r=dcamp
authorVictor Porof <vporof@mozilla.com>
Fri, 25 Oct 2013 10:18:41 +0300
changeset 166039 3097d7118a1863ae264a1edc02d4f69dad9ac226
parent 166038 273efc178016ad3c5c57cce15cf386938ede035d
child 166040 9ba511439633d4ea81a27f51c858138bcaf6d5c1
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs910955
milestone27.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 910955 - Implement a live WebGL shader editor, r=dcamp
browser/app/profile/firefox.js
browser/devtools/debugger/moz.build
browser/devtools/jar.mn
browser/devtools/main.js
browser/devtools/shadereditor/moz.build
browser/devtools/shadereditor/panel.js
browser/devtools/shadereditor/shadereditor.js
browser/devtools/shadereditor/shadereditor.xul
browser/devtools/shadereditor/test/browser.ini
browser/devtools/shadereditor/test/browser_se_aaa_run_first_leaktest.js
browser/devtools/shadereditor/test/browser_se_editors-contents.js
browser/devtools/shadereditor/test/browser_se_editors-lazy-init.js
browser/devtools/shadereditor/test/browser_se_first-run.js
browser/devtools/shadereditor/test/browser_se_navigation.js
browser/devtools/shadereditor/test/browser_se_programs-blackbox.js
browser/devtools/shadereditor/test/browser_se_programs-cache.js
browser/devtools/shadereditor/test/browser_se_programs-highlight.js
browser/devtools/shadereditor/test/browser_se_programs-list.js
browser/devtools/shadereditor/test/browser_se_shaders-edit-01.js
browser/devtools/shadereditor/test/browser_se_shaders-edit-02.js
browser/devtools/shadereditor/test/browser_se_shaders-edit-03.js
browser/devtools/shadereditor/test/head.js
browser/devtools/shared/telemetry.js
browser/devtools/shared/widgets/ViewHelpers.jsm
browser/devtools/sourceeditor/editor.js
browser/locales/en-US/chrome/browser/devtools/shadereditor.dtd
browser/locales/en-US/chrome/browser/devtools/shadereditor.properties
browser/locales/jar.mn
browser/themes/linux/devtools/shadereditor.css
browser/themes/linux/jar.mn
browser/themes/osx/devtools/shadereditor.css
browser/themes/osx/jar.mn
browser/themes/shared/devtools/shadereditor.inc.css
browser/themes/windows/devtools/shadereditor.css
browser/themes/windows/jar.mn
toolkit/components/telemetry/Histograms.json
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1161,16 +1161,19 @@ pref("devtools.tilt.outro_transition", t
 // Setting this preference to 0 will not clear any recent files, but rather hide
 // the 'Open Recent'-menu.
 pref("devtools.scratchpad.recentFilesMax", 10);
 
 // Enable the Style Editor.
 pref("devtools.styleeditor.enabled", true);
 pref("devtools.styleeditor.transitions", true);
 
+// Enable the Shader Editor.
+pref("devtools.shadereditor.enabled", false);
+
 // Enable tools for Chrome development.
 pref("devtools.chrome.enabled", false);
 
 // Default theme ("dark" or "light")
 pref("devtools.theme", "light");
 
 // Display the introductory text
 pref("devtools.gcli.hideIntro", false);
--- a/browser/devtools/debugger/moz.build
+++ b/browser/devtools/debugger/moz.build
@@ -1,9 +1,8 @@
-# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += ['test']
 
 JS_MODULES_PATH = 'modules/devtools/debugger'
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -46,16 +46,18 @@ browser.jar:
     content/browser/devtools/codemirror/mozilla.css                    (sourceeditor/codemirror/mozilla.css)
 *   content/browser/devtools/source-editor-overlay.xul                 (sourceeditor/source-editor-overlay.xul)
     content/browser/devtools/debugger.xul                              (debugger/debugger.xul)
     content/browser/devtools/debugger.css                              (debugger/debugger.css)
     content/browser/devtools/debugger-controller.js                    (debugger/debugger-controller.js)
     content/browser/devtools/debugger-view.js                          (debugger/debugger-view.js)
     content/browser/devtools/debugger-toolbar.js                       (debugger/debugger-toolbar.js)
     content/browser/devtools/debugger-panes.js                         (debugger/debugger-panes.js)
+    content/browser/devtools/shadereditor.xul                          (shadereditor/shadereditor.xul)
+    content/browser/devtools/shadereditor.js                           (shadereditor/shadereditor.js)
     content/browser/devtools/profiler.xul                              (profiler/profiler.xul)
     content/browser/devtools/cleopatra.html                            (profiler/cleopatra/cleopatra.html)
     content/browser/devtools/profiler/cleopatra/css/ui.css             (profiler/cleopatra/css/ui.css)
     content/browser/devtools/profiler/cleopatra/css/tree.css           (profiler/cleopatra/css/tree.css)
     content/browser/devtools/profiler/cleopatra/css/devtools.css       (profiler/cleopatra/css/devtools.css)
     content/browser/devtools/profiler/cleopatra/js/strings.js          (profiler/cleopatra/js/strings.js)
     content/browser/devtools/profiler/cleopatra/js/parser.js           (profiler/cleopatra/js/parser.js)
     content/browser/devtools/profiler/cleopatra/js/parserWorker.js     (profiler/cleopatra/js/parserWorker.js)
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -22,33 +22,36 @@ loader.lazyGetter(this, "osString", () =
 let events = require("sdk/system/events");
 
 // Panels
 loader.lazyGetter(this, "OptionsPanel", () => require("devtools/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel);
 loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel);
 loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/debugger-panel").DebuggerPanel);
 loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm");
+loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel);
 loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel"));
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/netmonitor-panel").NetMonitorPanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
 
 // Strings
 const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
 const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
 const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
 const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
+const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties";
 const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
 const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
 const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
 const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties";
 loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps));
 loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps));
 loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
 loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
+loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBundle(shaderEditorProps));
 loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
 loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
 loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
 loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
 
 let Tools = {};
 exports.Tools = Tools;
 
@@ -161,21 +164,40 @@ Tools.styleEditor = {
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new StyleEditorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
+Tools.shaderEditor = {
+  id: "shadereditor",
+  ordinal: 5,
+  visibilityswitch: "devtools.shadereditor.enabled",
+  icon: "chrome://browser/skin/devtools/tool-styleeditor.png",
+  url: "chrome://browser/content/devtools/shadereditor.xul",
+  label: l10n("ToolboxShaderEditor.label", shaderEditorStrings),
+  tooltip: l10n("ToolboxShaderEditor.tooltip", shaderEditorStrings),
+
+  isTargetSupported: function(target) {
+    return true;
+  },
+
+  build: function(iframeWindow, toolbox) {
+    let panel = new ShaderEditorPanel(iframeWindow, toolbox);
+    return panel.open();
+  }
+};
+
 Tools.jsprofiler = {
   id: "jsprofiler",
   accesskey: l10n("profiler.accesskey", profilerStrings),
   key: l10n("profiler2.commandkey", profilerStrings),
-  ordinal: 5,
+  ordinal: 6,
   modifiers: "shift",
   visibilityswitch: "devtools.profiler.enabled",
   icon: "chrome://browser/skin/devtools/tool-profiler.png",
   url: "chrome://browser/content/devtools/profiler.xul",
   label: l10n("profiler.label", profilerStrings),
   tooltip: l10n("profiler.tooltip2", profilerStrings),
   inMenu: true,
 
@@ -188,17 +210,17 @@ Tools.jsprofiler = {
     return panel.open();
   }
 };
 
 Tools.netMonitor = {
   id: "netmonitor",
   accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
   key: l10n("netmonitor.commandkey", netMonitorStrings),
-  ordinal: 6,
+  ordinal: 7,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   visibilityswitch: "devtools.netmonitor.enabled",
   icon: "chrome://browser/skin/devtools/tool-network.png",
   url: "chrome://browser/content/devtools/netmonitor.xul",
   label: l10n("netmonitor.label", netMonitorStrings),
   tooltip: l10n("netmonitor.tooltip", netMonitorStrings),
   inMenu: true,
 
@@ -209,17 +231,17 @@ Tools.netMonitor = {
   build: function(iframeWindow, toolbox) {
     let panel = new NetMonitorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
 Tools.scratchpad = {
   id: "scratchpad",
-  ordinal: 7,
+  ordinal: 8,
   visibilityswitch: "devtools.scratchpad.enabled",
   icon: "chrome://browser/skin/devtools/tool-scratchpad.png",
   url: "chrome://browser/content/devtools/scratchpad.xul",
   label: l10n("scratchpad.label", scratchpadStrings),
   tooltip: l10n("scratchpad.tooltip", scratchpadStrings),
   inMenu: false,
 
   isTargetSupported: function(target) {
@@ -229,20 +251,21 @@ Tools.scratchpad = {
   build: function(iframeWindow, toolbox) {
     let panel = new ScratchpadPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
 let defaultTools = [
   Tools.options,
+  Tools.webConsole,
+  Tools.inspector,
+  Tools.jsdebugger,
   Tools.styleEditor,
-  Tools.webConsole,
-  Tools.jsdebugger,
-  Tools.inspector,
+  Tools.shaderEditor,
   Tools.jsprofiler,
   Tools.netMonitor,
   Tools.scratchpad
 ];
 
 exports.defaultTools = defaultTools;
 
 for (let definition of defaultTools) {
--- a/browser/devtools/shadereditor/moz.build
+++ b/browser/devtools/shadereditor/moz.build
@@ -1,6 +1,13 @@
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += ['test']
+
+JS_MODULES_PATH = 'modules/devtools/shadereditor'
+
+EXTRA_JS_MODULES += [
+    'panel.js'
+]
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/panel.js
@@ -0,0 +1,65 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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/. */
+"use strict";
+
+const { Cc, Ci, Cu, Cr } = require("chrome");
+const promise = require("sdk/core/promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { WebGLFront } = require("devtools/server/actors/webgl");
+
+function ShaderEditorPanel(iframeWindow, toolbox) {
+  this.panelWin = iframeWindow;
+  this._toolbox = toolbox;
+  this._destroyer = null;
+
+  EventEmitter.decorate(this);
+};
+
+exports.ShaderEditorPanel = ShaderEditorPanel;
+
+ShaderEditorPanel.prototype = {
+  open: function() {
+    let targetPromise;
+
+    // Local debugging needs to make the target remote.
+    if (!this.target.isRemote) {
+      targetPromise = this.target.makeRemote();
+    } else {
+      targetPromise = promise.resolve(this.target);
+    }
+
+    return targetPromise
+      .then(() => {
+        this.panelWin.gTarget = this.target;
+        this.panelWin.gFront = new WebGLFront(this.target.client, this.target.form);
+        return this.panelWin.startupShaderEditor();
+      })
+      .then(() => {
+        this.isReady = true;
+        this.emit("ready");
+        return this;
+      })
+      .then(null, function onError(aReason) {
+        Cu.reportError("ShaderEditorPanel open failed. " +
+                       aReason.error + ": " + aReason.message);
+      });
+  },
+
+  // DevToolPanel API
+
+  get target() this._toolbox.target,
+
+  destroy: function() {
+    // Make sure this panel is not already destroyed.
+    if (this._destroyer) {
+      return this._destroyer;
+    }
+
+    return this._destroyer = this.panelWin.shutdownShaderEditor().then(() => {
+      this.emit("destroyed");
+    });
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/shadereditor.js
@@ -0,0 +1,396 @@
+/* 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/. */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const promise = require("sdk/core/promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const Editor = require("devtools/sourceeditor/editor");
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+  // When the vertex and fragment sources were shown in the editor.
+  SOURCES_SHOWN: "ShaderEditor:SourcesShown",
+  // When a shader's source was edited and compiled via the editor.
+  SHADER_COMPILED: "ShaderEditor:ShaderCompiled"
+};
+
+const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
+const HIGHLIGHT_COLOR = [1, 0, 0, 1];
+const BLACKBOX_COLOR = [0, 0, 0, 0];
+const TYPING_MAX_DELAY = 500;
+const SHADERS_AUTOGROW_ITEMS = 4;
+const DEFAULT_EDITOR_CONFIG = {
+  mode: Editor.modes.text,
+  lineNumbers: true,
+  showAnnotationRuler: true
+};
+
+/**
+ * The current target and the WebGL Editor front, set by this tool's host.
+ */
+let gTarget, gFront;
+
+/**
+ * Initializes the shader editor controller and views.
+ */
+function startupShaderEditor() {
+  return promise.all([
+    EventsHandler.initialize(),
+    ShadersListView.initialize(),
+    ShadersEditorsView.initialize()
+  ]);
+}
+
+/**
+ * Destroys the shader editor controller and views.
+ */
+function shutdownShaderEditor() {
+  return promise.all([
+    EventsHandler.destroy(),
+    ShadersListView.destroy(),
+    ShadersEditorsView.destroy()
+  ]);
+}
+
+/**
+ * Functions handling target-related lifetime events.
+ */
+let EventsHandler = {
+  /**
+   * Listen for events emitted by the current tab target.
+   */
+  initialize: function() {
+    this._onWillNavigate = this._onWillNavigate.bind(this);
+    this._onProgramLinked = this._onProgramLinked.bind(this);
+    gTarget.on("will-navigate", this._onWillNavigate);
+    gFront.on("program-linked", this._onProgramLinked);
+
+  },
+
+  /**
+   * Remove events emitted by the current tab target.
+   */
+  destroy: function() {
+    gTarget.off("will-navigate", this._onWillNavigate);
+    gFront.off("program-linked", this._onProgramLinked);
+  },
+
+  /**
+   * Called for each location change in the debugged tab.
+   */
+  _onWillNavigate: function() {
+    gFront.setup();
+
+    ShadersListView.empty();
+    ShadersEditorsView.setText({ vs: "", fs: "" });
+    $("#reload-notice").hidden = true;
+    $("#waiting-notice").hidden = false;
+    $("#content").hidden = true;
+  },
+
+  /**
+   * Called every time a program was linked in the debugged tab.
+   */
+  _onProgramLinked: function(programActor) {
+    $("#waiting-notice").hidden = true;
+    $("#reload-notice").hidden = true;
+    $("#content").hidden = false;
+    ShadersListView.addProgram(programActor);
+  }
+};
+
+/**
+ * Functions handling the sources UI.
+ */
+let ShadersListView = Heritage.extend(WidgetMethods, {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: function() {
+    this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
+      showArrows: true,
+      showItemCheckboxes: true
+    });
+
+    this._onShaderSelect = this._onShaderSelect.bind(this);
+    this._onShaderCheck = this._onShaderCheck.bind(this);
+    this._onShaderMouseEnter = this._onShaderMouseEnter.bind(this);
+    this._onShaderMouseLeave = this._onShaderMouseLeave.bind(this);
+
+    this.widget.addEventListener("select", this._onShaderSelect, false);
+    this.widget.addEventListener("check", this._onShaderCheck, false);
+    this.widget.addEventListener("mouseenter", this._onShaderMouseEnter, true);
+    this.widget.addEventListener("mouseleave", this._onShaderMouseLeave, true);
+  },
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    this.widget.removeEventListener("select", this._onShaderSelect, false);
+    this.widget.removeEventListener("check", this._onShaderCheck, false);
+    this.widget.removeEventListener("mouseenter", this._onShaderMouseEnter, true);
+    this.widget.removeEventListener("mouseleave", this._onShaderMouseLeave, true);
+  },
+
+  /**
+   * Adds a program to this programs container.
+   *
+   * @param object programActor
+   *        The program actor coming from the active thread.
+   */
+  addProgram: function(programActor) {
+    // Currently, there's no good way of differentiating between programs
+    // in a way that helps humans. It will be a good idea to implement a
+    // standard of allowing debuggees to add some identifiable metadata to their
+    // program sources or instances.
+    let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount);
+
+    // Append a program item to this container.
+    this.push([label, ""], {
+      index: -1, /* specifies on which position should the item be appended */
+      relaxed: true, /* this container should allow dupes & degenerates */
+      attachment: {
+        programActor: programActor,
+        checkboxState: true,
+        checkboxTooltip: L10N.getStr("shadersList.blackboxLabel")
+      }
+    });
+
+    // Make sure there's always a selected item available.
+    if (!this.selectedItem) {
+      this.selectedIndex = 0;
+    }
+  },
+
+  /**
+   * The select listener for the sources container.
+   */
+  _onShaderSelect: function({ detail: sourceItem }) {
+    if (!sourceItem) {
+      return;
+    }
+    // The container is not empty and an actual item was selected.
+    let attachment = sourceItem.attachment;
+
+    function getShaders() {
+      return promise.all([
+        attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()),
+        attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader())
+      ]);
+    }
+    function getSources([vertexShaderActor, fragmentShaderActor]) {
+      return promise.all([
+        vertexShaderActor.getText(),
+        fragmentShaderActor.getText()
+      ]);
+    }
+    function showSources([vertexShaderText, fragmentShaderText]) {
+      ShadersEditorsView.setText({
+        vs: vertexShaderText,
+        fs: fragmentShaderText
+      });
+    }
+
+    getShaders().then(getSources).then(showSources).then(null, Cu.reportError);
+  },
+
+  /**
+   * The check listener for the sources container.
+   */
+  _onShaderCheck: function({ detail: { checked }, target }) {
+    let sourceItem = this.getItemForElement(target);
+    let attachment = sourceItem.attachment;
+    attachment.isBlackBoxed = !checked;
+    attachment.programActor[checked ? "unhighlight" : "highlight"](BLACKBOX_COLOR);
+  },
+
+  /**
+   * The mouseenter listener for the sources container.
+   */
+  _onShaderMouseEnter: function(e) {
+    let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
+    if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
+      sourceItem.attachment.programActor.highlight(HIGHLIGHT_COLOR);
+
+      if (e instanceof Event) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    }
+  },
+
+  /**
+   * The mouseleave listener for the sources container.
+   */
+  _onShaderMouseLeave: function(e) {
+    let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
+    if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
+      sourceItem.attachment.programActor.unhighlight();
+
+      if (e instanceof Event) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    }
+  }
+});
+
+/**
+ * Functions handling the editors displaying the vertex and fragment shaders.
+ */
+let ShadersEditorsView = {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: function() {
+    XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map());
+    this._vsFocused = this._onFocused.bind(this, "vs", "fs");
+    this._fsFocused = this._onFocused.bind(this, "fs", "vs");
+    this._vsChanged = this._onChanged.bind(this, "vs");
+    this._fsChanged = this._onChanged.bind(this, "fs");
+  },
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    this._toggleListeners("off");
+  },
+
+  /**
+   * Sets the text displayed in the vertex and fragment shader editors.
+   *
+   * @param object sources
+   *        An object containing the following properties
+   *          - vs: the vertex shader source code
+   *          - fs: the fragment shader source code
+   */
+  setText: function(sources) {
+    function setTextAndClearHistory(editor, text) {
+      editor.setText(text);
+      editor.clearHistory();
+    }
+
+    this._toggleListeners("off");
+    this._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs));
+    this._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs));
+    this._toggleListeners("on");
+
+    window.emit(EVENTS.SOURCES_SHOWN, sources);
+  },
+
+  /**
+   * Lazily initializes and returns a promise for an Editor instance.
+   *
+   * @param string type
+   *        Specifies for which shader type should an editor be retrieved,
+   *        either are "vs" for a vertex, or "fs" for a fragment shader.
+   */
+  _getEditor: function(type) {
+    if ($("#content").hidden) {
+      return promise.reject(null);
+    }
+    if (this._editorPromises.has(type)) {
+      return this._editorPromises.get(type);
+    }
+
+    let deferred = promise.defer();
+    this._editorPromises.set(type, deferred.promise);
+
+    // Initialize the source editor and store the newly created instance
+    // in the ether of a resolved promise's value.
+    let parent = $("#" + type +"-editor");
+    let editor = new Editor(DEFAULT_EDITOR_CONFIG);
+    editor.appendTo(parent).then(() => deferred.resolve(editor));
+
+    return deferred.promise;
+  },
+
+  /**
+   * Toggles all the event listeners for the editors either on or off.
+   *
+   * @param string flag
+   *        Either "on" to enable the event listeners, "off" to disable them.
+   */
+  _toggleListeners: function(flag) {
+    ["vs", "fs"].forEach(type => {
+      this._getEditor(type).then(editor => {
+        editor[flag]("focus", this["_" + type + "Focused"]);
+        editor[flag]("change", this["_" + type + "Changed"]);
+      });
+    });
+  },
+
+  /**
+   * The focus listener for a source editor.
+   *
+   * @param string focused
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   * @param string focused
+   *        The corresponding shader type for the other editor (e.g. "fs").
+   */
+  _onFocused: function(focused, unfocused) {
+    $("#" + focused + "-editor-label").setAttribute("selected", "");
+    $("#" + unfocused + "-editor-label").removeAttribute("selected");
+  },
+
+  /**
+   * The change listener for a source editor.
+   *
+   * @param string type
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   */
+  _onChanged: function(type) {
+    setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));
+  },
+
+  /**
+   * Recompiles the source code for the shader being edited.
+   * This function is fired at a certain delay after the user stops typing.
+   *
+   * @param string type
+   *        The corresponding shader type for the focused editor (e.g. "vs").
+   */
+  _doCompile: function(type) {
+    Task.spawn(function() {
+      let editor = yield this._getEditor(type);
+      let shaderActor = yield ShadersListView.selectedAttachment[type];
+
+      try {
+        yield shaderActor.compile(editor.getText());
+        window.emit(EVENTS.SHADER_COMPILED, null);
+        // TODO: remove error gutter markers, after bug 919709 lands.
+      } catch (error) {
+        window.emit(EVENTS.SHADER_COMPILED, error);
+        // TODO: add error gutter markers, after bug 919709 lands.
+      }
+    }.bind(this));
+  }
+};
+
+/**
+ * Localization convenience methods.
+ */
+let L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helper.
+ */
+function $(selector, target = document) target.querySelector(selector);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/shadereditor.xul
@@ -0,0 +1,64 @@
+<?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/. -->
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/shadereditor.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<!DOCTYPE window [
+  <!ENTITY % debuggerDTD SYSTEM "chrome://browser/locale/devtools/shadereditor.dtd">
+  %debuggerDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="shadereditor.js"/>
+
+  <vbox id="body" flex="1">
+    <hbox id="reload-notice"
+          class="notice-container"
+          align="center"
+          pack="center"
+          flex="1">
+      <button id="requests-menu-reload-notice-button"
+              class="devtools-toolbarbutton"
+              label="&shaderEditorUI.reloadNotice1;"
+              oncommand="gFront.setup();"/>
+      <label id="requests-menu-reload-notice-label"
+             class="plain"
+             value="&shaderEditorUI.reloadNotice2;"/>
+    </hbox>
+    <hbox id="waiting-notice"
+          class="notice-container"
+          align="center"
+          pack="center"
+          flex="1"
+          hidden="true">
+      <label id="requests-menu-waiting-notice-label"
+             class="plain"
+             value="&shaderEditorUI.emptyNotice;"/>
+    </hbox>
+
+    <hbox id="content" flex="1" hidden="true">
+      <vbox id="shaders-pane"/>
+      <splitter class="devtools-side-splitter"/>
+      <hbox id="shaders-editors" flex="1">
+        <vbox flex="1">
+          <vbox id="vs-editor" flex="1"/>
+          <label id="vs-editor-label"
+                 class="plain editor-label"
+                 value="&shaderEditorUI.vertexShader;"/>
+        </vbox>
+        <splitter id="editors-splitter" class="devtools-side-splitter"/>
+        <vbox flex="1">
+          <vbox id="fs-editor" flex="1"/>
+          <label id="fs-editor-label"
+                 class="plain editor-label"
+                 value="&shaderEditorUI.fragmentShader;"/>
+        </vbox>
+      </hbox>
+    </hbox>
+  </vbox>
+
+</window>
--- a/browser/devtools/shadereditor/test/browser.ini
+++ b/browser/devtools/shadereditor/test/browser.ini
@@ -1,15 +1,27 @@
 [DEFAULT]
 support-files =
   doc_multiple-contexts.html
   doc_shader-order.html
   doc_simple-canvas.html
   head.js
 
+[browser_se_aaa_run_first_leaktest.js]
+[browser_se_editors-contents.js]
+[browser_se_editors-lazy-init.js]
+[browser_se_first-run.js]
+[browser_se_navigation.js]
+[browser_se_programs-blackbox.js]
+[browser_se_programs-cache.js]
+[browser_se_programs-highlight.js]
+[browser_se_programs-list.js]
+[browser_se_shaders-edit-01.js]
+[browser_se_shaders-edit-02.js]
+[browser_se_shaders-edit-03.js]
 [browser_webgl-actor-test-01.js]
 [browser_webgl-actor-test-02.js]
 [browser_webgl-actor-test-03.js]
 [browser_webgl-actor-test-04.js]
 [browser_webgl-actor-test-05.js]
 [browser_webgl-actor-test-06.js]
 [browser_webgl-actor-test-07.js]
 [browser_webgl-actor-test-08.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_aaa_run_first_leaktest.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+
+  ok(target, "Should have a target available.");
+  ok(debuggee, "Should have a debuggee available.");
+  ok(panel, "Should have a panel available.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_editors-contents.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the editors contain the correct text when a program
+ * becomes available.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(vsEditor.getText().indexOf("gl_Position"), 170,
+    "The vertex shader editor contains the correct text.");
+  is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+    "The fragment shader editor contains the correct text.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_editors-lazy-init.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if source editors are lazily initialized.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, ShadersEditorsView } = panel.panelWin;
+
+  try {
+    yield ShadersEditorsView._getEditor("vs");
+    ok(false, "The promise for a vertex shader editor should be rejected.");
+  } catch (e) {
+    ok(true, "The vertex shader editors wasn't initialized.");
+  }
+
+  try {
+    yield ShadersEditorsView._getEditor("fs");
+    ok(false, "The promise for a fragment shader editor should be rejected.");
+  } catch (e) {
+    ok(true, "The fragment shader editors wasn't initialized.");
+  }
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  ok(vsEditor, "A vertex shader editor was initialized.");
+  ok(fsEditor, "A fragment shader editor was initialized.");
+
+  isnot(vsEditor, fsEditor,
+    "The vertex shader editor is distinct from the fragment shader editor.");
+
+  let vsEditor2 = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor2 = yield ShadersEditorsView._getEditor("fs");
+
+  is(vsEditor, vsEditor2,
+    "The vertex shader editor instances are cached.");
+  is(fsEditor, fsEditor2,
+    "The fragment shader editor instances are cached.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_first-run.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the shader editor shows the appropriate UI when opened.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, $ } = panel.panelWin;
+
+  is($("#reload-notice").hidden, false,
+    "The 'reload this page' notice should initially be visible.");
+  is($("#waiting-notice").hidden, true,
+    "The 'waiting for a WebGL context' notice should initially be hidden.");
+  is($("#content").hidden, true,
+    "The tool's content should initially be hidden.");
+
+  let navigating = once(target, "will-navigate");
+  let linked = once(gFront, "program-linked");
+  reload(target);
+
+  yield navigating;
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should be hidden when navigating.");
+  is($("#waiting-notice").hidden, false,
+    "The 'waiting for a WebGL context' notice should be visible when navigating.");
+  is($("#content").hidden, true,
+    "The tool's content should still be hidden.");
+
+  yield linked;
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should be hidden after linking.");
+  is($("#waiting-notice").hidden, true,
+    "The 'waiting for a WebGL context' notice should be hidden after linking.");
+  is($("#content").hidden, false,
+    "The tool's content should not be hidden anymore.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_navigation.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests target navigations are handled correctly in the UI.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, $, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should be hidden after linking.");
+  is($("#waiting-notice").hidden, true,
+    "The 'waiting for a WebGL context' notice should be visible after linking.");
+  is($("#content").hidden, false,
+    "The tool's content should not be hidden anymore.");
+
+  is(ShadersListView.itemCount, 1,
+    "The shaders list contains one entry.");
+  is(ShadersListView.selectedItem, ShadersListView.items[0],
+    "The shaders list has a correct item selected.");
+  is(ShadersListView.selectedIndex, 0,
+    "The shaders list has a correct index selected.");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(vsEditor.getText().indexOf("gl_Position"), 170,
+    "The vertex shader editor contains the correct text.");
+  is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+    "The fragment shader editor contains the correct text.");
+
+  let navigating = once(target, "will-navigate");
+  let navigated = once(target, "will-navigate");
+  navigate(target, "about:blank");
+
+  yield navigating;
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should be hidden while navigating.");
+  is($("#waiting-notice").hidden, false,
+    "The 'waiting for a WebGL context' notice should be visible while navigating.");
+  is($("#content").hidden, true,
+    "The tool's content should be hidden now that there's no WebGL content.");
+
+  is(ShadersListView.itemCount, 0,
+    "The shaders list should be empty.");
+  is(ShadersListView.selectedItem, null,
+    "The shaders list has no correct item.");
+  is(ShadersListView.selectedIndex, -1,
+    "The shaders list has a negative index.");
+
+  try {
+    yield ShadersEditorsView._getEditor("vs");
+    ok(false, "The promise for a vertex shader editor should be rejected.");
+  } catch (e) {
+    ok(true, "The vertex shader editors wasn't initialized.");
+  }
+
+  try {
+    yield ShadersEditorsView._getEditor("fs");
+    ok(false, "The promise for a fragment shader editor should be rejected.");
+  } catch (e) {
+    ok(true, "The fragment shader editors wasn't initialized.");
+  }
+
+  yield navigated;
+
+  is($("#reload-notice").hidden, true,
+    "The 'reload this page' notice should still be hidden after navigating.");
+  is($("#waiting-notice").hidden, false,
+    "The 'waiting for a WebGL context' notice should still be visible after navigating.");
+  is($("#content").hidden, true,
+    "The tool's content should be still hidden since there's no WebGL content.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-blackbox.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if blackboxing a program works properly.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+    ok(false, "No shaders should be publicly compiled during this test.");
+  });
+
+  reload(target);
+  let firstProgramActor = yield once(gFront, "program-linked");
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  vsEditor.once("change", () => {
+    ok(false, "The vertex shader source was unexpectedly changed.");
+  });
+  fsEditor.once("change", () => {
+    ok(false, "The fragment shader source was unexpectedly changed.");
+  });
+  once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+    ok(false, "No sources should be changed form this point onward.");
+  });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+  ok(!ShadersListView.selectedAttachment.isBlackBoxed,
+    "The first program should not be blackboxed yet.");
+  is(getBlackBoxCheckbox(panel, 0).checked, true,
+    "The first blackbox checkbox should be initially checked.");
+  ok(!ShadersListView.attachments[1].isBlackBoxed,
+    "The second program should not be blackboxed yet.");
+  is(getBlackBoxCheckbox(panel, 1).checked, true,
+    "The second blackbox checkbox should be initially checked.");
+
+  getBlackBoxCheckbox(panel, 0).click();
+
+  ok(ShadersListView.selectedAttachment.isBlackBoxed,
+    "The first program should now be blackboxed.");
+  is(getBlackBoxCheckbox(panel, 0).checked, false,
+    "The first blackbox checkbox should now be unchecked.");
+  ok(!ShadersListView.attachments[1].isBlackBoxed,
+    "The second program should still not be blackboxed.");
+  is(getBlackBoxCheckbox(panel, 1).checked, true,
+    "The second blackbox checkbox should still be checked.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first program was correctly blackboxed.");
+
+  getBlackBoxCheckbox(panel, 1).click();
+
+  ok(ShadersListView.selectedAttachment.isBlackBoxed,
+    "The first program should still be blackboxed.");
+  is(getBlackBoxCheckbox(panel, 0).checked, false,
+    "The first blackbox checkbox should still be unchecked.");
+  ok(ShadersListView.attachments[1].isBlackBoxed,
+    "The second program should now be blackboxed.");
+  is(getBlackBoxCheckbox(panel, 1).checked, false,
+    "The second blackbox checkbox should now be unchecked.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  ok(true, "The second program was correctly blackboxed.");
+
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  ok(true, "Highlighting didn't work while blackboxed (1).");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 0) });
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  ok(true, "Highlighting didn't work while blackboxed (2).");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 0 }, true, "#canvas2");
+  ok(true, "Highlighting didn't work while blackboxed (3).");
+
+  getBlackBoxCheckbox(panel, 0).click();
+  getBlackBoxCheckbox(panel, 1).click();
+
+  ok(!ShadersListView.selectedAttachment.isBlackBoxed,
+    "The first program should now be unblackboxed.");
+  is(getBlackBoxCheckbox(panel, 0).checked, true,
+    "The first blackbox checkbox should now be rechecked.");
+  ok(!ShadersListView.attachments[1].isBlackBoxed,
+    "The second program should now be unblackboxed.");
+  is(getBlackBoxCheckbox(panel, 1).checked, true,
+    "The second blackbox checkbox should now be rechecked.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were correctly unblackboxed.");
+
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first program was correctly highlighted.");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 0) });
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  ok(true, "The second program was correctly highlighted.");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were correctly unhighlighted.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function getItemLabel(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-label")[aIndex];
+}
+
+function getBlackBoxCheckbox(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-checkbox")[aIndex];
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-cache.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that program and shader actors are cached in the frontend.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  let programActor = yield once(gFront, "program-linked");
+  let programItem = ShadersListView.selectedItem;
+
+  is(programItem.attachment.programActor, programActor,
+    "The correct program actor is cached for the selected item.");
+
+  is((yield programActor.getVertexShader()),
+     (yield programItem.attachment.vs),
+    "The cached vertex shader promise returns the correct actor.");
+
+  is((yield programActor.getFragmentShader()),
+     (yield programItem.attachment.fs),
+    "The cached fragment shader promise returns the correct actor.");
+
+  is((yield (yield programActor.getVertexShader()).getText()),
+     (yield (yield ShadersEditorsView._getEditor("vs")).getText()),
+    "The cached vertex shader promise returns the correct text.");
+
+  is((yield (yield programActor.getFragmentShader()).getText()),
+     (yield (yield ShadersEditorsView._getEditor("fs")).getText()),
+    "The cached fragment shader promise returns the correct text.");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-highlight.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if highlighting a program works properly.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+    ok(false, "No shaders should be publicly compiled during this test.");
+  });
+
+  reload(target);
+  let firstProgramActor = yield once(gFront, "program-linked");
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  vsEditor.once("change", () => {
+    ok(false, "The vertex shader source was unexpectedly changed.");
+  });
+  fsEditor.once("change", () => {
+    ok(false, "The fragment shader source was unexpectedly changed.");
+  });
+  once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+    ok(false, "No sources should be changed form this point onward.");
+  });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first program was correctly highlighted.");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 0) });
+  ShadersListView._onShaderMouseEnter({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  ok(true, "The second program was correctly highlighted.");
+
+  ShadersListView._onShaderMouseLeave({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were correctly unhighlighted.");
+
+  ShadersListView._onShaderMouseEnter({ target: getBlackBoxCheckbox(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were left unchanged after hovering a blackbox checkbox.");
+
+  ShadersListView._onShaderMouseLeave({ target: getBlackBoxCheckbox(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two programs were left unchanged after unhovering a blackbox checkbox.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function getItemLabel(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-label")[aIndex];
+}
+
+function getBlackBoxCheckbox(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-checkbox")[aIndex];
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-list.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the programs list contains an entry after vertex and fragment
+ * shaders are linked.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, EVENTS, L10N, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  is(ShadersListView.itemCount, 0,
+    "The shaders list should initially be empty.");
+  is(ShadersListView.selectedItem, null,
+    "The shaders list has no selected item.");
+  is(ShadersListView.selectedIndex, -1,
+    "The shaders list has a negative index.");
+
+  reload(target);
+
+  let firstProgramActor = yield once(gFront, "program-linked");
+
+  is(ShadersListView.itemCount, 1,
+    "The shaders list contains one entry.");
+  is(ShadersListView.selectedItem, ShadersListView.items[0],
+    "The shaders list has a correct item selected.");
+  is(ShadersListView.selectedIndex, 0,
+    "The shaders list has a correct index selected.");
+
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  is(ShadersListView.itemCount, 2,
+    "The shaders list contains two entries.");
+  is(ShadersListView.selectedItem, ShadersListView.items[0],
+    "The shaders list has a correct item selected.");
+  is(ShadersListView.selectedIndex, 0,
+    "The shaders list has a correct index selected.");
+
+  is(ShadersListView.labels[0], L10N.getFormatStr("shadersList.programLabel", 0),
+    "The correct first label is shown in the shaders list.");
+  is(ShadersListView.labels[1], L10N.getFormatStr("shadersList.programLabel", 1),
+    "The correct second label is shown in the shaders list.");
+
+  let vertexShader = yield firstProgramActor.getVertexShader();
+  let fragmentShader = yield firstProgramActor.getFragmentShader();
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(vertSource, vsEditor.getText(),
+    "The vertex shader editor contains the correct text.");
+  is(fragSource, fsEditor.getText(),
+    "The vertex shader editor contains the correct text.");
+
+  let compiled = once(panel.panelWin, EVENTS.SHADER_COMPILED).then(() => {
+    ok(false, "Selecting a different program shouldn't recompile its shaders.");
+  });
+
+  let shown = once(panel.panelWin, EVENTS.SOURCES_SHOWN).then(() => {
+    ok(true, "The vertex and fragment sources have changed in the editors.");
+  });
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[1].target);
+  yield shown;
+
+  is(ShadersListView.selectedItem, ShadersListView.items[1],
+    "The shaders list has a correct item selected.");
+  is(ShadersListView.selectedIndex, 1,
+    "The shaders list has a correct index selected.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if editing a vertex and a fragment shader works properly.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, $, EVENTS, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(vsEditor.getText().indexOf("gl_Position"), 170,
+    "The vertex shader editor contains the correct text.");
+  is(fsEditor.getText().indexOf("gl_FragColor"), 97,
+    "The fragment shader editor contains the correct text.");
+
+  is($("#vs-editor-label").hasAttribute("selected"), false,
+    "The vertex shader editor shouldn't be initially selected.");
+  is($("#fs-editor-label").hasAttribute("selected"), false,
+    "The vertex shader editor shouldn't be initially selected.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 191, g: 64, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+  vsEditor.focus();
+
+  is($("#vs-editor-label").hasAttribute("selected"), true,
+    "The vertex shader editor should now be selected.");
+  is($("#fs-editor-label").hasAttribute("selected"), false,
+    "The vertex shader editor shouldn't still not be selected.");
+
+  vsEditor.replaceText("2.0", { line: 7, ch: 44 }, { line: 7, ch: 47 });
+  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(true, "Vertex shader was changed.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+  ok(true, "The vertex shader was recompiled successfully.");
+
+  fsEditor.focus();
+
+  is($("#vs-editor-label").hasAttribute("selected"), false,
+    "The vertex shader editor should now be deselected.");
+  is($("#fs-editor-label").hasAttribute("selected"), true,
+    "The vertex shader editor should now be selected.");
+
+  fsEditor.replaceText("0.5", { line: 5, ch: 44 }, { line: 5, ch: 47 });
+  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(true, "Fragment shader was changed.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 127 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+  ok(true, "The fragment shader was recompiled successfully.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-02.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if compile or linkage errors are emitted when a shader source
+ * gets malformed after being edited.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
+  let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
+  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(error,
+    "The new vertex shader source was compiled with errors.");
+  is(error.compile, "",
+    "The compilation status should be empty.");
+  isnot(error.link, "",
+    "The linkage status should not be empty.");
+  is(error.link.split("ERROR").length - 1, 2,
+    "The linkage status contains two errors.");
+  ok(error.link.contains("ERROR: 0:8: 'constructor'"),
+    "A constructor error is contained in the linkage status.");
+  ok(error.link.contains("ERROR: 0:8: 'assign'"),
+    "An assignment error is contained in the linkage status.");
+
+  fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
+  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(error,
+    "The new fragment shader source was compiled with errors.");
+  is(error.compile, "",
+    "The compilation status should be empty.");
+  isnot(error.link, "",
+    "The linkage status should not be empty.");
+  is(error.link.split("ERROR").length - 1, 1,
+    "The linkage status contains one error.");
+  ok(error.link.contains("ERROR: 0:6: 'constructor'"),
+    "A constructor error is contained in the linkage status.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+  vsEditor.replaceText("vec4", { line: 7, ch: 22 }, { line: 7, ch: 26 });
+  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  ok(!error, "The new vertex shader source was compiled successfully.");
+
+  fsEditor.replaceText("vec3", { line: 2, ch: 14 }, { line: 2, ch: 18 });
+  let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+  ok(!error, "The new fragment shader source was compiled successfully.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+  yield teardown(panel);
+  finish();
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_shaders-edit-03.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if editing a vertex and a fragment shader works properly.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
+  let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  let firstProgramActor = yield once(gFront, "program-linked");
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  let vsEditor = yield ShadersEditorsView._getEditor("vs");
+  let fsEditor = yield ShadersEditorsView._getEditor("fs");
+
+  is(ShadersListView.selectedIndex, 0,
+    "The first program is currently selected.");
+  is(vsEditor.getText().indexOf("1);"), 136,
+    "The vertex shader editor contains the correct initial text (1).");
+  is(fsEditor.getText().indexOf("1);"), 117,
+    "The fragment shader editor contains the correct initial text (1).");
+  is(vsEditor.getText().indexOf("2.);"), -1,
+    "The vertex shader editor contains the correct initial text (2).");
+  is(fsEditor.getText().indexOf(".0);"), -1,
+    "The fragment shader editor contains the correct initial text (2).");
+
+  vsEditor.replaceText("2.", { line: 5, ch: 44 }, { line: 5, ch: 45 });
+  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  fsEditor.replaceText(".0", { line: 5, ch: 35 }, { line: 5, ch: 37 });
+  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
+
+  ok(true, "Vertex and fragment shaders were changed.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 32, y: 32 }, { r: 255, g: 255, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 255, g: 255, b: 0, a: 0 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 32, y: 32 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+
+  ok(true, "The vertex and fragment shaders were recompiled successfully.");
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[1].target);
+  yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+  is(ShadersListView.selectedIndex, 1,
+    "The second program is currently selected.");
+  is(vsEditor.getText().indexOf("1);"), 136,
+    "The vertex shader editor contains the correct text (1).");
+  is(fsEditor.getText().indexOf("1);"), 117,
+    "The fragment shader editor contains the correct text (1).");
+  is(vsEditor.getText().indexOf("2.);"), -1,
+    "The vertex shader editor contains the correct text (2).");
+  is(fsEditor.getText().indexOf(".0);"), -1,
+    "The fragment shader editor contains the correct text (2).");
+
+  EventUtils.sendMouseEvent({ type: "mousedown" }, ShadersListView.items[0].target);
+  yield once(panel.panelWin, EVENTS.SOURCES_SHOWN);
+
+  is(ShadersListView.selectedIndex, 0,
+    "The first program is currently selected again.");
+  is(vsEditor.getText().indexOf("1);"), -1,
+    "The vertex shader editor contains the correct text (3).");
+  is(fsEditor.getText().indexOf("1);"), -1,
+    "The fragment shader editor contains the correct text (3).");
+  is(vsEditor.getText().indexOf("2.);"), 136,
+    "The vertex shader editor contains the correct text (4).");
+  is(fsEditor.getText().indexOf(".0);"), 116,
+    "The fragment shader editor contains the correct text (4).");
+
+  yield teardown(panel);
+  finish();
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
--- a/browser/devtools/shadereditor/test/head.js
+++ b/browser/devtools/shadereditor/test/head.js
@@ -26,19 +26,22 @@ let Toolbox = devtools.Toolbox;
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/shadereditor/test/";
 const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
 const SHADER_ORDER_URL = EXAMPLE_URL + "doc_shader-order.html";
 const MULTIPLE_CONTEXTS_URL = EXAMPLE_URL + "doc_multiple-contexts.html";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
+let gToolEnabled = Services.prefs.getBoolPref("devtools.shadereditor.enabled");
+
 registerCleanupFunction(() => {
   info("finish() was called, cleaning up...");
   Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+  Services.prefs.setBoolPref("devtools.shadereditor.enabled", gToolEnabled);
 });
 
 function addTab(aUrl, aWindow) {
   info("Adding tab: " + aUrl);
 
   let deferred = promise.defer();
   let targetWindow = aWindow || window;
   let targetBrowser = targetWindow.gBrowser;
@@ -185,16 +188,22 @@ function ensurePixelIs(aDebuggee, aPosit
       yield waitForFrame(aDebuggee);
       yield ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag, aSelector);
     });
   }
   ok(false, "Expected pixel was not already shown at: " + aPosition.toSource());
   return promise.reject(null);
 }
 
+function navigate(aTarget, aUrl) {
+  let navigated = once(aTarget, "navigate");
+  aTarget.client.activeTab.navigateTo(aUrl);
+  return navigated;
+}
+
 function reload(aTarget) {
   let navigated = once(aTarget, "navigate");
   aTarget.client.activeTab.reload();
   return navigated;
 }
 
 function initBackend(aUrl) {
   info("Initializing a shader editor front.");
@@ -210,8 +219,34 @@ function initBackend(aUrl) {
     let debuggee = target.window.wrappedJSObject;
 
     yield target.makeRemote();
 
     let front = new WebGLFront(target.client, target.form);
     return [target, debuggee, front];
   });
 }
+
+function initShaderEditor(aUrl) {
+  info("Initializing a shader editor pane.");
+
+  return Task.spawn(function*() {
+    let tab = yield addTab(aUrl);
+    let target = TargetFactory.forTab(tab);
+    let debuggee = target.window.wrappedJSObject;
+
+    yield target.makeRemote();
+
+    Services.prefs.setBoolPref("devtools.shadereditor.enabled", true);
+    let toolbox = yield gDevTools.showToolbox(target, "shadereditor");
+    let panel = toolbox.getCurrentPanel();
+    return [target, debuggee, panel];
+  });
+}
+
+function teardown(aPanel) {
+  info("Destroying the specified shader editor.");
+
+  return promise.all([
+    once(aPanel, "destroyed"),
+    removeTab(aPanel.target.tab)
+  ]);
+}
--- a/browser/devtools/shared/telemetry.js
+++ b/browser/devtools/shared/telemetry.js
@@ -113,16 +113,21 @@ Telemetry.prototype = {
       userHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS"
     },
     styleeditor: {
       histogram: "DEVTOOLS_STYLEEDITOR_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_STYLEEDITOR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS"
     },
+    shadereditor: {
+      histogram: "DEVTOOLS_SHADEREDITOR_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_SHADEREDITOR_OPENED_PER_USER_FLAG",
+      timerHistogram: "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS"
+    },
     jsprofiler: {
       histogram: "DEVTOOLS_JSPROFILER_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_JSPROFILER_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS"
     },
     netmonitor: {
       histogram: "DEVTOOLS_NETMONITOR_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG",
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -969,16 +969,28 @@ this.WidgetMethods = {
     let selectedElement = this._widget.selectedItem;
     if (selectedElement) {
       return this._itemsByElement.get(selectedElement)._value;
     }
     return "";
   },
 
   /**
+   * Retrieves the attachment of the selected element.
+   * @return string
+   */
+  get selectedAttachment() {
+    let selectedElement = this._widget.selectedItem;
+    if (selectedElement) {
+      return this._itemsByElement.get(selectedElement).attachment;
+    }
+    return null;
+  },
+
+  /**
    * Selects the element with the entangled item in this container.
    * @param Item | function aItem
    */
   set selectedItem(aItem) {
     // A predicate is allowed to select a specific item.
     // If no item is matched, then the current selection is removed.
     if (typeof aItem == "function") {
       aItem = this.getItemForPredicate(aItem);
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -221,16 +221,17 @@ Editor.prototype = {
       // context menus won't work).
 
       cm = win.CodeMirror(win.document.body, this.config);
       cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
         ev.preventDefault();
         this.showContextMenu(doc, ev.screenX, ev.screenY);
       }, false);
 
+      cm.on("focus", () => this.emit("focus"));
       cm.on("change", () => this.emit("change"));
       cm.on("gutterClick", (cm, line) => this.emit("gutterClick", line));
       cm.on("cursorActivity", (cm) => this.emit("cursorActivity"));
 
       win.CodeMirror.defineExtension("l10n", (name) => {
         return L10N.GetStringFromName(name);
       });
 
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/shadereditor.dtd
@@ -0,0 +1,32 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Debugger strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+  - keep it in English, or another language commonly spoken among web developers.
+  - You want to make that choice consistent across the developer tools.
+  - A good criteria is the language in which you'd find the best
+  - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.vertexShader): This is the label for
+  -  the pane that displays a vertex shader's source. -->
+<!ENTITY shaderEditorUI.vertexShader    "Vertex Shader">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.fragmentShader): This is the label for
+  -  the pane that displays a fragment shader's source. -->
+<!ENTITY shaderEditorUI.fragmentShader  "Fragment Shader">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice1): This is the label shown
+  -  on the button that triggers a page refresh. -->
+<!ENTITY shaderEditorUI.reloadNotice1   "Reload">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.reloadNotice2): This is the label shown
+  -  along with the button that triggers a page refresh. -->
+<!ENTITY shaderEditorUI.reloadNotice2   "the page to be able to edit GLSL code.">
+
+<!-- LOCALIZATION NOTE (shaderEditorUI.emptyNotice): This is the label shown
+  -  while the page is refreshing and the tool waits for a WebGL context. -->
+<!ENTITY shaderEditorUI.emptyNotice     "Waiting for a WebGL context to be created…">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/shadereditor.properties
@@ -0,0 +1,32 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Debugger
+# which is available from the Web Developer sub-menu -> 'Debugger'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.label):
+# This string is displayed in the title of the tab when the Shader Editor is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+ToolboxShaderEditor.label=Shader Editor
+
+# LOCALIZATION NOTE (ToolboxShaderEditor.tooltip):
+# This string is displayed in the tooltip of the tab when the Shader Editor is
+# displayed inside the developer tools window.
+ToolboxShaderEditor.tooltip=Live GLSL shader language editor for WebGL
+
+# LOCALIZATION NOTE (shadersList.programLabel):
+# This string is displayed in the programs list of the Shader Editor,
+# identifying a set of linked GLSL shaders.
+shadersList.programLabel=Program %S
+
+# LOCALIZATION NOTE (shadersList.blackboxLabel):
+# This string is displayed in the programs list of the Shader Editor, while
+# the user hovers over the checkbox used to toggle blackboxing of a program's
+# associated fragment shader.
+shadersList.blackboxLabel=Toggle geometry visibility
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -22,16 +22,18 @@
     locale/browser/browser.dtd                     (%chrome/browser/browser.dtd)
     locale/browser/baseMenuOverlay.dtd             (%chrome/browser/baseMenuOverlay.dtd)
     locale/browser/browser.properties              (%chrome/browser/browser.properties)
     locale/browser/devtools/appcacheutils.properties  (%chrome/browser/devtools/appcacheutils.properties)
     locale/browser/devtools/debugger.dtd              (%chrome/browser/devtools/debugger.dtd)
     locale/browser/devtools/debugger.properties       (%chrome/browser/devtools/debugger.properties)
     locale/browser/devtools/netmonitor.dtd            (%chrome/browser/devtools/netmonitor.dtd)
     locale/browser/devtools/netmonitor.properties     (%chrome/browser/devtools/netmonitor.properties)
+    locale/browser/devtools/shadereditor.dtd          (%chrome/browser/devtools/shadereditor.dtd)
+    locale/browser/devtools/shadereditor.properties   (%chrome/browser/devtools/shadereditor.properties)
     locale/browser/devtools/gcli.properties           (%chrome/browser/devtools/gcli.properties)
     locale/browser/devtools/gclicommands.properties   (%chrome/browser/devtools/gclicommands.properties)
     locale/browser/devtools/webconsole.properties     (%chrome/browser/devtools/webconsole.properties)
     locale/browser/devtools/inspector.properties      (%chrome/browser/devtools/inspector.properties)
     locale/browser/devtools/tilt.properties           (%chrome/browser/devtools/tilt.properties)
     locale/browser/devtools/scratchpad.properties     (%chrome/browser/devtools/scratchpad.properties)
     locale/browser/devtools/scratchpad.dtd            (%chrome/browser/devtools/scratchpad.dtd)
     locale/browser/devtools/styleeditor.properties    (%chrome/browser/devtools/styleeditor.properties)
new file mode 100644
--- /dev/null
+++ b/browser/themes/linux/devtools/shadereditor.css
@@ -0,0 +1,5 @@
+/* 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/. */
+
+%include ../../shared/devtools/shadereditor.inc.css
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -167,16 +167,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs/rtl-middle-selected.png          (devtools/breadcrumbs/rtl-middle-selected.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
   skin/classic/browser/devtools/splitview.css         (devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css       (devtools/styleeditor.css)
+* skin/classic/browser/devtools/shadereditor.css      (devtools/shadereditor.css)
   skin/classic/browser/devtools/debugger.css          (devtools/debugger.css)
 * skin/classic/browser/devtools/profiler.css          (devtools/profiler.css)
   skin/classic/browser/devtools/netmonitor.css        (devtools/netmonitor.css)
 * skin/classic/browser/devtools/scratchpad.css        (devtools/scratchpad.css)
   skin/classic/browser/devtools/magnifying-glass.png  (devtools/magnifying-glass.png)
   skin/classic/browser/devtools/option-icon.png       (devtools/option-icon.png)
   skin/classic/browser/devtools/itemToggle.png        (devtools/itemToggle.png)
   skin/classic/browser/devtools/blackBoxMessageEye.png (devtools/blackBoxMessageEye.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/osx/devtools/shadereditor.css
@@ -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/. */
+
+%include ../shared.inc
+%include ../../shared/devtools/shadereditor.inc.css
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -259,16 +259,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs/rtl-middle-selected.png          (devtools/breadcrumbs/rtl-middle-selected.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
   skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
   skin/classic/browser/devtools/splitview.css               (devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css             (devtools/styleeditor.css)
+* skin/classic/browser/devtools/shadereditor.css            (devtools/shadereditor.css)
 * skin/classic/browser/devtools/debugger.css                (devtools/debugger.css)
 * skin/classic/browser/devtools/profiler.css                (devtools/profiler.css)
   skin/classic/browser/devtools/netmonitor.css              (devtools/netmonitor.css)
 * skin/classic/browser/devtools/scratchpad.css              (devtools/scratchpad.css)
   skin/classic/browser/devtools/magnifying-glass.png        (devtools/magnifying-glass.png)
   skin/classic/browser/devtools/option-icon.png             (devtools/option-icon.png)
   skin/classic/browser/devtools/itemToggle.png              (devtools/itemToggle.png)
   skin/classic/browser/devtools/blackBoxMessageEye.png      (devtools/blackBoxMessageEye.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/shadereditor.inc.css
@@ -0,0 +1,107 @@
+/* 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/. */
+
+#body {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+}
+
+#content {
+  background: #fff;
+}
+
+/* Reload and waiting notices */
+
+.notice-container {
+  background: transparent;
+  margin-top: -50vh;
+  color: #fff;
+}
+
+#reload-notice {
+  font-size: 120%;
+}
+
+#waiting-notice {
+  font-size: 110%;
+}
+
+#waiting-notice::before {
+  display: inline-block;
+  content: "";
+  background: url("chrome://global/skin/icons/loading_16.png") center no-repeat;
+  width: 16px;
+  height: 16px;
+  -moz-margin-end: 6px;
+}
+
+#requests-menu-reload-notice-button {
+  min-height: 2em;
+}
+
+/* Shaders pane */
+
+#shaders-pane {
+  min-width: 150px;
+}
+
+#shaders-pane + .devtools-side-splitter {
+  -moz-border-start-color: transparent;
+}
+
+.side-menu-widget-item-checkbox {
+  -moz-appearance: none;
+  -moz-margin-end: -6px;
+  padding: 0;
+  opacity: 0;
+  transition: opacity .15s ease-out 0s;
+}
+
+/* Only show the checkbox when the source is hovered over, is selected, or if it
+ * is not checked. */
+.side-menu-widget-item:hover > .side-menu-widget-item-checkbox,
+.side-menu-widget-item.selected > .side-menu-widget-item-checkbox,
+.side-menu-widget-item-checkbox:not([checked]) {
+  opacity: 1;
+  transition: opacity .15s ease-out 0s;
+}
+
+.side-menu-widget-item-checkbox > .checkbox-check {
+  -moz-appearance: none;
+  background: none;
+  background-image: url("chrome://browser/skin/devtools/itemToggle.png");
+  background-repeat: no-repeat;
+  background-clip: content-box;
+  background-size: 32px 16px;
+  background-position: -16px 0;
+  width: 16px;
+  height: 16px;
+  border: 0;
+}
+
+.side-menu-widget-item-checkbox[checked] > .checkbox-check {
+  background-position: 0 0;
+}
+
+.side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents {
+  color: #888;
+}
+
+/* Shader source editors */
+
+#editors-splitter {
+  -moz-border-start-color: rgb(61,69,76);
+}
+
+.editor-label {
+  background: url(background-noise-toolbar.png), hsl(208,11%,27%);
+  border-top: 1px solid #222426;
+  padding: 1px 12px;
+  color: #fff;
+}
+
+.editor-label[selected] {
+  background: linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left;
+  box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15),
+              inset 0 -1px 0 hsla(210,40%,83%,.05);
+}
new file mode 100644
--- /dev/null
+++ b/browser/themes/windows/devtools/shadereditor.css
@@ -0,0 +1,5 @@
+/* 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/. */
+
+%include ../../shared/devtools/shadereditor.inc.css
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -194,16 +194,17 @@ browser.jar:
         skin/classic/browser/devtools/breadcrumbs/rtl-middle-selected.png          (devtools/breadcrumbs/rtl-middle-selected.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
         skin/classic/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
         skin/classic/browser/devtools/splitview.css                 (devtools/splitview.css)
         skin/classic/browser/devtools/styleeditor.css               (devtools/styleeditor.css)
+*       skin/classic/browser/devtools/shadereditor.css              (devtools/shadereditor.css)
         skin/classic/browser/devtools/debugger.css                  (devtools/debugger.css)
 *       skin/classic/browser/devtools/profiler.css                  (devtools/profiler.css)
         skin/classic/browser/devtools/netmonitor.css                (devtools/netmonitor.css)
 *       skin/classic/browser/devtools/scratchpad.css                (devtools/scratchpad.css)
         skin/classic/browser/devtools/magnifying-glass.png          (devtools/magnifying-glass.png)
         skin/classic/browser/devtools/option-icon.png               (devtools/option-icon.png)
         skin/classic/browser/devtools/itemToggle.png                (devtools/itemToggle.png)
         skin/classic/browser/devtools/blackBoxMessageEye.png        (devtools/blackBoxMessageEye.png)
@@ -470,16 +471,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-middle-selected.png          (devtools/breadcrumbs/rtl-middle-selected.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-middle.png                   (devtools/breadcrumbs/rtl-middle.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-pressed.png            (devtools/breadcrumbs/rtl-start-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-selected-pressed.png   (devtools/breadcrumbs/rtl-start-selected-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start.png                    (devtools/breadcrumbs/rtl-start.png)
         skin/classic/aero/browser/devtools/breadcrumbs/rtl-start-selected.png           (devtools/breadcrumbs/rtl-start-selected.png)
         skin/classic/aero/browser/devtools/splitview.css             (devtools/splitview.css)
         skin/classic/aero/browser/devtools/styleeditor.css           (devtools/styleeditor.css)
+*       skin/classic/aero/browser/devtools/shadereditor.css          (devtools/shadereditor.css)
         skin/classic/aero/browser/devtools/debugger.css              (devtools/debugger.css)
 *       skin/classic/aero/browser/devtools/profiler.css              (devtools/profiler.css)
         skin/classic/aero/browser/devtools/netmonitor.css            (devtools/netmonitor.css)
 *       skin/classic/aero/browser/devtools/scratchpad.css            (devtools/scratchpad.css)
         skin/classic/aero/browser/devtools/magnifying-glass.png      (devtools/magnifying-glass.png)
         skin/classic/aero/browser/devtools/option-icon.png           (devtools/option-icon.png)
         skin/classic/aero/browser/devtools/itemToggle.png            (devtools/itemToggle.png)
         skin/classic/aero/browser/devtools/blackBoxMessageEye.png    (devtools/blackBoxMessageEye.png)
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3926,16 +3926,20 @@
   "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_BOOLEAN": {
     "kind": "boolean",
     "description": "How many times has the devtool's Browser Debugger been opened?"
   },
   "DEVTOOLS_STYLEEDITOR_OPENED_BOOLEAN": {
     "kind": "boolean",
     "description": "How many times has the devtool's Style Editor been opened?"
   },
+  "DEVTOOLS_SHADEREDITOR_OPENED_BOOLEAN": {
+    "kind": "boolean",
+    "description": "How many times has the devtool's Shader Editor been opened?"
+  },
   "DEVTOOLS_JSPROFILER_OPENED_BOOLEAN": {
     "kind": "boolean",
     "description": "How many times has the devtool's JS Profiler been opened?"
   },
   "DEVTOOLS_NETMONITOR_OPENED_BOOLEAN": {
     "kind": "boolean",
     "description": "How many times has the devtool's Network Monitor been opened?"
   },
@@ -3998,16 +4002,20 @@
   "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_PER_USER_FLAG": {
     "kind": "flag",
     "description": "How many users have opened the devtool's Browser Debugger?"
   },
   "DEVTOOLS_STYLEEDITOR_OPENED_PER_USER_FLAG": {
     "kind": "flag",
     "description": "How many users have opened the devtool's Style Editor?"
   },
+  "DEVTOOLS_SHADEREDITOR_OPENED_PER_USER_FLAG": {
+    "kind": "flag",
+    "description": "How many users have opened the devtool's Shader Editor?"
+  },
   "DEVTOOLS_JSPROFILER_OPENED_PER_USER_FLAG": {
     "kind": "flag",
     "description": "How many users have opened the devtool's JS Profiler?"
   },
   "DEVTOOLS_NETMONITOR_OPENED_PER_USER_FLAG": {
     "kind": "flag",
     "description": "How many users have opened the devtool's Network Monitor?"
   },
@@ -4098,16 +4106,22 @@
     "description": "How long has the JS browser debugger been active (seconds)"
   },
   "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS": {
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has the style editor been active (seconds)"
   },
+  "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS": {
+    "kind": "exponential",
+    "high": "10000000",
+    "n_buckets": 100,
+    "description": "How long has the Shader Editor been active (seconds)"
+  },
   "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS": {
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long has the JS profiler been active (seconds)"
   },
   "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS": {
     "kind": "exponential",