diff options
Diffstat (limited to 'js/codeq')
-rw-r--r-- | js/codeq/console.js | 351 | ||||
-rw-r--r-- | js/codeq/hint.js | 193 | ||||
-rw-r--r-- | js/codeq/startup.js | 1 |
3 files changed, 463 insertions, 82 deletions
diff --git a/js/codeq/console.js b/js/codeq/console.js index 99c1550..57d5a07 100644 --- a/js/codeq/console.js +++ b/js/codeq/console.js @@ -160,7 +160,7 @@ }; codeq.makeConsole = function (jqElt, options) { - var lines = [], // console content + var lines = [],// console content, line descriptors of the form {'content': 'string', 'classNames': [], 'jqLine': jQuery object, 'row': number} maxLines = (options && options.maxLines) || 10000, // how many lines to display at most currentRow = 0, // cursor position currentCol = 0, @@ -169,14 +169,72 @@ inputDisabled = false, // whether to ignore input lineBuffered = true, // whether we are line-buffered or not buffered + renderSpan = function (lineDescriptor, startCol, length) { + var jqLine = lineDescriptor.jqLine, + content = lineDescriptor.content, + classes = lineDescriptor.classNames, + endCol = startCol + length, + N = classes ? classes.length : 0, + i = 0, + spans = [], // what to render, items are of the form {'content': string, 'cssClass': string} + entry, classSpan, jq, span, s, prefixDelta, suffixDelta, e; + // seek out the first css class + while (i < N) { + entry = classes[i]; + if (((entry.start + entry.length) > startCol) && (entry.start < endCol)) break; + i++; + } + if (i < N) { + // render the first span + e = entry.start + entry.length; // end column of this css class + prefixDelta = entry.start < startCol ? startCol - entry.start : 0; + suffixDelta = e > endCol ? e - endCol : 0; + classSpan = entry.length - prefixDelta - suffixDelta; + span = {'content': content.substr(startCol, classSpan), 'cssClass': entry.name}; + spans.push(span); + startCol += classSpan; + i++; + // render the rest + while ((startCol < endCol) && (i < N)) { + entry = classes[i]; + e = entry.start + entry.length; // end column of this css class + prefixDelta = entry.start < startCol ? startCol - entry.start : 0; + suffixDelta = e > endCol ? e - endCol : 0; + classSpan = entry.length - prefixDelta - suffixDelta; + s = content.substr(startCol, classSpan); + if (entry.name === span.cssClass) { + span.content += s; // join content where the class is the same + } + else { + span = {'content': s, 'cssClass': entry.name}; + spans.push(span); + } + startCol += classSpan; + i++; + } + } + // render any leftover + if (startCol < endCol) { + entry = makeClassDescriptor('', startCol, endCol - startCol); + spans.push({'content': content.substring(startCol, endCol), 'cssClass': ''}); + } + // render spans + for (i = 0; i < spans.length; i++) { + span = spans[i]; + jq = $('<span class="cq-con-text ' + span.cssClass + '"></span>'); + jqLine.append(jq); + jq.text(span.content); + } + return entry; // return the last entry used + }, + renderLine = function (lineDescriptor) { var jqLine = lineDescriptor.jqLine, - className = 'cq-con-text ' + lineDescriptor.className, - content = lineDescriptor.content || '', - jq1, jq2, jqCursor; + content = lineDescriptor.content, + jqCursor, classDescriptor; if (!jqLine) { - jqLine = $('<pre class="cq-con-line ' + lineDescriptor.className + '"></pre>'); + jqLine = $('<pre class="cq-con-line"></pre>'); if (lineDescriptor.row == 0) { jqContent.prepend(jqLine); } @@ -196,36 +254,30 @@ if (currentRow == lineDescriptor.row) { // mark the cursor in the current line if (currentCol >= content.length) { - jq1 = $('<span class="' + className + '"></span>'); + // cursor after the last character + renderSpan(lineDescriptor, 0, content.length); jqCursor = $('<span class="cq-con-cursor"> </span>'); - jqLine.append(jq1); jqLine.append(jqCursor); - jq1.text(content); } else if (currentCol <= 0) { - jq2 = $('<span class="' + className + '"></span>'); - jqCursor = $('<span class="cq-con-cursor"></span>'); + // cursor at the first character + classDescriptor = lineDescriptor.classNames[0] || makeClassDescriptor('', 0, content.length); + jqCursor = $('<span class="cq-con-cursor ' + classDescriptor.name + '"></span>'); jqLine.append(jqCursor); - jqLine.append(jq2); jqCursor.text(content.charAt(0)); - jq2.text(content.substr(1)); + renderSpan(lineDescriptor, 1, content.length - 1); } else { - jq1 = $('<span class="' + className + '"></span>'); - jq2 = $('<span class="' + className + '"></span>'); - jqCursor = $('<span class="cq-con-cursor"></span>'); - jqLine.append(jq1); + // cursor somewhere in between + classDescriptor = renderSpan(lineDescriptor, 0, currentCol); + jqCursor = $('<span class="cq-con-cursor ' + classDescriptor.name + '"></span>'); jqLine.append(jqCursor); - jqLine.append(jq2); - jq1.text(content.substr(0, currentCol)); jqCursor.text(content.charAt(currentCol)); - jq2.text(content.substr(currentCol + 1)); + renderSpan(lineDescriptor, currentCol + 1, content.length - currentCol - 1); } } else { - jq1 = $('<span class="' + className + '"></span>'); - jqLine.append(jq1); - jq1.text(lineDescriptor.content); + renderSpan(lineDescriptor, 0, content.length); } }, @@ -259,6 +311,126 @@ return newLines; }, + makeClassDescriptor = function (name, start, length) { + return {'name': name, 'start': start, 'length': length}; + }, + + indexOfClassAtOrAfterColumn = function (classes, col) { + var N = classes.length, + i; + for (i = 0; i < N; i++) { + if (classes[i].start >= col) return i; + } + return N; + }, + + insertClass = function (classes, newClassName, startingCol, length) { + var newEntry = makeClassDescriptor(newClassName, startingCol, length), + endCol = startingCol + length, + i, N, prevClass, pos, auxEntry, delta; + // seek out the position where to insert the new class + i = indexOfClassAtOrAfterColumn(classes, startingCol); + prevClass = i > 0 ? classes[i-1] : null; + // insert the new class + classes.splice(i, 0, newEntry); + // if needed, adjust the length of the previous class + if (prevClass) { + pos = prevClass.start + prevClass.length; + if (pos > startingCol) { + // split the previous class in two, the new class gets inserted in between + delta = pos - startingCol; + auxEntry = makeClassDescriptor(prevClass.name, endCol, delta); + classes.splice(i+1, 0, auxEntry); + prevClass.length = prevClass.length - delta; + } + } + // readjust positions of the classes after the new one + N = classes.length; + for (i++; i < N; i++) { + auxEntry = classes[i]; + auxEntry.start = endCol; + endCol += auxEntry.length; + } + }, + + removeClasses = function (classes, startCol, length) { + var i, entry, endCol, e, prefixDelta, suffixDelta, N; + if (!length) return; + endCol = startCol + length; + for (i = classes.length - 1; i >= 0; i--) { + entry = classes[i]; + e = entry.start + entry.length; + if ((e > startCol) && (entry.start < endCol)) { + prefixDelta = entry.start < startCol ? startCol - entry.start : 0; + suffixDelta = e > endCol ? e - endCol : 0; + if ((prefixDelta == 0) && (suffixDelta == 0)) classes.splice(i, 1); + else entry.length = prefixDelta + suffixDelta; + } + } + // readjust positions + N = classes.length; + endCol = 0; + for (i = 0; i < N; i++) { + entry = classes[i]; + entry.start = endCol; + endCol += entry.length; + } + }, + + removeClassesBefore = function (classes, col) { + var i = indexOfClassAtOrAfterColumn(classes, col); + if (i == 0) return; // nothing to remove + classes.splice(0, i); + }, + + replaceClassesBeforeColumn = function (classes, col, newClassName, newLength) { + var i = indexOfClassAtOrAfterColumn(classes, col), + newEntry = makeClassDescriptor(newClassName, 0, newLength); + classes.splice(0, i, newEntry); + }, + + classesBetween = function (classes, startCol, endCol) { + var N = classes.length, + result = [], + i, e, entry, prefixDelta, suffixDelta; + for (i = 0; i < N; i++) { + entry = classes[i]; + e = entry.start + entry.length; // end column of this css class + if ((e > startCol) && (entry.start < endCol)) { + prefixDelta = entry.start < startCol ? startCol - entry.start : 0; + suffixDelta = e > endCol ? e - endCol : 0; + result.push(makeClassDescriptor(entry.name, entry.start + prefixDelta, entry.length - prefixDelta - suffixDelta)); + } + } + return result; + }, + + mergeClasses = function () { + var result = [], + i, arg, j, entry, endCol; + for (i = 0; i < arguments.length; i++) { + arg = arguments[i]; + if (arg instanceof Array) { + for (j = 0; j < arg.length; j++) result.push(arg[j]); + } + else result.push(arg); + } + // adjust positions of css classes + endCol = 0; + for (i = 0; i < result.length; i++) { + entry = result[i]; + entry.start = endCol; + endCol += entry.length; + } + return result; + }, + + scrollCursorIntoView = function () { + var dh = jqContent.height() - jqElt.height(); + if (dh > 0) + jqElt.scrollTop(dh); + }, + // the handler object that is returned handler = { 'setLineBuffered': function () { lineBuffered = true; }, @@ -309,14 +481,15 @@ 'insertRow': function (row, content, className, deferReflow) { // inserts a new row at the position specified - var lineDescriptor, i, jqLine; + var lineDescriptor, i; + if (!className) className = 'output'; for (i = lines.length; i < row; i++) { // insert any empty rows - lineDescriptor = {content: '', className: className, row: i}; + lineDescriptor = {content: '', classNames: [], row: i}; lines.push(lineDescriptor); if (!deferReflow) renderLine(lineDescriptor); } - lineDescriptor = {content: content, className: className, row: row}; + lineDescriptor = {content: content, classNames: content.length > 0 ? [makeClassDescriptor(className, 0, content.length)] : [], row: row}; if (row >= lines.length) lines.push(lineDescriptor); else lines.splice(row, 0, lineDescriptor); if (!deferReflow) this.reflow(row); @@ -324,7 +497,7 @@ }, 'splice': function (startRow, startCol, endRow, endCol, newText, className) { - var i, j, n, startLine, endLine, newLines, newTextLines, part1, part2; + var i, j, n, startLine, endLine, newLines, part1, part2, s, auxLine; if (!className) className = 'output'; // pre-process newText, convert it to newLines: an array of strings without newlines @@ -370,19 +543,25 @@ if (newLines.length > 1) { // the first and the last new lines are separate existing lines, modify them startLine.content = part1 + newLines[0]; + startLine.classNames = mergeClasses(classesBetween(startLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length)); if (startRow == endRow) { // we need to create one additional line to hold the compositum of the last line and part2 - this.insertRow(j, newLines[newLines.length - 1] + part2, className, true); + auxLine = this.insertRow(j, newLines[newLines.length - 1], className, true); + insertClass(auxLine.classNames, auxLine.content.length, part2.length); + auxLine.content = auxLine.content + part2; j++; } else { - endLine.content = newLines[newLines.length - 1] + part2; + s = newLines[newLines.length - 1]; + endLine.content = s + part2; + replaceClassesBeforeColumn(endLine.classNames, endCol, className, s.length); n--; // retain the last line } } else { // the first and the last new lines are the same existing line startLine.content = part1 + newLines[0] + part2; + startLine.classNames = mergeClasses(classesBetween(startLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length), classesBetween(endLine.classNames, endCol, endCol + part2.length)); } // remove the rest of the old lines while (n > 0) { @@ -422,7 +601,9 @@ startRow = lines.length - 1, i, lineDescriptor; if (newLines.length == 0) return; // nothing to do + if (typeof className !== 'string') className = 'output'; lineDescriptor = lines[startRow]; + insertClass(lineDescriptor.classNames, className, lineDescriptor.content.length, newLines[0].length); lineDescriptor.content = lineDescriptor.content + newLines[0]; for (i = 1; i < newLines.length; i++) { lineDescriptor = this.insertRow(startRow + i, newLines[i], className, true); @@ -441,20 +622,25 @@ part1 = currentContent.substring(0, currentCol), part2 = currentContent.substring(currentCol), newLines = formNewLines(text), - i, n; + i, n, auxLine; + if (typeof className !== 'string') className = 'output'; if (newLines.length == 0) return; // nothing to do else if (newLines.length == 1) { + insertClass(currentLine.classNames, className, part1.length, newLines[0].length); currentLine.content = part1 + newLines[0] + part2; if (!dontMoveCursor) currentCol += newLines[0].length; renderLine(currentLine); } else { + currentLine.classNames = mergeClasses(classesBetween(currentLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length)); currentLine.content = part1 + newLines[0]; n = newLines.length - 1; for (i = 1; i < n; i++) { this.insertRow(startRow + i, newLines[i], className, true); } - this.insertRow(startRow + n, newLines[n] + part2, className, true); + auxLine = this.insertRow(startRow + n, newLines[n], className, true); + insertClass(auxLine.classNames, auxLine.content.length, part2.length); + auxLine.content = auxLine.content + part2; if (!dontMoveCursor) { currentRow = startRow + n; currentCol = newLines[n].length; @@ -485,6 +671,7 @@ leftmost = typeof this.leftmostCol === 'number' && this.leftmostCol || 0; if ((currentCol > leftmost) && (currentCol <= content.length)) { currentCol--; + removeClasses(lineDescriptor.classNames, currentCol, 1); lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1); renderLine(lineDescriptor); } @@ -494,6 +681,7 @@ var lineDescriptor = lines[currentRow], content = lineDescriptor.content; if ((currentCol >= 0) && (currentCol < content.length)) { + removeClasses(lineDescriptor.classNames, currentCol, 1); lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1); renderLine(lineDescriptor); } @@ -554,10 +742,8 @@ handleInput = function (chars) { var isPastedFromClipboard = clipboardPasteInProgress || !aKeyIsDown, - newLines = chars.split('\n'), - N = newLines.length, - lastLineIndex = N - 1, - i, promise; + cookedChars = chars, // the default + cookedPromise, cookedLines, lastCookedLineIndex, i; if (inputDisabled || internallyDisabled) { // flatly ignore @@ -566,69 +752,70 @@ } internallyDisabled = true; - promise = Q(); + cookedPromise = Q(); - for (i = 0; i < N; i++) { - promise = promise - .then((function (newLine, lineIndex) {return function () { - var cookedChars = lineIndex < lastLineIndex ? newLine + '\n' : newLine, // default - thisRow = currentRow, - leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0, - dh; - - // first cook the input - if (typeof handler.onKeypress === 'function') { - // only invoke the handler if the newLine is not an empty string, otherwise cookedChars = '' - try { - cookedChars = cookedChars && handler.onKeypress(cookedChars, isPastedFromClipboard); - } - catch (e) { - codeq.log.error('Error during invocation of terminal onKeypress: ' + e, e); - } - } + // first cook the input + if (typeof handler.onKeypress === 'function') { + // only invoke the handler if the newLine is not an empty string, otherwise cookedChars = '' + try { + cookedChars = cookedChars && handler.onKeypress(cookedChars, isPastedFromClipboard); + } + catch (e) { + codeq.log.error('Error during invocation of terminal onKeypress: ' + e, e); + } + } - if (!cookedChars) { - // nothing to append - return; - } + if (typeof cookedChars !== 'string') cookedLines = ['']; + else cookedLines = cookedChars.split('\n'); + lastCookedLineIndex = cookedLines.length - 1; + + // now serve the cooking + for (i = 0; i <= lastCookedLineIndex; i++) { + cookedPromise = cookedPromise + .then(scrollCursorIntoView) // scroll to bottom on input + .then((function (cookedLine, cookedLineIndex) {return function () { + var thisRow = currentRow, + leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0; + + if (cookedLine) handler.insertAtCursor(cookedLine, 'input'); // append what we have to the display + if (cookedLineIndex < lastCookedLineIndex) { + // press on "enter" must be simulated -> jump to the start of the next line, which is first added + currentRow++; + currentCol = 0; + handler.insertRow(currentRow, '', 'input', true); + handler.reflow(currentRow - 1); // the previous line must be rendered without a cursor, and the new line must be rendered completely + } - // now serve the cooking - handler.insertAtCursor(cookedChars, 'input'); // append what we have to the display - // scroll to bottom on input - dh = jqContent.height() - jqElt.height(); - if (dh > 0) - jqElt.scrollTop(dh); - - if (typeof handler.onInput === 'function') { // now tell the owner what we've done - if (lineBuffered) { - if (thisRow < currentRow) { - // in line-buffered mode emit each line separately, except for the last (current) line + if (typeof handler.onInput === 'function') { // now tell the owner what we've done + if (lineBuffered) { + if (thisRow < currentRow) { + // in line-buffered mode emit each line separately, except for the last (current) line + try { + return handler.onInput(lines[thisRow].content.substring(leftmost)); + } + catch (e) { + codeq.log.error('Error while invoking terminal onInput: ' + e, e); + } + } + } + else if (cookedLine) { try { - return handler.onInput(lines[thisRow].content.substring(leftmost)); + return handler.onInput(cookedLine); } catch (e) { codeq.log.error('Error while invoking terminal onInput: ' + e, e); } } } - else if (cookedChars) { - try { - return handler.onInput(cookedChars); - } - catch (e) { - codeq.log.error('Error while invoking terminal onInput: ' + e, e); - } - } - } - - };})(newLines[i], i)) + };})(cookedLines[i], i)) .fail(function (e) { codeq.log.error('Error in handleInput loop: ' + e, e); }); } - promise.fin(function () { + cookedPromise.fin(function () { internallyDisabled = false; + scrollCursorIntoView(); }).done(); clipboardPasteInProgress = false; @@ -739,7 +926,7 @@ lines.push({content: options.greeting, className: 'greeting'}); currentRow++; } - lines.push({content: '', className: 'input'}); // initial input line + lines.push({content: '', classNames: []}); // initial input line render(); return handler; diff --git a/js/codeq/hint.js b/js/codeq/hint.js new file mode 100644 index 0000000..c22f39f --- /dev/null +++ b/js/codeq/hint.js @@ -0,0 +1,193 @@ +/** + * Creates a hint handler, displaying hints inside the provided jqHints <div>. + */ + +(function () { + // constants + var firstCharacterPos = {'line': 0, 'ch': 0}, + sel_no_scroll = {'scroll': false}; + + codeq.makeHinter = function (jqHints, jqEditor, editor, hintDefs) { + var 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]; + }); + }, + + typeHandlers = { + 'static': function (type, template, serverHint) { + var jqContainer, jqButton, promise, i, lastIndex; + if (template instanceof Array) { // unwrap the template if there's only one + if (template.length == 0) template = ''; + else if (template.length == 1) template = template[0] + ''; + } + if (template instanceof Array) { + codeq.log.debug('Processing an array of static hints'); + jqContainer = $('<div class="hint-static-group"></div>'); + jqButton = $('<button type="button">More...</button>'); // TODO: translate "more" + jqHints.append(jqContainer); + lastIndex = template.length - 1; + promise = Q(); + for (i = 0; i <= lastIndex; i++) { + promise = promise.then((function (tmpl, index) { + return Q.Promise(function (resolve, reject) { + var message = processTemplate(tmpl, serverHint.args), + onClick; + jqContainer.append('<div class="hint-static">' + message + '</div>'); + if (index < lastIndex) { + onClick = function () { + jqButton.off('click', onClick); + jqButton.remove(); + resolve(); + }; + jqContainer.append(jqButton); + jqButton.on('click', onClick); + } + else { + resolve(); + } + }); + })(template[i], i)); + } + promise.done(); + } + else { + codeq.log.debug('Processing a single static hint'); + var message = processTemplate(template, serverHint.args); + jqHints.append('<div class="hint-static">' + message + '</div>'); + } + // 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); + + 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.setSelection(firstCharacterPos, firstCharacterPos, sel_no_scroll); // deselect anything + } + + 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); + } + }; + + return { + /** + * 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 + */ + 'handle': function (serverHints) { + var n = serverHints.length, + /** number */ i, + /** ServerHint */ serverHint, + /** HintDefinition */ hintDef, + hintType, hintTemplate, t, fn, indices; + clearHints(); + mainLoop: + for (i = 0; i < n; i++) { + serverHint = serverHints[i]; + hintDef = hintDefs[serverHint.id]; + if (!hintDef) { + codeq.log.error('Undefined hint ' + serverHint.id); + continue; + } + if (serverHint.indices) { + indices = serverHint.indices; + for (i = 0; i < indices.length; i++) { + hintDef = hintDef[indices[i]]; + if (!hintDef) { + codeq.log.error('Undefined hint ' + serverHint.id + ' with indices ' + serverHint.indices); + continue mainLoop; + } + } + } + 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 = typeHandlers[hintType]; + if (!fn) codeq.log.error('Unsupported hint type: ' + hintType); + else fn(hintType, hintTemplate, serverHint); + } + }, + + 'destroy': function () { + clearHints(); + jqHints.empty(); + jqHints = null; + jqEditor = null; + editor = null; + } + }; + }; +})();
\ No newline at end of file diff --git a/js/codeq/startup.js b/js/codeq/startup.js index 2f7df97..ccef9f9 100644 --- a/js/codeq/startup.js +++ b/js/codeq/startup.js @@ -77,6 +77,7 @@ $(document).ready(function () { }) .fail(function (reason) { $('#disabled').css('display', 'none'); + codeq.log.error('Login request failed: ' + reason, reason); alert('Login request failed: ' + reason); }) .done(); |