Commit 52f2a814 authored by Victor Porof's avatar Victor Porof
Browse files

Bug 930928 - Shader compilation errors should be displayed in the editor,...

Bug 930928 - Shader compilation errors should be displayed in the editor, r=rcampbell,anton, a=bbajaj
parent 775ffb41
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -159,7 +159,7 @@ var Scratchpad = {
  {
    this._dirty = aValue;
    if (!aValue && this.editor)
      this.editor.markClean();
      this.editor.setClean();
    this._updateTitle();
  },

+118 −7
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ 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 {Tooltip} = require("devtools/shared/widgets/Tooltip");
const Editor = require("devtools/sourceeditor/editor");

// The panel's window global is an EventEmitter firing the following events:
@@ -31,11 +32,14 @@ const EVENTS = {
};

const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
const HIGHLIGHT_COLOR = [1, 0, 0, 1];
const TYPING_MAX_DELAY = 500;
const HIGHLIGHT_COLOR = [1, 0, 0, 1]; // rgba
const TYPING_MAX_DELAY = 500; // ms
const SHADERS_AUTOGROW_ITEMS = 4;
const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
const GUTTER_ERROR_PANEL_DELAY = 100; // ms
const DEFAULT_EDITOR_CONFIG = {
  mode: Editor.modes.text,
  gutters: ["errors"],
  lineNumbers: true,
  showAnnotationRuler: true
};
@@ -426,6 +430,9 @@ let ShadersEditorsView = {
   */
  _onChanged: function(type) {
    setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));

    // Remove all the gutter markers and line classes from the editor.
    this._cleanEditor(type);
  },

  /**
@@ -442,13 +449,117 @@ let ShadersEditorsView = {

      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.
        this._onSuccessfulCompilation();
      } catch (e) {
        this._onFailedCompilation(type, editor, e);
      }
    }.bind(this));
  },

  /**
   * Called uppon a successful shader compilation.
   */
  _onSuccessfulCompilation: function() {
    // Signal that the shader was compiled successfully.
    window.emit(EVENTS.SHADER_COMPILED, null);
  },

  /**
   * Called uppon an unsuccessful shader compilation.
   */
  _onFailedCompilation: function(type, editor, errors) {
    let lineCount = editor.lineCount();
    let currentLine = editor.getCursor().line;
    let listeners = { mouseenter: this._onMarkerMouseEnter };

    function matchLinesAndMessages(string) {
      return {
        // First number that is not equal to 0.
        lineMatch: string.match(/\d{2,}|[1-9]/),
        // The string after all the numbers, semicolons and spaces.
        textMatch: string.match(/[^\s\d:][^\r\n|]*/)
      };
    }
    function discardInvalidMatches(e) {
      // Discard empty line and text matches.
      return e.lineMatch && e.textMatch;
    }
    function sanitizeValidMatches(e) {
      return {
        // Drivers might yield retarded line numbers under some obscure
        // circumstances. Don't throw the errors away in those cases,
        // just display them on the currently edited line.
        line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
        // Trim whitespace from the beginning and the end of the message,
        // and replace all other occurences of double spaces to a single space.
        text: e.textMatch[0].trim().replace(/\s{2,}/g, " ")
      };
    }
    function sortByLine(first, second) {
      // Sort all the errors ascending by their corresponding line number.
      return first.line > second.line ? 1 : -1;
    }
    function groupSameLineMessages(accumulator, current) {
      // Group errors corresponding to the same line number to a single object.
      let previous = accumulator[accumulator.length - 1];
      if (!previous || previous.line != current.line) {
        return [...accumulator, {
          line: current.line,
          messages: [current.text]
        }];
      } else {
        previous.messages.push(current.text);
        return accumulator;
      }
    }
    function displayErrors({ line, messages }) {
      // Add gutter markers and line classes for every error in the source.
      editor.addMarker(line, "errors", "error");
      editor.setMarkerListeners(line, "errors", "error", listeners, messages);
      editor.addLineClass(line, "error-line");
    }

    (this._errors[type] = errors.link
      .split("ERROR")
      .map(matchLinesAndMessages)
      .filter(discardInvalidMatches)
      .map(sanitizeValidMatches)
      .sort(sortByLine)
      .reduce(groupSameLineMessages, []))
      .forEach(displayErrors);

    // Signal that the shader wasn't compiled successfully.
    window.emit(EVENTS.SHADER_COMPILED, errors);
  },

  /**
   * Event listener for the 'mouseenter' event on a marker in the editor gutter.
   */
  _onMarkerMouseEnter: function(line, node, messages) {
    if (node._markerErrorsTooltip) {
      return;
    }

    let tooltip = node._markerErrorsTooltip = new Tooltip(document);
    tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X;
    tooltip.setTextContent.apply(tooltip, messages);
    tooltip.startTogglingOnHover(node, () => true, GUTTER_ERROR_PANEL_DELAY);
  },

  /**
   * Removes all the gutter markers and line classes from the editor.
   */
  _cleanEditor: function(type) {
    this._getEditor(type).then(editor => {
      editor.removeAllMarkers("errors");
      this._errors[type].forEach(e => editor.removeLineClass(e.line));
      this._errors[type].length = 0;
    });
  },

  _errors: {
    vs: [],
    fs: []
  }
};

