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);