blob: 1ce4d371123fb442f697f50b2e448db52a6cbe37 [file] [log] [blame] [raw]
// Copyright (c) 2017, Compiler Explorer Authors
// 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 FontScale = require('../widgets/fontscale').FontScale;
var monaco = require('monaco-editor');
var _ = require('underscore');
var $ = require('jquery');
var ga = require('../analytics').ga;
var TomSelect = require('tom-select');
var PaneRenaming = require('../widgets/pane-renaming').PaneRenaming;
// note that these variables are saved to state, so don't change, only add to it
var DiffType_ASM = 0,
DiffType_CompilerStdOut = 1,
DiffType_CompilerStdErr = 2,
DiffType_ExecStdOut = 3,
DiffType_ExecStdErr = 4,
DiffType_GNAT_ExpandedCode = 5,
DiffType_GNAT_Tree = 6;
function State(id, model, difftype) {
this.id = id;
this.model = model;
this.compiler = null;
this.result = null;
this.difftype = difftype;
}
State.prototype.update = function (id, compiler, result) {
if (this.id !== id) return false;
this.compiler = compiler;
this.result = result;
this.refresh();
return true;
};
State.prototype.refresh = function () {
var output = [];
if (this.result) {
switch (this.difftype) {
case DiffType_ASM:
output = this.result.asm || [];
break;
case DiffType_CompilerStdOut:
output = this.result.stdout || [];
break;
case DiffType_CompilerStdErr:
output = this.result.stderr || [];
break;
case DiffType_ExecStdOut:
if (this.result.execResult) output = this.result.execResult.stdout || [];
break;
case DiffType_ExecStdErr:
if (this.result.execResult) output = this.result.execResult.stderr || [];
break;
case DiffType_GNAT_ExpandedCode:
if (this.result.hasGnatDebugOutput) output = this.result.gnatDebugOutput || [];
break;
case DiffType_GNAT_Tree:
if (this.result.hasGnatDebugTreeOutput) output = this.result.gnatDebugTreeOutput || [];
break;
}
}
this.model.setValue(_.pluck(output, 'text').join('\n'));
};
function getItemDisplayTitle(item) {
if (typeof item.id === 'string') {
var p = item.id.indexOf('_exec');
if (p !== -1) {
return 'Executor #' + item.id.substr(0, p);
}
}
return 'Compiler #' + item.id;
}
function Diff(hub, container, state) {
this.container = container;
this.eventHub = hub.createEventHub();
this.domRoot = container.getElement();
this.domRoot.html($('#diff').html());
this.compilers = {};
var root = this.domRoot.find('.monaco-placeholder');
this.outputEditor = monaco.editor.createDiffEditor(root[0], {
fontFamily: 'Consolas, "Liberation Mono", Courier, monospace',
scrollBeyondLastLine: true,
readOnly: true,
language: 'asm',
});
this.lhs = new State(state.lhs, monaco.editor.createModel('', 'asm'), state.lhsdifftype || DiffType_ASM);
this.rhs = new State(state.rhs, monaco.editor.createModel('', 'asm'), state.rhsdifftype || DiffType_ASM);
this.outputEditor.setModel({original: this.lhs.model, modified: this.rhs.model});
this.selectize = {};
this.domRoot[0].querySelectorAll('.difftype-picker').forEach(
_.bind(function (picker) {
var instance = new TomSelect(picker, {
sortField: 'name',
valueField: 'id',
labelField: 'name',
searchField: ['name'],
options: [
{id: DiffType_ASM, name: 'Assembly'},
{id: DiffType_CompilerStdOut, name: 'Compiler stdout'},
{id: DiffType_CompilerStdErr, name: 'Compiler stderr'},
{id: DiffType_ExecStdOut, name: 'Execution stdout'},
{id: DiffType_ExecStdErr, name: 'Execution stderr'},
{id: DiffType_GNAT_ExpandedCode, name: 'GNAT Expanded Code'},
{id: DiffType_GNAT_Tree, name: 'GNAT Tree Code'},
],
items: [],
render: {
option: function (item, escape) {
return '<div>' + escape(item.name) + '</div>';
},
},
dropdownParent: 'body',
plugins: ['input_autogrow'],
onChange: _.bind(function (value) {
if (picker.classList.contains('lhsdifftype')) {
this.lhs.difftype = parseInt(value);
this.lhs.refresh();
} else {
this.rhs.difftype = parseInt(value);
this.rhs.refresh();
}
this.updateState();
}, this),
});
if (picker.classList.contains('lhsdifftype')) {
this.selectize.lhsdifftype = instance;
} else {
this.selectize.rhsdifftype = instance;
}
}, this)
);
this.domRoot[0].querySelectorAll('.diff-picker').forEach(
_.bind(function (picker) {
var instance = new TomSelect(picker, {
sortField: 'name',
valueField: 'id',
labelField: 'name',
searchField: ['name'],
options: [],
items: [],
render: {
option: function (item, escape) {
var origin = item.editorId !== false ? 'Editor #' + item.editorId : 'Tree #' + item.treeId;
return (
'<div>' +
'<span class="compiler">' +
escape(item.compiler.name) +
'</span>' +
'<span class="options">' +
escape(item.options) +
'</span>' +
'<ul class="meta">' +
'<li class="editor">' +
escape(origin) +
'</li>' +
'<li class="compilerId">' +
escape(getItemDisplayTitle(item)) +
'</li>' +
'</ul></div>'
);
},
},
dropdownParent: 'body',
plugins: ['input_autogrow'],
onChange: _.bind(function (value) {
var compiler = this.compilers[value];
if (!compiler) return;
if (picker.classList.contains('lhs')) {
this.lhs.compiler = compiler;
this.lhs.id = compiler.id;
} else {
this.rhs.compiler = compiler;
this.rhs.id = compiler.id;
}
this.onDiffSelect(compiler.id);
}, this),
});
if (picker.classList.contains('lhs')) {
this.selectize.lhs = instance;
} else {
this.selectize.rhs = instance;
}
}, this)
);
this.paneRenaming = new PaneRenaming(this, state);
this.initButtons(state);
this.initCallbacks();
this.updateTitle();
this.updateCompilers();
ga.proxy('send', {
hitType: 'event',
eventCategory: 'OpenViewPane',
eventAction: 'Diff',
});
}
// TODO: de-dupe with compiler etc
Diff.prototype.resize = function () {
var topBarHeight = this.topBar.outerHeight(true);
this.outputEditor.layout({
width: this.domRoot.width(),
height: this.domRoot.height() - topBarHeight,
});
};
Diff.prototype.onDiffSelect = function (id) {
this.requestResendResult(id);
this.updateTitle();
this.updateState();
};
Diff.prototype.onCompileResult = function (id, compiler, result) {
// both sides must be updated, don't be tempted to rewrite this as
// var changes = lhs.update() || rhs.update();
var lhsChanged = this.lhs.update(id, compiler, result);
var rhsChanged = this.rhs.update(id, compiler, result);
if (lhsChanged || rhsChanged) {
this.updateTitle();
}
};
Diff.prototype.onExecuteResult = function (id, compiler, result) {
var compileResult = _.assign({}, result.buildResult);
compileResult.execResult = {
code: result.code,
stdout: result.stdout,
stderr: result.stderr,
};
this.onCompileResult(id + '_exec', compiler, compileResult);
};
Diff.prototype.initButtons = function (state) {
this.fontScale = new FontScale(this.domRoot, state, this.outputEditor);
this.topBar = this.domRoot.find('.top-bar');
};
Diff.prototype.initCallbacks = function () {
this.fontScale.on('change', _.bind(this.updateState, this));
this.paneRenaming.on('renamePane', this.updateState.bind(this));
this.eventHub.on('compileResult', this.onCompileResult, this);
this.eventHub.on('executeResult', this.onExecuteResult, this);
this.eventHub.on('compiler', this.onCompiler, this);
this.eventHub.on('compilerClose', this.onCompilerClose, this);
this.eventHub.on('executor', this.onExecutor, this);
this.eventHub.on('executorClose', this.onExecutorClose, this);
this.eventHub.on('settingsChange', this.onSettingsChange, this);
this.eventHub.on('themeChange', this.onThemeChange, this);
this.container.on(
'destroy',
function () {
this.eventHub.unsubscribe();
this.outputEditor.dispose();
},
this
);
this.container.on('resize', this.resize, this);
this.container.on('shown', this.resize, this);
this.requestResendResult(this.lhs.id);
this.requestResendResult(this.rhs.id);
this.eventHub.emit('findCompilers');
this.eventHub.emit('findExecutors');
this.eventHub.emit('requestTheme');
this.eventHub.emit('requestSettings');
};
Diff.prototype.requestResendResult = function (id) {
if (typeof id === 'string') {
var p = id.indexOf('_exec');
if (p !== -1) {
var execId = parseInt(id.substr(0, p));
this.eventHub.emit('resendExecution', execId);
}
} else {
this.eventHub.emit('resendCompilation', id);
}
};
Diff.prototype.onCompiler = function (id, compiler, options, editorId, treeId) {
if (!compiler) return;
options = options || '';
var name = compiler.name + ' ' + options;
// TODO: selectize doesn't play nicely with CSS tricks for truncation; this is the best I can do
// There's a plugin at: http://www.benbybenjacobs.com/blog/2014/04/09/no-wrap-plugin-for-selectize-dot-js
// but it doesn't look easy to integrate.
var maxLength = 30;
if (name.length > maxLength - 3) name = name.substr(0, maxLength - 3) + '...';
this.compilers[id] = {
id: id,
name: name,
options: options,
editorId: editorId,
treeId: treeId,
compiler: compiler,
};
if (!this.lhs.id) {
this.lhs.compiler = this.compilers[id];
this.lhs.id = id;
this.onDiffSelect(id);
} else if (!this.rhs.id) {
this.rhs.compiler = this.compilers[id];
this.rhs.id = id;
this.onDiffSelect(id);
}
this.updateCompilers();
};
Diff.prototype.onExecutor = function (id, compiler, options, editorId, treeId) {
this.onCompiler(id + '_exec', compiler, options, editorId, treeId);
};
Diff.prototype.onCompilerClose = function (id) {
delete this.compilers[id];
this.updateCompilers();
};
Diff.prototype.onExecutorClose = function (id) {
this.onCompilerClose(id + '_exec');
};
Diff.prototype.updateTitle = function () {
var name = this.paneName ? this.paneName : this.getPaneName();
this.container.setTitle(_.escape(name));
};
Diff.prototype.getPaneName = function () {
var name = 'Diff Viewer';
if (this.lhs.compiler && this.rhs.compiler) {
name += ' ' + this.lhs.compiler.name + ' vs ' + this.rhs.compiler.name;
}
return name;
};
Diff.prototype.updateCompilersFor = function (selectize, id) {
selectize.clearOptions();
_.each(
this.compilers,
function (compiler) {
selectize.addOption(compiler);
},
this
);
if (this.compilers[id]) {
selectize.setValue(id);
}
};
Diff.prototype.updateCompilers = function () {
this.updateCompilersFor(this.selectize.lhs, this.lhs.id);
this.updateCompilersFor(this.selectize.rhs, this.rhs.id);
this.selectize.lhsdifftype.setValue(this.lhs.difftype || DiffType_ASM);
this.selectize.rhsdifftype.setValue(this.rhs.difftype || DiffType_ASM);
};
Diff.prototype.updateState = function () {
var state = {
lhs: this.lhs.id,
rhs: this.rhs.id,
lhsdifftype: this.lhs.difftype,
rhsdifftype: this.rhs.difftype,
};
this.paneRenaming.addState(state);
this.fontScale.addState(state);
this.container.setState(state);
};
Diff.prototype.onThemeChange = function (newTheme) {
if (this.outputEditor) this.outputEditor.updateOptions({theme: newTheme.monaco});
};
Diff.prototype.onSettingsChange = function (newSettings) {
this.outputEditor.updateOptions({
minimap: {
enabled: newSettings.showMinimap,
},
fontFamily: newSettings.editorsFFont,
fontLigatures: newSettings.editorsFLigatures,
});
};
module.exports = {
Diff: Diff,
getComponent: function (lhs, rhs) {
return {
type: 'component',
componentName: 'diff',
componentState: {lhs: lhs, rhs: rhs},
};
},
};