From f1fc2707bb1081cf63169d71cde259482b7bcddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Smodi=C5=A1?= Date: Wed, 15 Jul 2015 22:32:32 +0200 Subject: Added support for displaying hints. For now triggered by hand via a console, e.g. FireBug. --- js/codeq.js | 32 +++++----- js/def_parser.js | 22 ++++++- js/prolog.js | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 20 deletions(-) create mode 100644 js/prolog.js (limited to 'js') diff --git a/js/codeq.js b/js/codeq.js index 770153e..e867ffd 100644 --- a/js/codeq.js +++ b/js/codeq.js @@ -1,6 +1,17 @@ // introduce the namespace object for codeq window.codeq = {}; window.siteDefinition = { logLevel: 'debug' }; // for debug purposes +window.phandler = null; // TODO: this is for debug only + +// type definitions, so the IDE and coders have easier work +/** + * @typedef {Object} ServerHint a hint sent by the server + * @property {string} id the hint ID + * @property {Object} args optional arguments for the hint in case of a pop-up or static hint + * @property {string[]} choices array of choices in case of a drop-down hint + * @property {number} start the starting position of the highlighted code, in case of a pop-up or drop-down hint + * @property {number} end the ending position of the highlighted code, in case of a pop-up or drop-down hint + */ (function () { @@ -700,8 +711,6 @@ window.siteDefinition = { logLevel: 'debug' }; // for debug purposes // if (s.length == 0) return; // empty hash // if (s.charAt(0) == '#') s = s.substring(1); // if (s.length == 0) return; // empty hash - var editor = CodeMirror(document.getElementById('code_editor'), { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true }); - editor.setValue('sister(X, Y) :-\n female(X),\n parent(Z, X),\n parent(Z, Y),\n X \\== Y.'); /* $('#console').terminal(function (command, term) { term.echo('Not implemented.'); @@ -715,27 +724,16 @@ window.siteDefinition = { logLevel: 'debug' }; // for debug purposes width: '100%' });*/ - var controller = $('#console').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.' - }); - codeq.system.load({ type: 'text', url: 'sister.py', callback: function (data, status, url) { if (!data) return; var info = codeq.parseDefinition(data); - $('#description').html(info.description); + window.phandler = codeq.createPrologHandler(info); // TODO: for debug only + // DEBUG: phandler.processServerHints([{id:'x_must_be_female'}]); + // DEBUG: phandler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); + // DEBUG: phandler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); } }); }; diff --git a/js/def_parser.js b/js/def_parser.js index 91ca35d..fd34e80 100644 --- a/js/def_parser.js +++ b/js/def_parser.js @@ -356,6 +356,23 @@ else throw new Error("Unexpected token: expected a string, number, object, or array, at line " + token.line + ", column " + token.column); }; + + /** + * @typedef {string|{type: string, message: string}} HintDefinition + */ + /** + * @typedef {Object} PrologTaskDef a Prolog assignment definition + * @property {string} description the assignment description + * @property {Object.} hint the assignment hint definitions, keyed by hint ID + */ + + /** + * Converts the given pythonic assignment definition into a JavaScriptish definition, + * executes it, and takes the variables "description" and "hint" from the definition. + * + * @param {string} definition The assignment definition. + * @returns {PrologTaskDef} + */ codeq.parseDefinition = function (definition) { var next = tokenize(definition), vars = { 'description': true, 'hint': true }, @@ -408,12 +425,11 @@ } parts[0] = 'var ' + v.join(', ') + ';\n'; - parts.push(';\n__params__.description = description;\n__params__.hint = hint;'); + parts.push(';\nreturn {"description":description,"hint":hint};'); v = parts.join(''); codeq.log.debug("Creating a new parseInfo function having the body:\n" + v); fn = new Function("__params__", v); - obj = {}; - fn(obj); + obj = fn(); return obj; // obj now contains "description" and "hint" }; // parseDefinition diff --git a/js/prolog.js b/js/prolog.js new file mode 100644 index 0000000..b697fca --- /dev/null +++ b/js/prolog.js @@ -0,0 +1,187 @@ +/** + * Handler for a Prolog assignment. Works with the corresponding prolog.html content. + */ + +(function () { + /** + * Creates a new handler for the given Prolog assignment definition. + * + * @param {PrologTaskDef} info + * @returns {{destroy: Function, processServerHints: Function}} + */ + codeq.createPrologHandler = function (info) { + 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.' + }), + /** 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; + }); + }, + '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 active + } + + 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('sister(X, Y) :-\n female(X),\n parent(Z, X),\n parent(Z, Y),\n X \\== Y.'); // demo + jqDescription.html(info.description); + + return { + 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); + } + } + }; + }; +})(); \ No newline at end of file -- cgit v1.2.1