/**
* Handler for a Python assignment. Works with the corresponding python.html content.
*/
(function () {
// a constant
var firstCharacterPos = {'line': 0, 'ch': 0};
var makePythonTerminalHandler = function (jqConsole, editor, problem_id, activityHandler) {
var terminal = codeq.makeConsole(jqConsole, {
'greeting': 'CodeQ Python terminal proxy'
}),
tcs = function terminalCommandSuccess (data) {
if (data.code !== 0) {
terminal.append(data.message, 'error');
}
},
tcf = function terminalCommandFailed (error) {
terminal.append(error + '\n', 'error');
};
terminal.onInput = function (text) {
return codeq.comms.sendPush({
'text': text + '\n'
}).then(tcs, tcf);
};
codeq.comms.on('terminal_output', function (data) {
var text = data.text;
terminal.append(text, 'output');
lines = text.split('\n');
terminal.leftmostCol = lines[lines.length-1].length;
});
terminal.leftmostCol = 1;
return terminal;
};
var makeActivityHandler = function (editor, problem_id) {
var lastActivityMillis = Date.now(),
deltaActivityMillis = function deltaActivityMillisFunc () {
var now = Date.now(),
dt = now - lastActivityMillis;
lastActivityMillis = now;
return dt;
},
queue = [],
ts = null,
timer = function () {
var promise;
ts = null;
if (queue.length === 0) return Q(true);
promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id);
queue.length = 0;
return promise;
},
flush = function () {
clearTimeout(ts);
return timer();
};
return {
'queueTrace': function (trace) {
trace['dt'] = deltaActivityMillis();
queue.push(trace);
if (ts === null) setTimeout(timer, 10000); // flush every 10 seconds
},
'flush': flush,
'addAndPurge': function (trace) {
var accumulatedTrace = queue;
queue = [];
trace['dt'] = deltaActivityMillis();
accumulatedTrace.push(trace);
if (ts !== null) {
clearTimeout(ts);
ts = null;
}
return accumulatedTrace;
}
};
};
/**
* Creates a new handler for the given Prolog assignment definition.
*
* @param {PrologTaskDef} info
* @returns {{destroy: Function, processServerHints: Function}}
*/
codeq.createPythonHandler = function (info) {
var problem = info.problem,
jqDescription = $('#description'),
jqEditor = $('#code_editor'),
jqConsole = $('#console'),
jqHints = $('#info'),
editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true, mode: 'python' }),
activityHandler = makeActivityHandler(editor, problem.id),
terminal = makePythonTerminalHandler(jqConsole, editor, problem.id, activityHandler),
hintDefs = problem.hint,
hintCounter = 0, // for generating unique class-names
hintCleaners = [],
clearHints = function () {
var i;
for (i = hintCleaners.length - 1; i >= 0; i--) {
hintCleaners[i]();
}
hintCleaners.length = 0;
hintCounter = 0;
},
addMark = function (start, end) {
var posStart = editor.posFromIndex(start),
posEnd = editor.posFromIndex(end),
doc = editor.getDoc(),
mark = doc.markText(posStart, posEnd, {className: 'editor-mark _emark_' + hintCounter}),
result = {start: posStart, end: posEnd, mark: mark, className: '_emark_' + hintCounter};
hintCleaners.push(function () { mark.clear(); mark = null; doc = null; result.mark = null; });
hintCounter++;
return result;
},
processTemplate = function (template, args) {
if (!args)
return template;
return template.replace(/\[%=(\w+)%\]/g, function(match, name) {
return args[name];
});
},
hintHandlers = {
'static': function (type, template, serverHint) {
codeq.log.debug('Processing static hint');
var message = processTemplate(template, serverHint.args);
jqHints.append('
' + message + '
');
// no hint cleaner here, a static hint remains on the screen
},
'popup': function (type, template, serverHint) {
codeq.log.debug('Processing popup hint');
var message = processTemplate(template, serverHint.args),
mark = addMark(serverHint.start, serverHint.end), // add the mark
jqMark = jqEditor.find('.' + mark.className);
/* jqPopup = null,
onBlur = function () {
codeq.log.debug('Removing popup');
if (jqPopup) {
jqPopup.off('blur', onBlur);
jqPopup.remove();
jqPopup = null;
}
};
window.jqMark = jqMark; // TODO: DEBUG
jqMark.on('click', function () {
if (jqPopup) return;
codeq.log.debug('Showing popup');
var pos = mark.mark.find(), // results in {from: {line: number, ch: number}, to: {line: number, ch: number}}
left = pos.from.ch < pos.to.ch ? pos.from.ch : pos.to.ch,
down = pos.from.line < pos.to.line ? pos.to.line : pos.from.line;
jqPopup = $('').html(template);
editor.addWidget({line: down, ch: left}, jqPopup[0], true);
jqPopup = jqEditor.find('.editor-popup.' + mark.className);
setTimeout(function () {jqPopup.trigger('focus'); jqPopup.on('click', onBlur);}, 50); // event handlers can be registered only when the DOM elements is part of the document
window.jqPopup = jqPopup; // TODO: DEBUG
});
hintCleaners.push(function () {
if (jqPopup) {
jqPopup.off('blur', onBlur);
jqPopup.remove();
jqPopup = null;
}
if (jqMark) {
jqMark = null;
}
mark = null;
});*/
jqMark.popover({content: message, html: true, placement: 'auto bottom', trigger: 'hover focus click', container: 'body'});
hintCleaners.push(function () { if (jqMark) { jqMark.popover('destroy'); jqMark = null; } });
mark.mark.on('', function () {});
},
'dropdown': function (type, template, serverHint) {
codeq.log.debug('Processing dropdown hint');
var completion = null, // the completion object, created in showHint()
close = function () {
if (completion) {
completion.close();
completion = null;
}
};
if ((editor.listSelections().length > 1) || editor.somethingSelected()) {
// showHint() doesn't work if a selection is activeparts
}
editor.showHint({
hint: function () {
var hints = {
list: serverHint.choices,
from: editor.posFromIndex(serverHint.start),
to: editor.posFromIndex(serverHint.end)
};
completion = editor.state.completionActive;
return hints;
},
completeOnSingleClick: true,
completeSingle: false
});
hintCleaners.push(close);
}
};
editor.setValue(info.solution);
$('#title').text(problem.slug);
jqDescription.html(problem.description);
editor.on('change', function (instance, changeObj) {
var doc = editor.getDoc(),
pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from));
if (changeObj.removed) {
activityHandler.queueTrace({'typ': 'rm', 'off': pos, 'len': codeq.codePointCount(changeObj.removed)});
}
if (changeObj.text) {
activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text});
}
});
var handler = {
destroy: function () {
terminal.destroy();
jqDescription.empty();
jqEditor.empty(); // TODO: perhaps you do not want to "free" the editor, just empty it
jqConsole.empty(); // TODO: the same with the console
jqHints.empty(); // TODO: just make static references to these
jqDescription = null;
jqEditor = null;
jqConsole = null;
jqHints = null;
},
/**
* Processes and display appropriately the server hints.
* TODO: sort hints so static and popup hints come first, and a (single) drop-down hint last
*
* @param {ServerHint[]} serverHints an array of hints from the server
*/
processServerHints: function (serverHints) {
var n = serverHints.length,
/** number */ i,
/** ServerHint */ serverHint,
/** HintDefinition */ hintDef,
hintType, hintTemplate, t, fn;
clearHints();
for (i = 0; i < n; i++) {
serverHint = serverHints[i];
hintDef = hintDefs[serverHint.id];
if (serverHint.indices) {
indices = serverHint.indices
for (i = 0; i < indices.length; i++) {
hintDef = hintDef[indices[i]];
if (!hintDef)
break;
}
}
if (!hintDef) {
codeq.log.error('Undefined hint ' + serverHint.id + ' with indices ' + serverHint.indices);
continue;
}
t = typeof hintDef;
if (t === 'object') {
hintType = hintDef.type;
hintTemplate = hintDef.message;
}
else if (t === 'string') {
hintType = 'static';
hintTemplate = hintDef;
}
else {
codeq.log.error('Unsupported hint definition: ' + t);
continue;
}
fn = hintHandlers[hintType];
if (!fn) codeq.log.error('Unsupported hint type: ' + hintType);
else fn(hintType, hintTemplate, serverHint);
}
}
};
var jqButtons = $('#block-toolbar button');
// $(jqButtons.get(0)).on('click', function () { handler.processServerHints([{id:'x_must_be_female'}]); });
// $(jqButtons.get(1)).on('click', function () { handler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); });
// $(jqButtons.get(2)).on('click', function () { handler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); });
$('#btn_code_hint').on('click', function () {
// handler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]);
var doc = editor.getDoc();
codeq.comms.sendHint({
'language': 'python',
'program': editor.getDoc().getValue(),
'problem_id': problem.id
}).then(
function hintSuccess(data) {
if (data.code === 0)
handler.processServerHints(data.hints);
else
terminal.append('error: ' + data.message);
},
function hintFailed (error) {
terminal.append('exception: ' + error);
}
).done();
});
$('#btn_code_test').on('click', function () {
var doc = editor.getDoc();
codeq.comms.sendTest({
'language': 'python',
'program': editor.getDoc().getValue(),
'problem_id': problem.id
}).then(
function testSuccess(data) {
if (data.code === 0)
handler.processServerHints(data.hints);
else
terminal.append('error: ' + data.message);
},
function testFailed (error) {
terminal.append('exception: ' + error);
}
).done();
});
// TODO first line of interpreter output is buffered without this, why?
codeq.comms.sendPush({
'text': ''
});
return handler;
};
})();