browser/devtools/shadereditor/shadereditor.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4 "use strict";
michael@0 5
michael@0 6 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
michael@0 7
michael@0 8 Cu.import("resource://gre/modules/Services.jsm");
michael@0 9 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 10 Cu.import("resource://gre/modules/Task.jsm");
michael@0 11 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
michael@0 12 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
michael@0 13
michael@0 14 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
michael@0 15 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
michael@0 16 const EventEmitter = require("devtools/toolkit/event-emitter");
michael@0 17 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
michael@0 18 const Editor = require("devtools/sourceeditor/editor");
michael@0 19
michael@0 20 // The panel's window global is an EventEmitter firing the following events:
michael@0 21 const EVENTS = {
michael@0 22 // When new programs are received from the server.
michael@0 23 NEW_PROGRAM: "ShaderEditor:NewProgram",
michael@0 24 PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
michael@0 25
michael@0 26 // When the vertex and fragment sources were shown in the editor.
michael@0 27 SOURCES_SHOWN: "ShaderEditor:SourcesShown",
michael@0 28
michael@0 29 // When a shader's source was edited and compiled via the editor.
michael@0 30 SHADER_COMPILED: "ShaderEditor:ShaderCompiled",
michael@0 31
michael@0 32 // When the UI is reset from tab navigation
michael@0 33 UI_RESET: "ShaderEditor:UIReset",
michael@0 34
michael@0 35 // When the editor's error markers are all removed
michael@0 36 EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned"
michael@0 37 };
michael@0 38
michael@0 39 const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
michael@0 40 const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba
michael@0 41 const TYPING_MAX_DELAY = 500; // ms
michael@0 42 const SHADERS_AUTOGROW_ITEMS = 4;
michael@0 43 const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
michael@0 44 const GUTTER_ERROR_PANEL_DELAY = 100; // ms
michael@0 45 const DEFAULT_EDITOR_CONFIG = {
michael@0 46 gutters: ["errors"],
michael@0 47 lineNumbers: true,
michael@0 48 showAnnotationRuler: true
michael@0 49 };
michael@0 50
michael@0 51 /**
michael@0 52 * The current target and the WebGL Editor front, set by this tool's host.
michael@0 53 */
michael@0 54 let gToolbox, gTarget, gFront;
michael@0 55
michael@0 56 /**
michael@0 57 * Initializes the shader editor controller and views.
michael@0 58 */
michael@0 59 function startupShaderEditor() {
michael@0 60 return promise.all([
michael@0 61 EventsHandler.initialize(),
michael@0 62 ShadersListView.initialize(),
michael@0 63 ShadersEditorsView.initialize()
michael@0 64 ]);
michael@0 65 }
michael@0 66
michael@0 67 /**
michael@0 68 * Destroys the shader editor controller and views.
michael@0 69 */
michael@0 70 function shutdownShaderEditor() {
michael@0 71 return promise.all([
michael@0 72 EventsHandler.destroy(),
michael@0 73 ShadersListView.destroy(),
michael@0 74 ShadersEditorsView.destroy()
michael@0 75 ]);
michael@0 76 }
michael@0 77
michael@0 78 /**
michael@0 79 * Functions handling target-related lifetime events.
michael@0 80 */
michael@0 81 let EventsHandler = {
michael@0 82 /**
michael@0 83 * Listen for events emitted by the current tab target.
michael@0 84 */
michael@0 85 initialize: function() {
michael@0 86 this._onHostChanged = this._onHostChanged.bind(this);
michael@0 87 this._onTabNavigated = this._onTabNavigated.bind(this);
michael@0 88 this._onProgramLinked = this._onProgramLinked.bind(this);
michael@0 89 this._onProgramsAdded = this._onProgramsAdded.bind(this);
michael@0 90 gToolbox.on("host-changed", this._onHostChanged);
michael@0 91 gTarget.on("will-navigate", this._onTabNavigated);
michael@0 92 gTarget.on("navigate", this._onTabNavigated);
michael@0 93 gFront.on("program-linked", this._onProgramLinked);
michael@0 94
michael@0 95 },
michael@0 96
michael@0 97 /**
michael@0 98 * Remove events emitted by the current tab target.
michael@0 99 */
michael@0 100 destroy: function() {
michael@0 101 gToolbox.off("host-changed", this._onHostChanged);
michael@0 102 gTarget.off("will-navigate", this._onTabNavigated);
michael@0 103 gTarget.off("navigate", this._onTabNavigated);
michael@0 104 gFront.off("program-linked", this._onProgramLinked);
michael@0 105 },
michael@0 106
michael@0 107 /**
michael@0 108 * Handles a host change event on the parent toolbox.
michael@0 109 */
michael@0 110 _onHostChanged: function() {
michael@0 111 if (gToolbox.hostType == "side") {
michael@0 112 $("#shaders-pane").removeAttribute("height");
michael@0 113 }
michael@0 114 },
michael@0 115
michael@0 116 /**
michael@0 117 * Called for each location change in the debugged tab.
michael@0 118 */
michael@0 119 _onTabNavigated: function(event) {
michael@0 120 switch (event) {
michael@0 121 case "will-navigate": {
michael@0 122 Task.spawn(function() {
michael@0 123 // Make sure the backend is prepared to handle WebGL contexts.
michael@0 124 gFront.setup({ reload: false });
michael@0 125
michael@0 126 // Reset UI.
michael@0 127 ShadersListView.empty();
michael@0 128 $("#reload-notice").hidden = true;
michael@0 129 $("#waiting-notice").hidden = false;
michael@0 130 yield ShadersEditorsView.setText({ vs: "", fs: "" });
michael@0 131 $("#content").hidden = true;
michael@0 132 }).then(() => window.emit(EVENTS.UI_RESET));
michael@0 133 break;
michael@0 134 }
michael@0 135 case "navigate": {
michael@0 136 // Manually retrieve the list of program actors known to the server,
michael@0 137 // because the backend won't emit "program-linked" notifications
michael@0 138 // in the case of a bfcache navigation (since no new programs are
michael@0 139 // actually linked).
michael@0 140 gFront.getPrograms().then(this._onProgramsAdded);
michael@0 141 break;
michael@0 142 }
michael@0 143 }
michael@0 144 },
michael@0 145
michael@0 146 /**
michael@0 147 * Called every time a program was linked in the debugged tab.
michael@0 148 */
michael@0 149 _onProgramLinked: function(programActor) {
michael@0 150 this._addProgram(programActor);
michael@0 151 window.emit(EVENTS.NEW_PROGRAM);
michael@0 152 },
michael@0 153
michael@0 154 /**
michael@0 155 * Callback for the front's getPrograms() method.
michael@0 156 */
michael@0 157 _onProgramsAdded: function(programActors) {
michael@0 158 programActors.forEach(this._addProgram);
michael@0 159 window.emit(EVENTS.PROGRAMS_ADDED);
michael@0 160 },
michael@0 161
michael@0 162 /**
michael@0 163 * Adds a program to the shaders list and unhides any modal notices.
michael@0 164 */
michael@0 165 _addProgram: function(programActor) {
michael@0 166 $("#waiting-notice").hidden = true;
michael@0 167 $("#reload-notice").hidden = true;
michael@0 168 $("#content").hidden = false;
michael@0 169 ShadersListView.addProgram(programActor);
michael@0 170 }
michael@0 171 };
michael@0 172
michael@0 173 /**
michael@0 174 * Functions handling the sources UI.
michael@0 175 */
michael@0 176 let ShadersListView = Heritage.extend(WidgetMethods, {
michael@0 177 /**
michael@0 178 * Initialization function, called when the tool is started.
michael@0 179 */
michael@0 180 initialize: function() {
michael@0 181 this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
michael@0 182 showArrows: true,
michael@0 183 showItemCheckboxes: true
michael@0 184 });
michael@0 185
michael@0 186 this._onProgramSelect = this._onProgramSelect.bind(this);
michael@0 187 this._onProgramCheck = this._onProgramCheck.bind(this);
michael@0 188 this._onProgramMouseEnter = this._onProgramMouseEnter.bind(this);
michael@0 189 this._onProgramMouseLeave = this._onProgramMouseLeave.bind(this);
michael@0 190
michael@0 191 this.widget.addEventListener("select", this._onProgramSelect, false);
michael@0 192 this.widget.addEventListener("check", this._onProgramCheck, false);
michael@0 193 this.widget.addEventListener("mouseenter", this._onProgramMouseEnter, true);
michael@0 194 this.widget.addEventListener("mouseleave", this._onProgramMouseLeave, true);
michael@0 195 },
michael@0 196
michael@0 197 /**
michael@0 198 * Destruction function, called when the tool is closed.
michael@0 199 */
michael@0 200 destroy: function() {
michael@0 201 this.widget.removeEventListener("select", this._onProgramSelect, false);
michael@0 202 this.widget.removeEventListener("check", this._onProgramCheck, false);
michael@0 203 this.widget.removeEventListener("mouseenter", this._onProgramMouseEnter, true);
michael@0 204 this.widget.removeEventListener("mouseleave", this._onProgramMouseLeave, true);
michael@0 205 },
michael@0 206
michael@0 207 /**
michael@0 208 * Adds a program to this programs container.
michael@0 209 *
michael@0 210 * @param object programActor
michael@0 211 * The program actor coming from the active thread.
michael@0 212 */
michael@0 213 addProgram: function(programActor) {
michael@0 214 if (this.hasProgram(programActor)) {
michael@0 215 return;
michael@0 216 }
michael@0 217
michael@0 218 // Currently, there's no good way of differentiating between programs
michael@0 219 // in a way that helps humans. It will be a good idea to implement a
michael@0 220 // standard of allowing debuggees to add some identifiable metadata to their
michael@0 221 // program sources or instances.
michael@0 222 let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount);
michael@0 223 let contents = document.createElement("label");
michael@0 224 contents.className = "plain program-item";
michael@0 225 contents.setAttribute("value", label);
michael@0 226 contents.setAttribute("crop", "start");
michael@0 227 contents.setAttribute("flex", "1");
michael@0 228
michael@0 229 // Append a program item to this container.
michael@0 230 this.push([contents], {
michael@0 231 index: -1, /* specifies on which position should the item be appended */
michael@0 232 attachment: {
michael@0 233 label: label,
michael@0 234 programActor: programActor,
michael@0 235 checkboxState: true,
michael@0 236 checkboxTooltip: L10N.getStr("shadersList.blackboxLabel")
michael@0 237 }
michael@0 238 });
michael@0 239
michael@0 240 // Make sure there's always a selected item available.
michael@0 241 if (!this.selectedItem) {
michael@0 242 this.selectedIndex = 0;
michael@0 243 }
michael@0 244
michael@0 245 // Prevent this container from growing indefinitely in height when the
michael@0 246 // toolbox is docked to the side.
michael@0 247 if (gToolbox.hostType == "side" && this.itemCount == SHADERS_AUTOGROW_ITEMS) {
michael@0 248 this._pane.setAttribute("height", this._pane.getBoundingClientRect().height);
michael@0 249 }
michael@0 250 },
michael@0 251
michael@0 252 /**
michael@0 253 * Returns whether a program was already added to this programs container.
michael@0 254 *
michael@0 255 * @param object programActor
michael@0 256 * The program actor coming from the active thread.
michael@0 257 * @param boolean
michael@0 258 * True if the program was added, false otherwise.
michael@0 259 */
michael@0 260 hasProgram: function(programActor) {
michael@0 261 return !!this.attachments.filter(e => e.programActor == programActor).length;
michael@0 262 },
michael@0 263
michael@0 264 /**
michael@0 265 * The select listener for the programs container.
michael@0 266 */
michael@0 267 _onProgramSelect: function({ detail: sourceItem }) {
michael@0 268 if (!sourceItem) {
michael@0 269 return;
michael@0 270 }
michael@0 271 // The container is not empty and an actual item was selected.
michael@0 272 let attachment = sourceItem.attachment;
michael@0 273
michael@0 274 function getShaders() {
michael@0 275 return promise.all([
michael@0 276 attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()),
michael@0 277 attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader())
michael@0 278 ]);
michael@0 279 }
michael@0 280 function getSources([vertexShaderActor, fragmentShaderActor]) {
michael@0 281 return promise.all([
michael@0 282 vertexShaderActor.getText(),
michael@0 283 fragmentShaderActor.getText()
michael@0 284 ]);
michael@0 285 }
michael@0 286 function showSources([vertexShaderText, fragmentShaderText]) {
michael@0 287 return ShadersEditorsView.setText({
michael@0 288 vs: vertexShaderText,
michael@0 289 fs: fragmentShaderText
michael@0 290 });
michael@0 291 }
michael@0 292
michael@0 293 getShaders()
michael@0 294 .then(getSources)
michael@0 295 .then(showSources)
michael@0 296 .then(null, Cu.reportError);
michael@0 297 },
michael@0 298
michael@0 299 /**
michael@0 300 * The check listener for the programs container.
michael@0 301 */
michael@0 302 _onProgramCheck: function({ detail: { checked }, target }) {
michael@0 303 let sourceItem = this.getItemForElement(target);
michael@0 304 let attachment = sourceItem.attachment;
michael@0 305 attachment.isBlackBoxed = !checked;
michael@0 306 attachment.programActor[checked ? "unblackbox" : "blackbox"]();
michael@0 307 },
michael@0 308
michael@0 309 /**
michael@0 310 * The mouseenter listener for the programs container.
michael@0 311 */
michael@0 312 _onProgramMouseEnter: function(e) {
michael@0 313 let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
michael@0 314 if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
michael@0 315 sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT);
michael@0 316
michael@0 317 if (e instanceof Event) {
michael@0 318 e.preventDefault();
michael@0 319 e.stopPropagation();
michael@0 320 }
michael@0 321 }
michael@0 322 },
michael@0 323
michael@0 324 /**
michael@0 325 * The mouseleave listener for the programs container.
michael@0 326 */
michael@0 327 _onProgramMouseLeave: function(e) {
michael@0 328 let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
michael@0 329 if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
michael@0 330 sourceItem.attachment.programActor.unhighlight();
michael@0 331
michael@0 332 if (e instanceof Event) {
michael@0 333 e.preventDefault();
michael@0 334 e.stopPropagation();
michael@0 335 }
michael@0 336 }
michael@0 337 }
michael@0 338 });
michael@0 339
michael@0 340 /**
michael@0 341 * Functions handling the editors displaying the vertex and fragment shaders.
michael@0 342 */
michael@0 343 let ShadersEditorsView = {
michael@0 344 /**
michael@0 345 * Initialization function, called when the tool is started.
michael@0 346 */
michael@0 347 initialize: function() {
michael@0 348 XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map());
michael@0 349 this._vsFocused = this._onFocused.bind(this, "vs", "fs");
michael@0 350 this._fsFocused = this._onFocused.bind(this, "fs", "vs");
michael@0 351 this._vsChanged = this._onChanged.bind(this, "vs");
michael@0 352 this._fsChanged = this._onChanged.bind(this, "fs");
michael@0 353 },
michael@0 354
michael@0 355 /**
michael@0 356 * Destruction function, called when the tool is closed.
michael@0 357 */
michael@0 358 destroy: function() {
michael@0 359 this._toggleListeners("off");
michael@0 360 },
michael@0 361
michael@0 362 /**
michael@0 363 * Sets the text displayed in the vertex and fragment shader editors.
michael@0 364 *
michael@0 365 * @param object sources
michael@0 366 * An object containing the following properties
michael@0 367 * - vs: the vertex shader source code
michael@0 368 * - fs: the fragment shader source code
michael@0 369 * @return object
michael@0 370 * A promise resolving upon completion of text setting.
michael@0 371 */
michael@0 372 setText: function(sources) {
michael@0 373 let view = this;
michael@0 374 function setTextAndClearHistory(editor, text) {
michael@0 375 editor.setText(text);
michael@0 376 editor.clearHistory();
michael@0 377 }
michael@0 378
michael@0 379 return Task.spawn(function() {
michael@0 380 yield view._toggleListeners("off");
michael@0 381 yield promise.all([
michael@0 382 view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
michael@0 383 view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs))
michael@0 384 ]);
michael@0 385 yield view._toggleListeners("on");
michael@0 386 }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources));
michael@0 387 },
michael@0 388
michael@0 389 /**
michael@0 390 * Lazily initializes and returns a promise for an Editor instance.
michael@0 391 *
michael@0 392 * @param string type
michael@0 393 * Specifies for which shader type should an editor be retrieved,
michael@0 394 * either are "vs" for a vertex, or "fs" for a fragment shader.
michael@0 395 * @return object
michael@0 396 * Returns a promise that resolves to an editor instance
michael@0 397 */
michael@0 398 _getEditor: function(type) {
michael@0 399 if ($("#content").hidden) {
michael@0 400 return promise.reject(new Error("Shader Editor is still waiting for a WebGL context to be created."));
michael@0 401 }
michael@0 402 if (this._editorPromises.has(type)) {
michael@0 403 return this._editorPromises.get(type);
michael@0 404 }
michael@0 405
michael@0 406 let deferred = promise.defer();
michael@0 407 this._editorPromises.set(type, deferred.promise);
michael@0 408
michael@0 409 // Initialize the source editor and store the newly created instance
michael@0 410 // in the ether of a resolved promise's value.
michael@0 411 let parent = $("#" + type +"-editor");
michael@0 412 let editor = new Editor(DEFAULT_EDITOR_CONFIG);
michael@0 413 editor.config.mode = Editor.modes[type];
michael@0 414 editor.appendTo(parent).then(() => deferred.resolve(editor));
michael@0 415
michael@0 416 return deferred.promise;
michael@0 417 },
michael@0 418
michael@0 419 /**
michael@0 420 * Toggles all the event listeners for the editors either on or off.
michael@0 421 *
michael@0 422 * @param string flag
michael@0 423 * Either "on" to enable the event listeners, "off" to disable them.
michael@0 424 * @return object
michael@0 425 * A promise resolving upon completion of toggling the listeners.
michael@0 426 */
michael@0 427 _toggleListeners: function(flag) {
michael@0 428 return promise.all(["vs", "fs"].map(type => {
michael@0 429 return this._getEditor(type).then(editor => {
michael@0 430 editor[flag]("focus", this["_" + type + "Focused"]);
michael@0 431 editor[flag]("change", this["_" + type + "Changed"]);
michael@0 432 });
michael@0 433 }));
michael@0 434 },
michael@0 435
michael@0 436 /**
michael@0 437 * The focus listener for a source editor.
michael@0 438 *
michael@0 439 * @param string focused
michael@0 440 * The corresponding shader type for the focused editor (e.g. "vs").
michael@0 441 * @param string focused
michael@0 442 * The corresponding shader type for the other editor (e.g. "fs").
michael@0 443 */
michael@0 444 _onFocused: function(focused, unfocused) {
michael@0 445 $("#" + focused + "-editor-label").setAttribute("selected", "");
michael@0 446 $("#" + unfocused + "-editor-label").removeAttribute("selected");
michael@0 447 },
michael@0 448
michael@0 449 /**
michael@0 450 * The change listener for a source editor.
michael@0 451 *
michael@0 452 * @param string type
michael@0 453 * The corresponding shader type for the focused editor (e.g. "vs").
michael@0 454 */
michael@0 455 _onChanged: function(type) {
michael@0 456 setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));
michael@0 457
michael@0 458 // Remove all the gutter markers and line classes from the editor.
michael@0 459 this._cleanEditor(type);
michael@0 460 },
michael@0 461
michael@0 462 /**
michael@0 463 * Recompiles the source code for the shader being edited.
michael@0 464 * This function is fired at a certain delay after the user stops typing.
michael@0 465 *
michael@0 466 * @param string type
michael@0 467 * The corresponding shader type for the focused editor (e.g. "vs").
michael@0 468 */
michael@0 469 _doCompile: function(type) {
michael@0 470 Task.spawn(function() {
michael@0 471 let editor = yield this._getEditor(type);
michael@0 472 let shaderActor = yield ShadersListView.selectedAttachment[type];
michael@0 473
michael@0 474 try {
michael@0 475 yield shaderActor.compile(editor.getText());
michael@0 476 this._onSuccessfulCompilation();
michael@0 477 } catch (e) {
michael@0 478 this._onFailedCompilation(type, editor, e);
michael@0 479 }
michael@0 480 }.bind(this));
michael@0 481 },
michael@0 482
michael@0 483 /**
michael@0 484 * Called uppon a successful shader compilation.
michael@0 485 */
michael@0 486 _onSuccessfulCompilation: function() {
michael@0 487 // Signal that the shader was compiled successfully.
michael@0 488 window.emit(EVENTS.SHADER_COMPILED, null);
michael@0 489 },
michael@0 490
michael@0 491 /**
michael@0 492 * Called uppon an unsuccessful shader compilation.
michael@0 493 */
michael@0 494 _onFailedCompilation: function(type, editor, errors) {
michael@0 495 let lineCount = editor.lineCount();
michael@0 496 let currentLine = editor.getCursor().line;
michael@0 497 let listeners = { mouseenter: this._onMarkerMouseEnter };
michael@0 498
michael@0 499 function matchLinesAndMessages(string) {
michael@0 500 return {
michael@0 501 // First number that is not equal to 0.
michael@0 502 lineMatch: string.match(/\d{2,}|[1-9]/),
michael@0 503 // The string after all the numbers, semicolons and spaces.
michael@0 504 textMatch: string.match(/[^\s\d:][^\r\n|]*/)
michael@0 505 };
michael@0 506 }
michael@0 507 function discardInvalidMatches(e) {
michael@0 508 // Discard empty line and text matches.
michael@0 509 return e.lineMatch && e.textMatch;
michael@0 510 }
michael@0 511 function sanitizeValidMatches(e) {
michael@0 512 return {
michael@0 513 // Drivers might yield confusing line numbers under some obscure
michael@0 514 // circumstances. Don't throw the errors away in those cases,
michael@0 515 // just display them on the currently edited line.
michael@0 516 line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
michael@0 517 // Trim whitespace from the beginning and the end of the message,
michael@0 518 // and replace all other occurences of double spaces to a single space.
michael@0 519 text: e.textMatch[0].trim().replace(/\s{2,}/g, " ")
michael@0 520 };
michael@0 521 }
michael@0 522 function sortByLine(first, second) {
michael@0 523 // Sort all the errors ascending by their corresponding line number.
michael@0 524 return first.line > second.line ? 1 : -1;
michael@0 525 }
michael@0 526 function groupSameLineMessages(accumulator, current) {
michael@0 527 // Group errors corresponding to the same line number to a single object.
michael@0 528 let previous = accumulator[accumulator.length - 1];
michael@0 529 if (!previous || previous.line != current.line) {
michael@0 530 return [...accumulator, {
michael@0 531 line: current.line,
michael@0 532 messages: [current.text]
michael@0 533 }];
michael@0 534 } else {
michael@0 535 previous.messages.push(current.text);
michael@0 536 return accumulator;
michael@0 537 }
michael@0 538 }
michael@0 539 function displayErrors({ line, messages }) {
michael@0 540 // Add gutter markers and line classes for every error in the source.
michael@0 541 editor.addMarker(line, "errors", "error");
michael@0 542 editor.setMarkerListeners(line, "errors", "error", listeners, messages);
michael@0 543 editor.addLineClass(line, "error-line");
michael@0 544 }
michael@0 545
michael@0 546 (this._errors[type] = errors.link
michael@0 547 .split("ERROR")
michael@0 548 .map(matchLinesAndMessages)
michael@0 549 .filter(discardInvalidMatches)
michael@0 550 .map(sanitizeValidMatches)
michael@0 551 .sort(sortByLine)
michael@0 552 .reduce(groupSameLineMessages, []))
michael@0 553 .forEach(displayErrors);
michael@0 554
michael@0 555 // Signal that the shader wasn't compiled successfully.
michael@0 556 window.emit(EVENTS.SHADER_COMPILED, errors);
michael@0 557 },
michael@0 558
michael@0 559 /**
michael@0 560 * Event listener for the 'mouseenter' event on a marker in the editor gutter.
michael@0 561 */
michael@0 562 _onMarkerMouseEnter: function(line, node, messages) {
michael@0 563 if (node._markerErrorsTooltip) {
michael@0 564 return;
michael@0 565 }
michael@0 566
michael@0 567 let tooltip = node._markerErrorsTooltip = new Tooltip(document);
michael@0 568 tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X;
michael@0 569 tooltip.setTextContent({ messages: messages });
michael@0 570 tooltip.startTogglingOnHover(node, () => true, GUTTER_ERROR_PANEL_DELAY);
michael@0 571 },
michael@0 572
michael@0 573 /**
michael@0 574 * Removes all the gutter markers and line classes from the editor.
michael@0 575 */
michael@0 576 _cleanEditor: function(type) {
michael@0 577 this._getEditor(type).then(editor => {
michael@0 578 editor.removeAllMarkers("errors");
michael@0 579 this._errors[type].forEach(e => editor.removeLineClass(e.line));
michael@0 580 this._errors[type].length = 0;
michael@0 581 window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
michael@0 582 });
michael@0 583 },
michael@0 584
michael@0 585 _errors: {
michael@0 586 vs: [],
michael@0 587 fs: []
michael@0 588 }
michael@0 589 };
michael@0 590
michael@0 591 /**
michael@0 592 * Localization convenience methods.
michael@0 593 */
michael@0 594 let L10N = new ViewHelpers.L10N(STRINGS_URI);
michael@0 595
michael@0 596 /**
michael@0 597 * Convenient way of emitting events from the panel window.
michael@0 598 */
michael@0 599 EventEmitter.decorate(this);
michael@0 600
michael@0 601 /**
michael@0 602 * DOM query helper.
michael@0 603 */
michael@0 604 function $(selector, target = document) target.querySelector(selector);

mercurial