+2 −0
Original line number Diff line number Diff line
@@ -9,6 +9,8 @@ support-files =
[browser_se_aaa_run_first_leaktest.js]
[browser_se_bfcache.js]
[browser_se_editors-contents.js]
[browser_se_editors-error-gutter.js]
[browser_se_editors-error-tooltip.js]
[browser_se_editors-lazy-init.js]
[browser_se_first-run.js]
[browser_se_navigation.js]
+156 −0
Original line number Diff line number Diff line
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests if error indicators are shown in the editor's gutter and text area
 * when there's a shader compilation error.
 */

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 vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
  checkHasVertFirstError(true, vertError);
  checkHasVertSecondError(false, vertError);
  info("Error marks added in the vertex shader editor.");

  vsEditor.insertText(" ", { line: 1, ch: 0 });
  is(vsEditor.getText(1), "       precision lowp float;", "Typed space.");
  checkHasVertFirstError(false, vertError);
  checkHasVertSecondError(false, vertError);
  info("Error marks removed while typing in the vertex shader editor.");

  let vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
  checkHasVertFirstError(true, vertError);
  checkHasVertSecondError(false, vertError);
  info("Error marks were re-added after recompiling the vertex shader.");

  fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
  let fragError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
  checkHasVertFirstError(true, vertError);
  checkHasVertSecondError(false, vertError);
  checkHasFragError(true, fragError);
  info("Error marks added in the fragment shader editor.");

  fsEditor.insertText(" ", { line: 1, ch: 0 });
  is(fsEditor.getText(1), "       precision lowp float;", "Typed space.");
  checkHasVertFirstError(true, vertError);
  checkHasVertSecondError(false, vertError);
  checkHasFragError(false, fragError);
  info("Error marks removed while typing in the fragment shader editor.");

  let fragError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
  checkHasVertFirstError(true, vertError);
  checkHasVertSecondError(false, vertError);
  checkHasFragError(true, fragError);
  info("Error marks were re-added after recompiling the fragment shader.");

  vsEditor.replaceText("2", { line: 3, ch: 19 }, { line: 3, ch: 20 });
  checkHasVertFirstError(false, vertError);
  checkHasVertSecondError(false, vertError);
  checkHasFragError(true, fragError);
  info("Error marks removed while typing in the vertex shader editor again.");

  let vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
  checkHasVertFirstError(true, vertError);
  checkHasVertSecondError(true, vertError);
  checkHasFragError(true, fragError);
  info("Error marks were re-added after recompiling the fragment shader again.");

  yield teardown(panel);
  finish();

  function checkHasVertFirstError(bool, error) {
    ok(error, "Vertex shader compiled with errors.");
    isnot(error.link, "", "The linkage status should not be empty.");

    let line = 7;
    info("Checking first vertex shader error on line " + line + "...");

    is(vsEditor.hasMarker(line, "errors", "error"), bool,
      "Error is " + (bool ? "" : "not ") + "shown in the editor's gutter.");
    is(vsEditor.hasLineClass(line, "error-line"), bool,
      "Error style is " + (bool ? "" : "not ") + "applied to the faulty line.");

    let parsed = ShadersEditorsView._errors.vs;
    is(parsed.length >= 1, bool,
      "There's " + (bool ? ">= 1" : "< 1") + " parsed vertex shader error(s).");

    if (bool) {
      is(parsed[0].line, line,
        "The correct line was parsed.");
      is(parsed[0].messages.length, 2,
        "There are 2 parsed messages.");
      ok(parsed[0].messages[0].contains("'constructor' : too many arguments"),
        "The correct first message was parsed.");
      ok(parsed[0].messages[1].contains("'assign' : cannot convert from"),
        "The correct second message was parsed.");
    }
  }

  function checkHasVertSecondError(bool, error) {
    ok(error, "Vertex shader compiled with errors.");
    isnot(error.link, "", "The linkage status should not be empty.");

    let line = 8;
    info("Checking second vertex shader error on line " + line + "...");

    is(vsEditor.hasMarker(line, "errors", "error"), bool,
      "Error is " + (bool ? "" : "not ") + "shown in the editor's gutter.");
    is(vsEditor.hasLineClass(line, "error-line"), bool,
      "Error style is " + (bool ? "" : "not ") + "applied to the faulty line.");

    let parsed = ShadersEditorsView._errors.vs;
    is(parsed.length >= 2, bool,
      "There's " + (bool ? ">= 2" : "< 2") + " parsed vertex shader error(s).");

    if (bool) {
      is(parsed[1].line, line,
        "The correct line was parsed.");
      is(parsed[1].messages.length, 1,
        "There is 1 parsed message.");
      ok(parsed[1].messages[0].contains("'assign' : cannot convert from"),
        "The correct message was parsed.");
    }
  }

  function checkHasFragError(bool, error) {
    ok(error, "Fragment shader compiled with errors.");
    isnot(error.link, "", "The linkage status should not be empty.");

    let line = 5;
    info("Checking first vertex shader error on line " + line + "...");

    is(fsEditor.hasMarker(line, "errors", "error"), bool,
      "Error is " + (bool ? "" : "not ") + "shown in the editor's gutter.");
    is(fsEditor.hasLineClass(line, "error-line"), bool,
      "Error style is " + (bool ? "" : "not ") + "applied to the faulty line.");

    let parsed = ShadersEditorsView._errors.fs;
    is(parsed.length >= 1, bool,
      "There's " + (bool ? ">= 2" : "< 1") + " parsed fragment shader error(s).");

    if (bool) {
      is(parsed[0].line, line,
        "The correct line was parsed.");
      is(parsed[0].messages.length, 1,
        "There is 1 parsed message.");
      ok(parsed[0].messages[0].contains("'constructor' : too many arguments"),
        "The correct message was parsed.");
    }
  }
}

