blob: 7eecfb8023907b7fd793e6a926da67380ac944e5 [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');
var Promise = require('es6-promise').Promise;
var ga = require('../analytics');
var Components = require('../components');
require('selectize');
function Conformance(hub, container, state) {
this.hub = hub;
this.container = container;
this.eventHub = hub.createEventHub();
this.compilerService = hub.compilerService;
this.domRoot = container.getElement();
this.domRoot.html($('#conformance').html());
this.editorId = state.editorid;
this.maxCompilations = options.cvCompilerCountMax || 6;
this.langId = state.langId || _.keys(options.languages)[0];
this.source = state.source || "";
this.sourceNeedsExpanding = true;
this.expandedSource = null;
this.status = {
allowCompile: false,
allowAdd: true
};
this.stateByLang = {};
this.initButtons();
this.initLibraries(state);
this.updateLibsDropdown();
this.initCallbacks();
this.initFromState(state);
this.handleToolbarUI();
ga.proxy('send', {
hitType: 'event',
eventCategory: 'OpenViewPane',
eventAction: 'Conformance'
});
// Dismiss the popover on escape.
$(document).on('keyup.editable', _.bind(function (e) {
if (e.which === 27) {
this.libsButton.popover('hide');
}
}, this));
// Dismiss on any click that isn't either in the opening element, inside
// the popover or on any alert
$(document).on('click', _.bind(function (e) {
var elem = this.libsButton;
var target = $(e.target);
if (!target.is(elem) && elem.has(target).length === 0 && target.closest('.popover').length === 0) {
elem.popover('hide');
}
}, this));
}
Conformance.prototype.initLibraries = function (state) {
this.availableLibs = {};
this.updateAvailableLibs();
_.each(state.libs, _.bind(function (lib) {
this.markLibraryAsUsed(lib.name, lib.ver);
}, this));
};
Conformance.prototype.updateAvailableLibs = function () {
if (!this.availableLibs[this.langId]) {
this.availableLibs[this.langId] = $.extend(true, {}, options.libs[this.langId]);
this.initLangDefaultLibs();
}
};
Conformance.prototype.initLangDefaultLibs = function () {
var defaultLibs = options.defaultLibs[this.langId];
if (!defaultLibs) return;
_.each(defaultLibs.split(':'), _.bind(function (libPair) {
var pairSplits = libPair.split('.');
if (pairSplits.length === 2) {
var lib = pairSplits[0];
var ver = pairSplits[1];
this.markLibraryAsUsed(lib, ver);
}
}, this));
};
Conformance.prototype.updateLibsDropdown = function () {
this.updateAvailableLibs();
this.libsButton.popover({
container: 'body',
content: _.bind(function () {
var libsCount = _.keys(this.availableLibs[this.langId]).length;
if (libsCount === 0) return this.noLibsPanel;
var columnCount = Math.ceil(libsCount / 5);
var currentLibIndex = -1;
var libLists = [];
for (var i = 0; i < columnCount; i++) {
libLists.push($('<ul></ul>').addClass('lib-list'));
}
// Utility function so we can iterate indefinitely over our lists
var getNextList = function () {
currentLibIndex = (currentLibIndex + 1) % columnCount;
return libLists[currentLibIndex];
};
var handleArrow = function (libGroup, libArrow) {
var anyInUse = _.any(libGroup.children().children('input'), function (element) {
return $(element).prop('checked');
});
var isVisible = libGroup.is(":visible");
libArrow.toggleClass('lib-arrow-up', isVisible);
libArrow.toggleClass('lib-arrow-down', !isVisible);
libArrow.toggleClass('lib-arrow-used', anyInUse);
};
var onChecked = _.bind(function (e) {
var elem = $(e.target);
// Uncheck every lib checkbox with the same name if we're checking the target
if (elem.prop('checked')) {
_.each(e.data.group.children().children('input'), function (other) {
$(other).prop('checked', false);
});
// Recheck the targeted one
elem.prop('checked', true);
}
// And now do the same with the availableLibs object
_.each(this.availableLibs[this.langId][elem.prop('data-lib')].versions, function (version) {
version.used = false;
});
this.availableLibs[this.langId][elem.prop('data-lib')]
.versions[elem.prop('data-version')].used = elem.prop('checked');
handleArrow(e.data.group, e.data.arrow);
this.saveState();
this.compileAll();
}, this);
_.each(this.availableLibs[this.langId], function (lib, libKey) {
var libsList = getNextList();
var libArrow = $('<div></div>').addClass('lib-arrow');
var libName = $('<span></span>').text(lib.name);
var libHeader = $('<span></span>')
.addClass('lib-header')
.append(libArrow)
.append(libName);
if (lib.url && lib.url.length > 0) {
libHeader.append($('<a></a>')
.css("float", "right")
.addClass('opens-new-window')
.prop('href', lib.url)
.prop('target', '_blank')
.prop('rel', 'noopener noreferrer')
.append($('<sup></sup>')
.addClass('glyphicon glyphicon-new-window')
)
);
}
if (lib.description && lib.description.length > 0) {
libName
.addClass('lib-described')
.prop('title', lib.description);
}
var libCat = $('<li></li>')
.append(libHeader)
.addClass('lib-item');
var libGroup = $('<div></div>');
if (libsList.children().length > 0)
libsList.append($('<hr>').addClass('lib-separator'));
_.each(lib.versions, function (version, vKey) {
var verCheckbox = $('<input type="checkbox">')
.addClass('lib-checkbox')
.prop('data-lib', libKey)
.prop('data-version', vKey)
.prop('checked', version.used)
.prop('name', libKey)
.on('change', {arrow: libArrow, group: libGroup}, onChecked);
libGroup
.append($('<div></div>')
.append(verCheckbox)
.append($('<label></label>')
.addClass('lib-label')
.text(version.version)
.on('click', function () {
verCheckbox.trigger('click');
})
)
);
});
libGroup.hide();
handleArrow(libGroup, libArrow);
libHeader.on('click', function () {
libGroup.toggle();
handleArrow(libGroup, libArrow);
});
libGroup.appendTo(libCat);
libCat.appendTo(libsList);
});
return $('<div></div>').addClass('libs-container').append(libLists);
}, this),
html: true,
placement: 'bottom',
trigger: 'manual'
}).click(_.bind(function () {
this.libsButton.popover('show');
}, this)).on('show.bs.popover', function () {
$(this).data('bs.popover').tip().css('max-width', '100%').css('width', 'auto');
});
};
Conformance.prototype.markLibraryAsUsed = function (name, version) {
if (this.availableLibs[this.langId] &&
this.availableLibs[this.langId][name] &&
this.availableLibs[this.langId][name].versions[version]) {
this.availableLibs[this.langId][name].versions[version].used = true;
}
};
Conformance.prototype.initButtons = function () {
this.selectorList = this.domRoot.find('.compiler-list');
this.addCompilerButton = this.domRoot.find('.add-compiler');
this.selectorTemplate = $('#compiler-selector').find('.compiler-row');
this.topBar = this.domRoot.find('.top-bar');
this.libsButton = this.topBar.find('.show-libs');
this.libsTemplates = $('.template #libs-dropdown');
this.noLibsPanel = this.libsTemplates.children('.no-libs');
this.hideable = this.domRoot.find('.hideable');
};
Conformance.prototype.initCallbacks = function () {
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.container.on('resize', this.resize, this);
this.container.on('shown', this.resize, this);
this.eventHub.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.addCompilerButton.on('click', _.bind(function () {
this.addCompilerSelector();
this.saveState();
}, this));
};
Conformance.prototype.setTitle = function (compilerCount) {
this.container.setTitle("Conformance viewer (Editor #" + this.editorId + ") " + (
compilerCount !== 0 ? (compilerCount + "/" + this.maxCompilations) : ""
));
};
Conformance.prototype.getGroupsInUse = function () {
return _.chain(this.compilerService.getCompilersForLang(this.langId))
.map()
.uniq(false, function (compiler) {
return compiler.group;
})
.map(function (compiler) {
return {value: compiler.group, label: compiler.groupName || compiler.group};
})
.sortBy('label')
.value();
};
Conformance.prototype.addCompilerSelector = function (config) {
if (!config) {
config = {
// Compiler id which is being used
compilerId: "",
// Options which are in use
options: ""
};
}
var newEntry = this.selectorTemplate.clone();
var onOptionsChange = _.debounce(_.bind(function () {
this.saveState();
this.compileChild(newEntry);
}, this), 800);
var optionsField = newEntry.find('.options')
.val(config.options)
.on("change", onOptionsChange)
.on("keyup", onOptionsChange);
newEntry.find('.close')
.on("click", _.bind(function () {
this.removeCompilerSelector(newEntry);
}, this));
this.selectorList.append(newEntry);
var status = newEntry.find('.status');
var prependOptions = newEntry.find('.prepend-options');
var langId = this.langId;
var isVisible = function (compiler) {
return compiler.lang === langId;
};
var onCompilerChange = _.bind(function (compilerId) {
// Hide the results icon when a new compiler is selected
this.handleStatusIcon(status, {code: 0, text: ""});
var compiler = this.compilerService.findCompiler(langId, compilerId);
if (compiler) {
prependOptions.prop('title', compiler.options);
prependOptions.toggle(!!compiler.options);
} else {
prependOptions.hide();
}
}, this);
var compilerPicker = newEntry.find('.compiler-picker')
.selectize({
sortField: [
{field: '$order'},
{field: 'name'}
],
valueField: 'id',
labelField: 'name',
searchField: ['name'],
optgroupField: 'group',
optgroups: this.getGroupsInUse(),
options: _.filter(options.compilers, isVisible),
items: config.compilerId ? [config.compilerId] : []
})
.on('change', _.bind(function (e) {
onCompilerChange($(e.target).val());
this.compileChild(newEntry);
}, this));
onCompilerChange(config.compilerId);
var popCompilerButton = $('<td></td>')
.append($('<button></button>')
.addClass('close glyphicon glyphicon-share-alt')
);
newEntry.append(popCompilerButton);
var getCompilerConfig = _.bind(function () {
return Components.getCompilerWith(
this.editorId, undefined, optionsField.val(), compilerPicker.val(), this.langId, this.lastState.libs);
}, this);
this.container.layoutManager
.createDragSource(popCompilerButton, getCompilerConfig);
popCompilerButton.click(_.bind(function () {
var insertPoint = this.hub.findParentRowOrColumn(this.container) ||
this.container.layoutManager.root.contentItems[0];
insertPoint.addChild(getCompilerConfig);
}, this));
this.handleToolbarUI();
this.saveState();
};
Conformance.prototype.removeCompilerSelector = function (element) {
if (element) element.remove();
this.handleToolbarUI();
this.saveState();
};
Conformance.prototype.expandSource = function () {
if (this.sourceNeedsExpanding || !this.expandedSource) {
return this.compilerService.expand(this.source).then(_.bind(function (expandedSource) {
this.expandedSource = expandedSource;
this.sourceNeedsExpanding = false;
return expandedSource;
}, this));
}
return Promise.resolve(this.expandedSource);
};
Conformance.prototype.onEditorChange = function (editorId, newSource, langId) {
if (editorId === this.editorId) {
this.langId = langId;
this.source = newSource;
this.sourceNeedsExpanding = true;
this.compileAll();
}
};
Conformance.prototype.onEditorClose = function (editorId) {
if (editorId === this.editorId) {
this.close();
_.defer(function (self) {
self.container.close();
}, this);
}
};
Conformance.prototype.onCompileResponse = function (child, 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)
};
child.find('.prepend-options')
.toggle(!!result.compilationOptions)
.prop('title', result.compilationOptions ? result.compilationOptions.join(' ') : '');
this.handleStatusIcon(child.find('.status'), status);
this.saveState();
};
Conformance.prototype.compileChild = function (child) {
// Hide previous status icons
var picker = child.find('.compiler-picker');
if (!picker || !picker.val()) return;
this.handleStatusIcon(child.find('.status'), {
code: 4, // Compiling code
text: "Compiling"
});
this.expandSource().then(_.bind(function (expandedSource) {
var request = {
source: expandedSource,
compiler: picker.val(),
options: {
userArguments: child.find(".options").val() || "",
filters: {},
compilerOptions: {produceAst: false, produceOptInfo: false}
}
};
var compiler = this.compilerService.findCompiler(this.langId, picker.val());
var includeFlag = compiler ? compiler.includeFlag : '-I';
_.each(this.availableLibs[this.langId], function (lib) {
_.each(lib.versions, function (version) {
if (version.used) {
_.each(version.path, function (path) {
request.options.userArguments += ' ' + includeFlag + path;
});
}
});
});
// 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(child, x.result);
}, this))
.catch(_.bind(function (x) {
this.onCompileResponse(child, {
asm: "",
code: -1,
stdout: "",
stderr: x.error
});
}, this));
}, this));
};
Conformance.prototype.compileAll = function () {
_.each(this.selectorList.children(), _.bind(function (child) {
this.compileChild($(child));
}, this));
};
Conformance.prototype.handleToolbarUI = function () {
var compilerCount = this.selectorList.children().length;
// Only allow new compilers if we allow for more
this.addCompilerButton.prop("disabled", compilerCount >= this.maxCompilations);
this.setTitle(compilerCount);
};
Conformance.prototype.handleStatusIcon = function (element, status) {
if (!element) return;
function ariaLabel(code) {
if (code === 4) return "Compiling";
if (code === 3) return "Compilation failed";
if (code === 2) return "Compiled with warnings";
return "Compiled without warnings";
}
function color(code) {
if (code === 4) return "black";
if (code === 3) return "red";
if (code === 2) return "#BBBB00";
return "green";
}
element
.toggleClass('status', true)
.css("color", color(status.code))
.toggle(status.code !== 0)
.prop("title", status.text)
.prop("aria-label", ariaLabel(status.code))
.prop("data-status", status.code);
element.toggleClass('glyphicon-tasks', status.code === 4);
element.toggleClass('glyphicon-remove-sign', status.code === 3);
element.toggleClass('glyphicon-info-sign', status.code === 2);
element.toggleClass('glyphicon-ok-sign', status.code === 1);
};
Conformance.prototype.currentState = function () {
var libs = [];
_.each(this.availableLibs[this.langId], function (library, name) {
_.each(library.versions, function (version, ver) {
if (library.versions[ver].used) {
libs.push({name: name, ver: ver});
}
});
});
var compilers = _.map(this.selectorList.children(), function (child) {
child = $(child);
return {
compilerId: child.find('.compiler-picker').val() || "",
options: child.find(".options").val() || ""
};
});
return {
editorid: this.editorId,
langId: this.langId,
compilers: compilers,
libs: libs
};
};
Conformance.prototype.saveState = function () {
this.lastState = this.currentState();
this.container.setState(this.lastState);
};
Conformance.prototype.resize = function () {
this.updateHideables();
this.selectorList.css("height", this.domRoot.height() - this.topBar.outerHeight(true));
};
Conformance.prototype.updateHideables = function () {
this.hideable.toggle(this.domRoot.width() > this.addCompilerButton.width());
};
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();
var langState = this.stateByLang[newLangId];
this.initFromState(langState);
this.updateLibsDropdown();
this.handleToolbarUI();
this.saveState();
}
};
Conformance.prototype.close = function () {
this.eventHub.unsubscribe();
this.eventHub.emit("conformanceViewClose", this.editorId);
};
Conformance.prototype.initFromState = function (state) {
if (state && state.compilers) {
this.lastState = state;
_.each(state.compilers, _.bind(this.addCompilerSelector, this));
} else {
this.lastState = this.currentState();
}
};
module.exports = {
Conformance: Conformance
};