blob: 29e4cc72ccdc4d6064ba5a4b991c10e9c7679b91 [file] [log] [blame] [raw]
// Copyright (c) 2017, Rubén Rincón
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
"use strict";
var options = require('options');
var _ = require('underscore');
var $ = require('jquery');
require('selectize');
function Conformance(hub, container, state) {
this.container = container;
this.eventHub = hub.createEventHub();
this.compilerService = hub.compilerService;
this.domRoot = container.getElement();
this.domRoot.html($('#conformance').html());
this.selectorList = this.domRoot.find('.compiler-list');
this.addCompilerButton = this.domRoot.find('.add-compiler');
this.selectorTemplate = $('#compiler-selector').find('.compiler-row');
this.editorId = state.editorid;
this.nextSelectorId = 0;
this.maxCompilations = options.cvCompilerCountMax || 6;
this.langId = state.langId || _.keys(options.languages)[0];
this.source = state.source || "";
this.status = {
allowCompile: false,
allowAdd: true
};
this.stateByLang = {};
this.container.on('destroy', function () {
this.eventHub.unsubscribe();
this.eventHub.emit("conformanceViewClose", this.editorId);
}, this);
this.container.on('destroy', this.close, this);
this.container.on('open', function () {
this.eventHub.emit("conformanceViewOpen", this.editorId);
}, this);
this.eventHub.on('resize', this.resize, this);
this.container.on('resize', this.resize, this);
this.eventHub.on('editorChange', this.onEditorChange, this);
this.eventHub.on('editorClose', this.onEditorClose, this);
this.eventHub.on('languageChange', this.onLanguageChange, this);
this.container.on('shown', this.resize, this);
this.eventHub.on('editorChange', this.onEditorChange, this);
this.eventHub.on('editorClose', this.onEditorClose, this);
this.addCompilerButton.on('click', _.bind(function () {
this.addCompilerSelector();
this.saveState();
}, this));
if (state.compilers) {
_.each(state.compilers, _.bind(function (config) {
config.cv = this.nextSelectorId;
this.nextSelectorId++;
this.addCompilerSelector(config);
}, this));
}
this.handleToolbarUI();
}
Conformance.prototype.setTitle = function (compilerCount) {
this.container.setTitle("Conformance viewer (Editor #" + this.editorId + ") " + (
compilerCount !== 0 ? (compilerCount + "/" + this.maxCompilations) : ""
));
};
Conformance.prototype.addCompilerSelector = function (config) {
if (!config) {
config = {
// Code we have
cv: this.nextSelectorId,
// Compiler id which is being used
compilerId: "",
// Options which are in use
options: ""
};
this.nextSelectorId++;
}
config.cv = Number(config.cv);
var newEntry = this.selectorTemplate.clone();
newEntry.attr("data-cv", config.cv);
var onOptionsChange = _.debounce(_.bind(function () {
this.saveState();
this.compileAll();
}, this), 800);
newEntry.find('.options')
.attr("data-cv", config.cv)
.val(config.options)
.on("click", onOptionsChange)
.on("keyup", onOptionsChange);
newEntry.find('.close')
.attr("data-cv", config.cv)
.on("click", _.bind(function () {
this.removeCompilerSelector(config.cv);
}, this));
this.selectorList.append(newEntry);
var status = newEntry.find('.status').attr("data-cv", config.cv);
var langId = this.langId;
var isVisible = function (compiler) {
return compiler.lang === langId;
};
newEntry.find('.compiler-picker')
.attr("data-cv", config.cv)
.selectize({
sortField: 'name',
valueField: 'id',
labelField: 'name',
searchField: ['name'],
options: _.filter(options.compilers, isVisible),
items: config.compilerId ? [config.compilerId] : []
})
.on('change', _.bind(function () {
// Hide the results button when a new compiler is selected
this.handleStatusIcon(status, {code: 0, text: ""});
// We could narrow the compilation to only this compiler!
this.compileAll();
// We're not saving state here. It's done after compiling
}, this));
this.handleStatusIcon(status, {code: 0, text: ""});
this.handleToolbarUI();
this.saveState();
};
Conformance.prototype.removeCompilerSelector = function (cv) {
_.each(this.selectorList.children(), function (row) {
var child = $(row);
if (child.attr("data-cv") === cv) {
child.remove();
}
}, this);
this.handleToolbarUI();
this.saveState();
};
Conformance.prototype.onEditorChange = function (editorId, newSource, langId) {
if (editorId === this.editorId) {
this.langId = langId;
this.source = newSource;
this.compileAll();
}
};
Conformance.prototype.onEditorClose = function (editorId) {
if (editorId === this.editorId) {
this.close();
_.defer(function (self) {
self.container.close();
}, this);
}
};
Conformance.prototype.onCompileResponse = function (cv, result) {
var allText = _.pluck((result.stdout || []).concat(result.stderr || []), 'text').join("\n");
var failed = result.code !== 0;
var warns = !failed && !!allText;
var status = {
text: allText.replace(/\x1b\\[[0-9;]*m/, ''),
code: failed ? 3 : (warns ? 2 : 1)
};
this.handleStatusIcon(this.selectorList.find('[data-cv="' + cv + '"] .status'), status);
this.saveState();
};
Conformance.prototype.compileAll = function () {
if (!this.source) return;
// Hide previous status icons
this.selectorList.find('.status').css("visibility", "hidden");
this.compilerService.expand(this.source).then(_.bind(function (expanded) {
var compileCount = 0;
_.each(this.selectorList.children(), _.bind(function (child) {
var picker = $(child).find('.compiler-picker');
// We make sure we are not over our limit
if (picker && compileCount < this.maxCompilations) {
compileCount++;
if (picker.val()) {
var cv = Number(picker.attr("data-cv"));
var request = {
source: expanded || "",
compiler: picker.val(),
options: {
userArguments: $(child).find(".options[data-cv='" + cv + "']").val(),
filters: {},
compilerOptions: {produceAst: false, produceOptInfo: false}
}
};
// This error function ensures that the user will know we had a problem (As we don't save asm)
this.compilerService.submit(request)
.then(_.bind(function (x) {
this.onCompileResponse(cv, x.result);
}, this))
.catch(_.bind(function (x) {
this.onCompileResponse(cv, {
asm: "",
code: -1,
stdout: "",
stderr: x.error
});
}, this));
}
}
}, this));
}, this));
};
Conformance.prototype.handleToolbarUI = function () {
var compilerCount = this.selectorList.children().length;
// Only allow new compilers if we allow for more
this.addCompilerButton.attr("disabled", compilerCount >= this.maxCompilations);
this.setTitle(compilerCount);
};
Conformance.prototype.handleStatusIcon = function (element, status) {
if (!element) return;
function setGlyphClass(element) {
element.toggleClass('glyphicon-remove-sign', status.code === 3);
element.toggleClass('glyphicon-info-sign', status.code === 2);
element.toggleClass('glyphicon-ok-sign', status.code !== 3 && status.code !== 2);
}
function ariaLabel(code) {
if (code === 3) return "Compilation failed";
if (code === 2) return "Compiled with warnings";
return "Compiled without warnings";
}
function color(code) {
if (code === 3) return "red";
if (code === 2) return "yellow";
return "green";
}
element
.toggleClass('status', true)
.css("visibility", status.code === 0 ? "hidden" : "visible")
.css("color", color(status.code))
.prop("title", status.text.replace(/\x1b\[[0-9;]*m(.\[K)?/g, ''))
.prop("aria-label", ariaLabel(status.code))
.prop("data-status", status.code);
setGlyphClass(element);
};
Conformance.prototype.currentState = function () {
var state = {
editorid: this.editorId,
langId: this.langId,
compilers: []
};
_.each(this.selectorList.children(), _.bind(function (child) {
state.compilers.push({
// Code we have
cv: $(child).attr("data-cv"),
// Compiler which is being used
compilerId: $(child).find('.compiler-picker').val(),
// Options which are in use
options: $(child).find(".options").val()
});
}, this));
return state;
};
Conformance.prototype.saveState = function () {
this.container.setState(this.currentState());
};
Conformance.prototype.resize = function () {
this.selectorList.css("height", this.domRoot.height() - this.domRoot.find('.top-bar').outerHeight(true));
};
Conformance.prototype.onLanguageChange = function (editorId, newLangId) {
if (editorId === this.editorId && this.langId !== newLangId) {
var oldLangId = this.langId;
this.stateByLang[oldLangId] = this.currentState();
this.langId = newLangId;
this.selectorList.children().remove();
this.nextSelectorId = 0;
if (this.stateByLang[newLangId]) {
this.initFromState(this.stateByLang[newLangId]);
}
this.handleToolbarUI();
this.saveState();
}
};
Conformance.prototype.close = function () {
this.eventHub.unsubscribe();
this.eventHub.emit("conformanceViewClose", this.editorId);
};
Conformance.prototype.initFromState = function (state) {
if (state.compilers) {
_.each(state.compilers, _.bind(function (config) {
config.cv = this.nextSelectorId;
this.nextSelectorId++;
this.addCompilerSelector(config);
}, this));
}
};
module.exports = {
Conformance: Conformance
};