function once(aTarget, aEvent) {
  let deferred = promise.defer();
  aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
  return deferred.promise;
}
+59 −0
Original line number Diff line number Diff line
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests if error tooltips can be opened from the editor's gutter when there's
 * a shader compilation error.
 */

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 });
  yield once(panel.panelWin, EVENTS.SHADER_COMPILED);

  // Synthesizing 'mouseenter' events doesn't work, hack around this by
  // manually calling the event listener with the expected arguments.
  let editorDocument = vsEditor.container.contentDocument;
  let marker = editorDocument.querySelector(".error");
  let parsed = ShadersEditorsView._errors.vs[0].messages;
  ShadersEditorsView._onMarkerMouseEnter(7, marker, parsed);

  let tooltip = marker._markerErrorsTooltip;
  ok(tooltip, "A tooltip was created successfully.");

  let content = tooltip.content;
  ok(tooltip.content,
    "Some tooltip's content was set.");
  is(tooltip.content.className, "devtools-tooltip-simple-text-container",
    "The tooltip's content container was created correctly.");

  let messages = content.childNodes;
  is(messages.length, 2,
    "There are two messages displayed in the tooltip.");
  is(messages[0].className, "devtools-tooltip-simple-text",
    "The first message was created correctly.");
  is(messages[1].className, "devtools-tooltip-simple-text",
    "The second message was created correctly.");

  ok(messages[0].textContent.contains("'constructor' : too many arguments"),
    "The first message contains the correct text.");
  ok(messages[1].textContent.contains("'assign' : cannot convert"),
    "The second message contains the correct text.");

  yield teardown(panel);
  finish();
}

function once(aTarget, aEvent) {
  let deferred = promise.defer();
  aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
  return deferred.promise;
}
Loading