/** * Handler for a Prolog assignment. Works with the corresponding prolog.html content. */ (function () { // a constant var firstCharacterPos = {'line': 0, 'ch': 0}; var makePrologTerminalHandler = function (jqConsole, editor, problem_group, problem) { var promptMode = true, // default: query composition; alternative: query result browsing tcs = function terminalCommandSuccess (data) { var t, lines, i; terminal.resume(); if (data.code === 0) { t = data.terminal; lines = t.messages; for (i = 0; i < lines.length; i++) { terminal.echo(lines[i]); } promptMode = !t.have_more; } else { terminal.error(data.message); promptMode = true; } }, tcf = function terminalCommandFailed (error) { terminal.resume(); terminal.exception(error); promptMode = true; }, terminal = jqConsole.terminal(function (command, terminal) { if (promptMode) { terminal.pause(); codeq.comms.sendQuery({ 'step': 'run', 'program': editor.getDoc().getValue(), 'query': command, 'language': 'prolog', 'problem_group': problem_group, 'problem': problem }).then(tcs, tcf).done(); } else { // not in prompt mode -- we should never land here, but handle it anyway codeq.comms.sendQuery({ 'step': 'end' }).then(tcs, tcf).done(); } }, { 'history': true, 'prompt': '?- ', 'greetings': 'CodeQ prolog terminal proxy', 'exit': false, 'clear': false, 'keypress': function (event, terminal) { if (promptMode) return true; setTimeout(function () { terminal.echo(''); // send newline after semicolon or full-stop terminal.pause(); }, 0); if ((event.which == 32) || (event.which == 59)) { // space or semicolon -> show next answer event.which = 59; // semicolon codeq.comms.sendQuery({ 'step': 'next' }).then(tcs, tcf).done(); } else { // everything else: stop searching for answers event.which = 46; // full stop codeq.comms.sendQuery({ 'step': 'end' }).then(tcs, tcf).done(); } } }); return {}; }; /** * Creates a new handler for the given Prolog assignment definition. * * @param {PrologTaskDef} info * @returns {{destroy: Function, processServerHints: Function}} */ codeq.createPrologHandler = function (info, problem_group, problem) { var jqDescription = $('#description'), jqEditor = $('#code_editor'), jqConsole = $('#console'), jqHints = $('#info'), editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true }), /* controller = jqConsole.console({ promptLabel: '?- ', commandValidate: function (line) { return !!line; }, commandHandle: function (line) { return [{msg:'Not implemented.', className:'console-response'}]; }, autofocus: false, animateScroll: false, promptHistory: false, welcomeMessage: 'Prolog REPL.' }),*/ terminal = makePrologTerminalHandler(jqConsole, editor, problem_group, problem), /** Object. */ hintDefs = info.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; }, hintHandlers = { 'static': function (type, template, serverHint) { codeq.log.debug('Processing static hint'); jqHints.append('
' + template + '
'); // TODO: incorporate any serverHint.args // no hint cleaner here, a static hint remains on the screen }, 'popup': function (type, template, serverHint) { codeq.log.debug('Processing popup hint'); var 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: template, 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); } }, lastActivityMillis = Date.now(), deltaActivityMillis = function deltaActivityMillisFunc () { var now = Date.now(), dt = Math.max(0, Math.min(30000, now - lastActivityMillis)); // 0 sec <= dt <= 30 sec lastActivityMillis = now; return dt; }; editor.setValue('sister(X, Y) :-\n female(X),\n parent(Z, X),\n parent(Z, Y),\n X \\== Y.'); // demo jqDescription.html(info.description); editor.on('change', function (instance, changeObj) { var doc = editor.getDoc(), pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from)), dt = deltaActivityMillis(); if (changeObj.removed) { // codeq.comms.sendActivity({'typ': 'rm', 'dt': dt, 'off': pos, 'len': codeq.codePointCount(changeObj.removed)}); } if (changeObj.text) { // codeq.comms.sendActivity({'typ': 'ins', 'dt': dt, 'off': pos, 'txt': changeObj.text}); } }); var handler = { destroy: function () { 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 (!hintDef) { codeq.log.error('Undefined hint: ' + serverHint.id); 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_run').on('click', function () { var doc = editor.getDoc(), dt = deltaActivityMillis(); // codeq.comms.sendActivity({'typ': 'slv', 'dt': dt, 'qry': }); handler.processServerHints([{id:'list_empty'}]); }); $('#btn_code_break').on('click', function () { handler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); }); $('#btn_code_hint').on('click', function () { handler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); }); return handler; }; })();