browser/devtools/shadereditor/shadereditor.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/shadereditor/shadereditor.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,604 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +"use strict";
     1.8 +
     1.9 +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
    1.10 +
    1.11 +Cu.import("resource://gre/modules/Services.jsm");
    1.12 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.13 +Cu.import("resource://gre/modules/Task.jsm");
    1.14 +Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
    1.15 +Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
    1.16 +
    1.17 +const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    1.18 +const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
    1.19 +const EventEmitter = require("devtools/toolkit/event-emitter");
    1.20 +const {Tooltip} = require("devtools/shared/widgets/Tooltip");
    1.21 +const Editor = require("devtools/sourceeditor/editor");
    1.22 +
    1.23 +// The panel's window global is an EventEmitter firing the following events:
    1.24 +const EVENTS = {
    1.25 +  // When new programs are received from the server.
    1.26 +  NEW_PROGRAM: "ShaderEditor:NewProgram",
    1.27 +  PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
    1.28 +
    1.29 +  // When the vertex and fragment sources were shown in the editor.
    1.30 +  SOURCES_SHOWN: "ShaderEditor:SourcesShown",
    1.31 +
    1.32 +  // When a shader's source was edited and compiled via the editor.
    1.33 +  SHADER_COMPILED: "ShaderEditor:ShaderCompiled",
    1.34 +
    1.35 +  // When the UI is reset from tab navigation
    1.36 +  UI_RESET: "ShaderEditor:UIReset",
    1.37 +
    1.38 +  // When the editor's error markers are all removed
    1.39 +  EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned"
    1.40 +};
    1.41 +
    1.42 +const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
    1.43 +const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba
    1.44 +const TYPING_MAX_DELAY = 500; // ms
    1.45 +const SHADERS_AUTOGROW_ITEMS = 4;
    1.46 +const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
    1.47 +const GUTTER_ERROR_PANEL_DELAY = 100; // ms
    1.48 +const DEFAULT_EDITOR_CONFIG = {
    1.49 +  gutters: ["errors"],
    1.50 +  lineNumbers: true,
    1.51 +  showAnnotationRuler: true
    1.52 +};
    1.53 +
    1.54 +/**
    1.55 + * The current target and the WebGL Editor front, set by this tool's host.
    1.56 + */
    1.57 +let gToolbox, gTarget, gFront;
    1.58 +
    1.59 +/**
    1.60 + * Initializes the shader editor controller and views.
    1.61 + */
    1.62 +function startupShaderEditor() {
    1.63 +  return promise.all([
    1.64 +    EventsHandler.initialize(),
    1.65 +    ShadersListView.initialize(),
    1.66 +    ShadersEditorsView.initialize()
    1.67 +  ]);
    1.68 +}
    1.69 +
    1.70 +/**
    1.71 + * Destroys the shader editor controller and views.
    1.72 + */
    1.73 +function shutdownShaderEditor() {
    1.74 +  return promise.all([
    1.75 +    EventsHandler.destroy(),
    1.76 +    ShadersListView.destroy(),
    1.77 +    ShadersEditorsView.destroy()
    1.78 +  ]);
    1.79 +}
    1.80 +
    1.81 +/**
    1.82 + * Functions handling target-related lifetime events.
    1.83 + */
    1.84 +let EventsHandler = {
    1.85 +  /**
    1.86 +   * Listen for events emitted by the current tab target.
    1.87 +   */
    1.88 +  initialize: function() {
    1.89 +    this._onHostChanged = this._onHostChanged.bind(this);
    1.90 +    this._onTabNavigated = this._onTabNavigated.bind(this);
    1.91 +    this._onProgramLinked = this._onProgramLinked.bind(this);
    1.92 +    this._onProgramsAdded = this._onProgramsAdded.bind(this);
    1.93 +    gToolbox.on("host-changed", this._onHostChanged);
    1.94 +    gTarget.on("will-navigate", this._onTabNavigated);
    1.95 +    gTarget.on("navigate", this._onTabNavigated);
    1.96 +    gFront.on("program-linked", this._onProgramLinked);
    1.97 +
    1.98 +  },
    1.99 +
   1.100 +  /**
   1.101 +   * Remove events emitted by the current tab target.
   1.102 +   */
   1.103 +  destroy: function() {
   1.104 +    gToolbox.off("host-changed", this._onHostChanged);
   1.105 +    gTarget.off("will-navigate", this._onTabNavigated);
   1.106 +    gTarget.off("navigate", this._onTabNavigated);
   1.107 +    gFront.off("program-linked", this._onProgramLinked);
   1.108 +  },
   1.109 +
   1.110 +  /**
   1.111 +   * Handles a host change event on the parent toolbox.
   1.112 +   */
   1.113 +  _onHostChanged: function() {
   1.114 +    if (gToolbox.hostType == "side") {
   1.115 +      $("#shaders-pane").removeAttribute("height");
   1.116 +    }
   1.117 +  },
   1.118 +
   1.119 +  /**
   1.120 +   * Called for each location change in the debugged tab.
   1.121 +   */
   1.122 +  _onTabNavigated: function(event) {
   1.123 +    switch (event) {
   1.124 +      case "will-navigate": {
   1.125 +        Task.spawn(function() {
   1.126 +          // Make sure the backend is prepared to handle WebGL contexts.
   1.127 +          gFront.setup({ reload: false });
   1.128 +
   1.129 +          // Reset UI.
   1.130 +          ShadersListView.empty();
   1.131 +          $("#reload-notice").hidden = true;
   1.132 +          $("#waiting-notice").hidden = false;
   1.133 +          yield ShadersEditorsView.setText({ vs: "", fs: "" });
   1.134 +          $("#content").hidden = true;
   1.135 +        }).then(() => window.emit(EVENTS.UI_RESET));
   1.136 +        break;
   1.137 +      }
   1.138 +      case "navigate": {
   1.139 +        // Manually retrieve the list of program actors known to the server,
   1.140 +        // because the backend won't emit "program-linked" notifications
   1.141 +        // in the case of a bfcache navigation (since no new programs are
   1.142 +        // actually linked).
   1.143 +        gFront.getPrograms().then(this._onProgramsAdded);
   1.144 +        break;
   1.145 +      }
   1.146 +    }
   1.147 +  },
   1.148 +
   1.149 +  /**
   1.150 +   * Called every time a program was linked in the debugged tab.
   1.151 +   */
   1.152 +  _onProgramLinked: function(programActor) {
   1.153 +    this._addProgram(programActor);
   1.154 +    window.emit(EVENTS.NEW_PROGRAM);
   1.155 +  },
   1.156 +
   1.157 +  /**
   1.158 +   * Callback for the front's getPrograms() method.
   1.159 +   */
   1.160 +  _onProgramsAdded: function(programActors) {
   1.161 +    programActors.forEach(this._addProgram);
   1.162 +    window.emit(EVENTS.PROGRAMS_ADDED);
   1.163 +  },
   1.164 +
   1.165 +  /**
   1.166 +   * Adds a program to the shaders list and unhides any modal notices.
   1.167 +   */
   1.168 +  _addProgram: function(programActor) {
   1.169 +    $("#waiting-notice").hidden = true;
   1.170 +    $("#reload-notice").hidden = true;
   1.171 +    $("#content").hidden = false;
   1.172 +    ShadersListView.addProgram(programActor);
   1.173 +  }
   1.174 +};
   1.175 +
   1.176 +/**
   1.177 + * Functions handling the sources UI.
   1.178 + */
   1.179 +let ShadersListView = Heritage.extend(WidgetMethods, {
   1.180 +  /**
   1.181 +   * Initialization function, called when the tool is started.
   1.182 +   */
   1.183 +  initialize: function() {
   1.184 +    this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
   1.185 +      showArrows: true,
   1.186 +      showItemCheckboxes: true
   1.187 +    });
   1.188 +
   1.189 +    this._onProgramSelect = this._onProgramSelect.bind(this);
   1.190 +    this._onProgramCheck = this._onProgramCheck.bind(this);
   1.191 +    this._onProgramMouseEnter = this._onProgramMouseEnter.bind(this);
   1.192 +    this._onProgramMouseLeave = this._onProgramMouseLeave.bind(this);
   1.193 +
   1.194 +    this.widget.addEventListener("select", this._onProgramSelect, false);
   1.195 +    this.widget.addEventListener("check", this._onProgramCheck, false);
   1.196 +    this.widget.addEventListener("mouseenter", this._onProgramMouseEnter, true);
   1.197 +    this.widget.addEventListener("mouseleave", this._onProgramMouseLeave, true);
   1.198 +  },
   1.199 +
   1.200 +  /**
   1.201 +   * Destruction function, called when the tool is closed.
   1.202 +   */
   1.203 +  destroy: function() {
   1.204 +    this.widget.removeEventListener("select", this._onProgramSelect, false);
   1.205 +    this.widget.removeEventListener("check", this._onProgramCheck, false);
   1.206 +    this.widget.removeEventListener("mouseenter", this._onProgramMouseEnter, true);
   1.207 +    this.widget.removeEventListener("mouseleave", this._onProgramMouseLeave, true);
   1.208 +  },
   1.209 +
   1.210 +  /**
   1.211 +   * Adds a program to this programs container.
   1.212 +   *
   1.213 +   * @param object programActor
   1.214 +   *        The program actor coming from the active thread.
   1.215 +   */
   1.216 +  addProgram: function(programActor) {
   1.217 +    if (this.hasProgram(programActor)) {
   1.218 +      return;
   1.219 +    }
   1.220 +
   1.221 +    // Currently, there's no good way of differentiating between programs
   1.222 +    // in a way that helps humans. It will be a good idea to implement a
   1.223 +    // standard of allowing debuggees to add some identifiable metadata to their
   1.224 +    // program sources or instances.
   1.225 +    let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount);
   1.226 +    let contents = document.createElement("label");
   1.227 +    contents.className = "plain program-item";
   1.228 +    contents.setAttribute("value", label);
   1.229 +    contents.setAttribute("crop", "start");
   1.230 +    contents.setAttribute("flex", "1");
   1.231 +
   1.232 +    // Append a program item to this container.
   1.233 +    this.push([contents], {
   1.234 +      index: -1, /* specifies on which position should the item be appended */
   1.235 +      attachment: {
   1.236 +        label: label,
   1.237 +        programActor: programActor,
   1.238 +        checkboxState: true,
   1.239 +        checkboxTooltip: L10N.getStr("shadersList.blackboxLabel")
   1.240 +      }
   1.241 +    });
   1.242 +
   1.243 +    // Make sure there's always a selected item available.
   1.244 +    if (!this.selectedItem) {
   1.245 +      this.selectedIndex = 0;
   1.246 +    }
   1.247 +
   1.248 +    // Prevent this container from growing indefinitely in height when the
   1.249 +    // toolbox is docked to the side.
   1.250 +    if (gToolbox.hostType == "side" && this.itemCount == SHADERS_AUTOGROW_ITEMS) {
   1.251 +      this._pane.setAttribute("height", this._pane.getBoundingClientRect().height);
   1.252 +    }
   1.253 +  },
   1.254 +
   1.255 +  /**
   1.256 +   * Returns whether a program was already added to this programs container.
   1.257 +   *
   1.258 +   * @param object programActor
   1.259 +   *        The program actor coming from the active thread.
   1.260 +   * @param boolean
   1.261 +   *        True if the program was added, false otherwise.
   1.262 +   */
   1.263 +  hasProgram: function(programActor) {
   1.264 +    return !!this.attachments.filter(e => e.programActor == programActor).length;
   1.265 +  },
   1.266 +
   1.267 +  /**
   1.268 +   * The select listener for the programs container.
   1.269 +   */
   1.270 +  _onProgramSelect: function({ detail: sourceItem }) {
   1.271 +    if (!sourceItem) {
   1.272 +      return;
   1.273 +    }
   1.274 +    // The container is not empty and an actual item was selected.
   1.275 +    let attachment = sourceItem.attachment;
   1.276 +
   1.277 +    function getShaders() {
   1.278 +      return promise.all([
   1.279 +        attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()),
   1.280 +        attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader())
   1.281 +      ]);
   1.282 +    }
   1.283 +    function getSources([vertexShaderActor, fragmentShaderActor]) {
   1.284 +      return promise.all([
   1.285 +        vertexShaderActor.getText(),
   1.286 +        fragmentShaderActor.getText()
   1.287 +      ]);
   1.288 +    }
   1.289 +    function showSources([vertexShaderText, fragmentShaderText]) {
   1.290 +      return ShadersEditorsView.setText({
   1.291 +        vs: vertexShaderText,
   1.292 +        fs: fragmentShaderText
   1.293 +      });
   1.294 +    }
   1.295 +
   1.296 +    getShaders()
   1.297 +      .then(getSources)
   1.298 +      .then(showSources)
   1.299 +      .then(null, Cu.reportError);
   1.300 +  },
   1.301 +
   1.302 +  /**
   1.303 +   * The check listener for the programs container.
   1.304 +   */
   1.305 +  _onProgramCheck: function({ detail: { checked }, target }) {
   1.306 +    let sourceItem = this.getItemForElement(target);
   1.307 +    let attachment = sourceItem.attachment;
   1.308 +    attachment.isBlackBoxed = !checked;
   1.309 +    attachment.programActor[checked ? "unblackbox" : "blackbox"]();
   1.310 +  },
   1.311 +
   1.312 +  /**
   1.313 +   * The mouseenter listener for the programs container.
   1.314 +   */
   1.315 +  _onProgramMouseEnter: function(e) {
   1.316 +    let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
   1.317 +    if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
   1.318 +      sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT);
   1.319 +
   1.320 +      if (e instanceof Event) {
   1.321 +        e.preventDefault();
   1.322 +        e.stopPropagation();
   1.323 +      }
   1.324 +    }
   1.325 +  },
   1.326 +
   1.327 +  /**
   1.328 +   * The mouseleave listener for the programs container.
   1.329 +   */
   1.330 +  _onProgramMouseLeave: function(e) {
   1.331 +    let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
   1.332 +    if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
   1.333 +      sourceItem.attachment.programActor.unhighlight();
   1.334 +
   1.335 +      if (e instanceof Event) {
   1.336 +        e.preventDefault();
   1.337 +        e.stopPropagation();
   1.338 +      }
   1.339 +    }
   1.340 +  }
   1.341 +});
   1.342 +
   1.343 +/**
   1.344 + * Functions handling the editors displaying the vertex and fragment shaders.
   1.345 + */
   1.346 +let ShadersEditorsView = {
   1.347 +  /**
   1.348 +   * Initialization function, called when the tool is started.
   1.349 +   */
   1.350 +  initialize: function() {
   1.351 +    XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map());
   1.352 +    this._vsFocused = this._onFocused.bind(this, "vs", "fs");
   1.353 +    this._fsFocused = this._onFocused.bind(this, "fs", "vs");
   1.354 +    this._vsChanged = this._onChanged.bind(this, "vs");
   1.355 +    this._fsChanged = this._onChanged.bind(this, "fs");
   1.356 +  },
   1.357 +
   1.358 +  /**
   1.359 +   * Destruction function, called when the tool is closed.
   1.360 +   */
   1.361 +  destroy: function() {
   1.362 +    this._toggleListeners("off");
   1.363 +  },
   1.364 +
   1.365 +  /**
   1.366 +   * Sets the text displayed in the vertex and fragment shader editors.
   1.367 +   *
   1.368 +   * @param object sources
   1.369 +   *        An object containing the following properties
   1.370 +   *          - vs: the vertex shader source code
   1.371 +   *          - fs: the fragment shader source code
   1.372 +   * @return object
   1.373 +   *        A promise resolving upon completion of text setting.
   1.374 +   */
   1.375 +  setText: function(sources) {
   1.376 +    let view = this;
   1.377 +    function setTextAndClearHistory(editor, text) {
   1.378 +      editor.setText(text);
   1.379 +      editor.clearHistory();
   1.380 +    }
   1.381 +
   1.382 +    return Task.spawn(function() {
   1.383 +      yield view._toggleListeners("off");
   1.384 +      yield promise.all([
   1.385 +        view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
   1.386 +        view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs))
   1.387 +      ]);
   1.388 +      yield view._toggleListeners("on");
   1.389 +    }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources));
   1.390 +  },
   1.391 +
   1.392 +  /**
   1.393 +   * Lazily initializes and returns a promise for an Editor instance.
   1.394 +   *
   1.395 +   * @param string type
   1.396 +   *        Specifies for which shader type should an editor be retrieved,
   1.397 +   *        either are "vs" for a vertex, or "fs" for a fragment shader.
   1.398 +   * @return object
   1.399 +   *        Returns a promise that resolves to an editor instance
   1.400 +   */
   1.401 +  _getEditor: function(type) {
   1.402 +    if ($("#content").hidden) {
   1.403 +      return promise.reject(new Error("Shader Editor is still waiting for a WebGL context to be created."));
   1.404 +    }
   1.405 +    if (this._editorPromises.has(type)) {
   1.406 +      return this._editorPromises.get(type);
   1.407 +    }
   1.408 +
   1.409 +    let deferred = promise.defer();
   1.410 +    this._editorPromises.set(type, deferred.promise);
   1.411 +
   1.412 +    // Initialize the source editor and store the newly created instance
   1.413 +    // in the ether of a resolved promise's value.
   1.414 +    let parent = $("#" + type +"-editor");
   1.415 +    let editor = new Editor(DEFAULT_EDITOR_CONFIG);
   1.416 +    editor.config.mode = Editor.modes[type];
   1.417 +    editor.appendTo(parent).then(() => deferred.resolve(editor));
   1.418 +
   1.419 +    return deferred.promise;
   1.420 +  },
   1.421 +
   1.422 +  /**
   1.423 +   * Toggles all the event listeners for the editors either on or off.
   1.424 +   *
   1.425 +   * @param string flag
   1.426 +   *        Either "on" to enable the event listeners, "off" to disable them.
   1.427 +   * @return object
   1.428 +   *        A promise resolving upon completion of toggling the listeners.
   1.429 +   */
   1.430 +  _toggleListeners: function(flag) {
   1.431 +    return promise.all(["vs", "fs"].map(type => {
   1.432 +      return this._getEditor(type).then(editor => {
   1.433 +        editor[flag]("focus", this["_" + type + "Focused"]);
   1.434 +        editor[flag]("change", this["_" + type + "Changed"]);
   1.435 +      });
   1.436 +    }));
   1.437 +  },
   1.438 +
   1.439 +  /**
   1.440 +   * The focus listener for a source editor.
   1.441 +   *
   1.442 +   * @param string focused
   1.443 +   *        The corresponding shader type for the focused editor (e.g. "vs").
   1.444 +   * @param string focused
   1.445 +   *        The corresponding shader type for the other editor (e.g. "fs").
   1.446 +   */
   1.447 +  _onFocused: function(focused, unfocused) {
   1.448 +    $("#" + focused + "-editor-label").setAttribute("selected", "");
   1.449 +    $("#" + unfocused + "-editor-label").removeAttribute("selected");
   1.450 +  },
   1.451 +
   1.452 +  /**
   1.453 +   * The change listener for a source editor.
   1.454 +   *
   1.455 +   * @param string type
   1.456 +   *        The corresponding shader type for the focused editor (e.g. "vs").
   1.457 +   */
   1.458 +  _onChanged: function(type) {
   1.459 +    setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));
   1.460 +
   1.461 +    // Remove all the gutter markers and line classes from the editor.
   1.462 +    this._cleanEditor(type);
   1.463 +  },
   1.464 +
   1.465 +  /**
   1.466 +   * Recompiles the source code for the shader being edited.
   1.467 +   * This function is fired at a certain delay after the user stops typing.
   1.468 +   *
   1.469 +   * @param string type
   1.470 +   *        The corresponding shader type for the focused editor (e.g. "vs").
   1.471 +   */
   1.472 +  _doCompile: function(type) {
   1.473 +    Task.spawn(function() {
   1.474 +      let editor = yield this._getEditor(type);
   1.475 +      let shaderActor = yield ShadersListView.selectedAttachment[type];
   1.476 +
   1.477 +      try {
   1.478 +        yield shaderActor.compile(editor.getText());
   1.479 +        this._onSuccessfulCompilation();
   1.480 +      } catch (e) {
   1.481 +        this._onFailedCompilation(type, editor, e);
   1.482 +      }
   1.483 +    }.bind(this));
   1.484 +  },
   1.485 +
   1.486 +  /**
   1.487 +   * Called uppon a successful shader compilation.
   1.488 +   */
   1.489 +  _onSuccessfulCompilation: function() {
   1.490 +    // Signal that the shader was compiled successfully.
   1.491 +    window.emit(EVENTS.SHADER_COMPILED, null);
   1.492 +  },
   1.493 +
   1.494 +  /**
   1.495 +   * Called uppon an unsuccessful shader compilation.
   1.496 +   */
   1.497 +  _onFailedCompilation: function(type, editor, errors) {
   1.498 +    let lineCount = editor.lineCount();
   1.499 +    let currentLine = editor.getCursor().line;
   1.500 +    let listeners = { mouseenter: this._onMarkerMouseEnter };
   1.501 +
   1.502 +    function matchLinesAndMessages(string) {
   1.503 +      return {
   1.504 +        // First number that is not equal to 0.
   1.505 +        lineMatch: string.match(/\d{2,}|[1-9]/),
   1.506 +        // The string after all the numbers, semicolons and spaces.
   1.507 +        textMatch: string.match(/[^\s\d:][^\r\n|]*/)
   1.508 +      };
   1.509 +    }
   1.510 +    function discardInvalidMatches(e) {
   1.511 +      // Discard empty line and text matches.
   1.512 +      return e.lineMatch && e.textMatch;
   1.513 +    }
   1.514 +    function sanitizeValidMatches(e) {
   1.515 +      return {
   1.516 +        // Drivers might yield confusing line numbers under some obscure
   1.517 +        // circumstances. Don't throw the errors away in those cases,
   1.518 +        // just display them on the currently edited line.
   1.519 +        line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
   1.520 +        // Trim whitespace from the beginning and the end of the message,
   1.521 +        // and replace all other occurences of double spaces to a single space.
   1.522 +        text: e.textMatch[0].trim().replace(/\s{2,}/g, " ")
   1.523 +      };
   1.524 +    }
   1.525 +    function sortByLine(first, second) {
   1.526 +      // Sort all the errors ascending by their corresponding line number.
   1.527 +      return first.line > second.line ? 1 : -1;
   1.528 +    }
   1.529 +    function groupSameLineMessages(accumulator, current) {
   1.530 +      // Group errors corresponding to the same line number to a single object.
   1.531 +      let previous = accumulator[accumulator.length - 1];
   1.532 +      if (!previous || previous.line != current.line) {
   1.533 +        return [...accumulator, {
   1.534 +          line: current.line,
   1.535 +          messages: [current.text]
   1.536 +        }];
   1.537 +      } else {
   1.538 +        previous.messages.push(current.text);
   1.539 +        return accumulator;
   1.540 +      }
   1.541 +    }
   1.542 +    function displayErrors({ line, messages }) {
   1.543 +      // Add gutter markers and line classes for every error in the source.
   1.544 +      editor.addMarker(line, "errors", "error");
   1.545 +      editor.setMarkerListeners(line, "errors", "error", listeners, messages);
   1.546 +      editor.addLineClass(line, "error-line");
   1.547 +    }
   1.548 +
   1.549 +    (this._errors[type] = errors.link
   1.550 +      .split("ERROR")
   1.551 +      .map(matchLinesAndMessages)
   1.552 +      .filter(discardInvalidMatches)
   1.553 +      .map(sanitizeValidMatches)
   1.554 +      .sort(sortByLine)
   1.555 +      .reduce(groupSameLineMessages, []))
   1.556 +      .forEach(displayErrors);
   1.557 +
   1.558 +    // Signal that the shader wasn't compiled successfully.
   1.559 +    window.emit(EVENTS.SHADER_COMPILED, errors);
   1.560 +  },
   1.561 +
   1.562 +  /**
   1.563 +   * Event listener for the 'mouseenter' event on a marker in the editor gutter.
   1.564 +   */
   1.565 +  _onMarkerMouseEnter: function(line, node, messages) {
   1.566 +    if (node._markerErrorsTooltip) {
   1.567 +      return;
   1.568 +    }
   1.569 +
   1.570 +    let tooltip = node._markerErrorsTooltip = new Tooltip(document);
   1.571 +    tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X;
   1.572 +    tooltip.setTextContent({ messages: messages });
   1.573 +    tooltip.startTogglingOnHover(node, () => true, GUTTER_ERROR_PANEL_DELAY);
   1.574 +  },
   1.575 +
   1.576 +  /**
   1.577 +   * Removes all the gutter markers and line classes from the editor.
   1.578 +   */
   1.579 +  _cleanEditor: function(type) {
   1.580 +    this._getEditor(type).then(editor => {
   1.581 +      editor.removeAllMarkers("errors");
   1.582 +      this._errors[type].forEach(e => editor.removeLineClass(e.line));
   1.583 +      this._errors[type].length = 0;
   1.584 +      window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
   1.585 +    });
   1.586 +  },
   1.587 +
   1.588 +  _errors: {
   1.589 +    vs: [],
   1.590 +    fs: []
   1.591 +  }
   1.592 +};
   1.593 +
   1.594 +/**
   1.595 + * Localization convenience methods.
   1.596 + */
   1.597 +let L10N = new ViewHelpers.L10N(STRINGS_URI);
   1.598 +
   1.599 +/**
   1.600 + * Convenient way of emitting events from the panel window.
   1.601 + */
   1.602 +EventEmitter.decorate(this);
   1.603 +
   1.604 +/**
   1.605 + * DOM query helper.
   1.606 + */
   1.607 +function $(selector, target = document) target.querySelector(selector);

mercurial