blob: 0eb75b71b5657b485d11c46dc20e9359f0df55f7 [file] [log] [blame] [raw]
define(function (require) {
"use strict";
var CodeMirror = require('codemirror');
var _ = require('underscore');
var colour = require('colour');
require('codemirror/mode/clike/clike');
require('codemirror/mode/d/d');
require('codemirror/mode/go/go');
require('codemirror/mode/rust/rust');
function Editor(hub, state, container, lang, defaultSrc) {
var self = this;
this.id = state.id || hub.nextId();
this.eventHub = container.layoutManager.eventHub;
this.widgetsByCompiler = {};
this.asmByCompiler = {};
this.container = container;
var domRoot = container.getElement();
domRoot.html($('#codeEditor').html());
var cmMode;
switch (lang.toLowerCase()) {
default:
cmMode = "text/x-c++src";
break;
case "c":
cmMode = "text/x-c";
break;
case "rust":
cmMode = "text/x-rustsrc";
break;
case "d":
cmMode = "text/x-d";
break;
case "go":
cmMode = "text/x-go";
break;
}
this.editor = CodeMirror.fromTextArea(domRoot.find("textarea")[0], {
lineNumbers: true,
matchBrackets: true,
useCPP: true,
mode: cmMode
});
if (state.src) {
this.editor.setValue(state.src);
} else if (defaultSrc) {
this.editor.setValue(defaultSrc);
}
// With reference to "fix typing '#' in emacs mode"
// https://github.com/mattgodbolt/gcc-explorer/pull/131
this.editor.setOption("extraKeys", {
"Alt-F": false
});
// We suppress posting changes until the user has stopped typing by:
// * Using _.debounce() to run emitChange on any key event or change
// only after a delay.
// * Only actually triggering a change if the document text has changed from
// the previous emitted.
this.lastChangeEmitted = null;
var ChangeDebounceMs = 500;
this.debouncedEmitChange = _.debounce(function () {
self.maybeEmitChange();
}, ChangeDebounceMs);
this.editor.on("change", _.bind(function () {
this.debouncedEmitChange();
this.updateState();
}, this));
this.editor.on("keydown", _.bind(function () {
// Not strictly a change; but this suppresses changes until some time
// after the last key down (be it an actual change or a just a cursor
// movement etc).
this.debouncedEmitChange();
}, this));
// A small debounce used to give multiple compilers a chance to respond
// before unioning the colours and updating them. Another approach to reducing
// flicker as multiple compilers update is to track those compilers which
// are busy, and only union/update colours when all are complete.
var ColourDebounceMs = 200;
this.debouncedUpdateColours = _.debounce(function (colours) {
self.updateColours(colours);
}, ColourDebounceMs);
function resize() {
self.editor.setSize(domRoot.width(), domRoot.height());
self.editor.refresh();
}
container.on('resize', resize);
container.on('open', function () {
self.eventHub.emit('editorOpen', self.id);
});
container.on('close', function () {
self.eventHub.emit('editorClose', self.id);
});
container.setTitle(lang + " source");
this.container.layoutManager.on('initialised', function () {
// Once initialized, let everyone know what text we have.
self.maybeEmitChange();
});
this.eventHub.on('compilerClose', this.onCompilerClose, this);
this.eventHub.on('compileResult', this.onCompileResponse, this);
}
Editor.prototype.maybeEmitChange = function () {
var source = this.getSource();
if (source == this.lastChangeEmitted) return;
this.lastChangeEmitted = this.getSource();
this.eventHub.emit('editorChange', this.id, this.lastChangeEmitted);
};
Editor.prototype.updateState = function () {
this.container.setState({
id: this.id,
src: this.getSource()
});
};
Editor.prototype.getSource = function () {
return this.editor.getValue();
};
function makeErrorNode(text, compiler) {
var clazz = "error";
if (text.match(/^warning/)) clazz = "warning";
if (text.match(/^note/)) clazz = "note";
var node = $('<div class="' + clazz + ' inline-msg"><span class="icon">!!</span><span class="compiler">: </span><span class="msg"></span></div>');
node.find(".msg").text(text);
node.find(".compiler").text(compiler);
return node[0];
}
function parseLines(lines, callback) {
var re = /^\/tmp\/[^:]+:([0-9]+)(:([0-9]+))?:\s+(.*)/;
$.each(lines.split('\n'), function (_, line) {
line = line.trim();
if (line !== "") {
var match = line.match(re);
if (match) {
callback(parseInt(match[1]), match[4].trim());
} else {
callback(null, line);
}
}
});
}
Editor.prototype.removeWidgets = function (widgets) {
var self = this;
_.each(widgets, function (widget) {
self.editor.removeLineWidget(widget);
});
};
Editor.prototype.numberUsedLines = function () {
var result = {};
// First, note all lines used.
_.each(this.asmByCompiler, function (asm) {
_.each(asm, function (asmLine) {
if (asmLine.source) result[asmLine.source - 1] = true;
});
});
// Now assign an ordinal to each used line.
var ordinal = 0;
_.each(result, function (v, k) {
result[k] = ordinal++;
});
// TODO: make colourise an option on the editor not the asm views
this.debouncedUpdateColours(result);
};
Editor.prototype.updateColours = function (colours) {
colour.applyColours(this.editor, colours);
this.eventHub.emit('colours', this.id, colours);
};
Editor.prototype.onCompilerClose = function (compilerId) {
this.removeWidgets(this.widgetsByCompiler[compilerId]);
delete this.widgetsByCompiler[compilerId];
delete this.asmByCompiler[compilerId];
this.numberUsedLines();
};
Editor.prototype.onCompileResponse = function (compilerId, compiler, result) {
var output = (result.stdout || "") + (result.stderr || "");
var self = this;
this.removeWidgets(this.widgetsByCompiler[compilerId]);
var widgets = [];
parseLines(output, function (lineNum, msg) {
if (lineNum) {
var widget = self.editor.addLineWidget(
lineNum - 1,
makeErrorNode(msg, compiler.name),
{coverGutter: false, noHScroll: true});
widgets.push(widget);
}
});
this.widgetsByCompiler[compilerId] = widgets;
this.asmByCompiler[compilerId] = result.asm;
this.numberUsedLines();
};
return Editor;
});