diff --git a/rhodecode/public/js/src/rhodecode/codemirror.js b/rhodecode/public/js/src/rhodecode/codemirror.js --- a/rhodecode/public/js/src/rhodecode/codemirror.js +++ b/rhodecode/public/js/src/rhodecode/codemirror.js @@ -29,6 +29,178 @@ cmLog.setLevel(Logger.OFF); //global cache for inline forms var userHintsCache = {}; +// global timer, used to cancel async loading +var CodeMirrorLoadUserHintTimer; + +var escapeRegExChars = function(value) { + return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); +}; + +/** + * Load hints from external source returns an array of objects in a format + * that hinting lib requires + * @returns {Array} + */ +var CodeMirrorLoadUserHints = function(query, triggerHints) { + cmLog.debug('Loading mentions users via AJAX'); + var _users = []; + $.ajax({ + type: 'GET', + data: {query: query}, + url: pyroutes.url('user_autocomplete_data'), + headers: {'X-PARTIAL-XHR': true}, + async: true + }) + .done(function(data) { + var tmpl = '{1}'; + $.each(data.suggestions, function(i) { + var userObj = data.suggestions[i]; + + if (userObj.username !== "default") { + _users.push({ + text: userObj.username + " ", + org_text: userObj.username, + displayText: userObj.value_display, // search that field + // internal caches + _icon_link: userObj.icon_link, + _text: userObj.value_display, + + render: function(elt, data, completion) { + var el = document.createElement('div'); + el.className = "CodeMirror-hint-entry"; + el.innerHTML = tmpl.format( + completion._icon_link, completion._text); + elt.appendChild(el); + } + }); + } + }); + cmLog.debug('Mention users loaded'); + // set to global cache + userHintsCache[query] = _users; + triggerHints(userHintsCache[query]); + }) + .fail(function(data, textStatus, xhr) { + alert("error processing request: " + textStatus); + }); +}; + +/** + * filters the results based on the current context + * @param users + * @param context + * @returns {Array} + */ +var CodeMirrorFilterUsers = function(users, context) { + var MAX_LIMIT = 10; + var filtered_users = []; + var curWord = context.string; + + cmLog.debug('Filtering users based on query:', curWord); + $.each(users, function(i) { + var match = users[i]; + var searchText = match.displayText; + + if (!curWord || + searchText.toLowerCase().lastIndexOf(curWord) !== -1) { + // reset state + match._text = match.displayText; + if (curWord) { + // do highlighting + var pattern = '(' + escapeRegExChars(curWord) + ')'; + match._text = searchText.replace( + new RegExp(pattern, 'gi'), '$1<\/strong>'); + } + + filtered_users.push(match); + } + // to not return to many results, use limit of filtered results + if (filtered_users.length > MAX_LIMIT) { + return false; + } + }); + + return filtered_users; +}; + +var CodeMirrorMentionHint = function(editor, callback, options) { + var cur = editor.getCursor(); + var curLine = editor.getLine(cur.line).slice(0, cur.ch); + + // match on @ +1char + var tokenMatch = new RegExp( + '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine); + + var tokenStr = ''; + if (tokenMatch !== null && tokenMatch.length > 0){ + tokenStr = tokenMatch[0].strip(); + } else { + // skip if we didn't match our token + return; + } + + var context = { + start: (cur.ch - tokenStr.length) + 1, + end: cur.ch, + string: tokenStr.slice(1), + type: null + }; + + // case when we put the @sign in fron of a string, + // eg <@ we put it here>sometext then we need to prepend to text + if (context.end > cur.ch) { + context.start = context.start + 1; // we add to the @ sign + context.end = cur.ch; // don't eat front part just append + context.string = context.string.slice(1, cur.ch - context.start); + } + + cmLog.debug('Mention context', context); + + var triggerHints = function(userHints){ + return callback({ + list: CodeMirrorFilterUsers(userHints, context), + from: CodeMirror.Pos(cur.line, context.start), + to: CodeMirror.Pos(cur.line, context.end) + }); + }; + + var queryBasedHintsCache = undefined; + // if we have something in the cache, try to fetch the query based cache + if (userHintsCache !== {}){ + queryBasedHintsCache = userHintsCache[context.string]; + } + + if (queryBasedHintsCache !== undefined) { + cmLog.debug('Users loaded from cache'); + triggerHints(queryBasedHintsCache); + } else { + // this takes care for async loading, and then displaying results + // and also propagates the userHintsCache + window.clearTimeout(CodeMirrorLoadUserHintTimer); + CodeMirrorLoadUserHintTimer = setTimeout(function() { + CodeMirrorLoadUserHints(context.string, triggerHints); + }, 300); + } +}; + +var CodeMirrorCompleteAfter = function(cm, pred) { + var options = { + completeSingle: false, + async: true, + closeOnUnfocus: true + }; + var cur = cm.getCursor(); + setTimeout(function() { + if (!cm.state.completionActive) { + cmLog.debug('Trigger mentions hinting'); + CodeMirror.showHint(cm, CodeMirror.hint.mentions, options); + } + }, 100); + + // tell CodeMirror we didn't handle the key + // trick to trigger on a char but still complete it + return CodeMirror.Pass; +}; var initCodeMirror = function(textAreadId, resetUrl, focus, options) { var ta = $('#' + textAreadId).get(0); @@ -61,9 +233,6 @@ var initCodeMirror = function(textAreadI var initCommentBoxCodeMirror = function(textAreaId, triggerActions){ var initialHeight = 100; - // global timer, used to cancel async loading - var loadUserHintTimer; - if (typeof userHintsCache === "undefined") { userHintsCache = {}; cmLog.debug('Init empty cache for mentions'); @@ -72,96 +241,6 @@ var initCommentBoxCodeMirror = function( cmLog.debug('Element for textarea not found', textAreaId); return; } - var escapeRegExChars = function(value) { - return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }; - /** - * Load hints from external source returns an array of objects in a format - * that hinting lib requires - * @returns {Array} - */ - var loadUserHints = function(query, triggerHints) { - cmLog.debug('Loading mentions users via AJAX'); - var _users = []; - $.ajax({ - type: 'GET', - data: {query: query}, - url: pyroutes.url('user_autocomplete_data'), - headers: {'X-PARTIAL-XHR': true}, - async: true - }) - .done(function(data) { - var tmpl = '{1}'; - $.each(data.suggestions, function(i) { - var userObj = data.suggestions[i]; - - if (userObj.username !== "default") { - _users.push({ - text: userObj.username + " ", - org_text: userObj.username, - displayText: userObj.value_display, // search that field - // internal caches - _icon_link: userObj.icon_link, - _text: userObj.value_display, - - render: function(elt, data, completion) { - var el = document.createElement('div'); - el.className = "CodeMirror-hint-entry"; - el.innerHTML = tmpl.format( - completion._icon_link, completion._text); - elt.appendChild(el); - } - }); - } - }); - cmLog.debug('Mention users loaded'); - // set to global cache - userHintsCache[query] = _users; - triggerHints(userHintsCache[query]); - }) - .fail(function(data, textStatus, xhr) { - alert("error processing request: " + textStatus); - }); - }; - - /** - * filters the results based on the current context - * @param users - * @param context - * @returns {Array} - */ - var filterUsers = function(users, context) { - var MAX_LIMIT = 10; - var filtered_users = []; - var curWord = context.string; - - cmLog.debug('Filtering users based on query:', curWord); - $.each(users, function(i) { - var match = users[i]; - var searchText = match.displayText; - - if (!curWord || - searchText.toLowerCase().lastIndexOf(curWord) !== -1) { - // reset state - match._text = match.displayText; - if (curWord) { - // do highlighting - var pattern = '(' + escapeRegExChars(curWord) + ')'; - match._text = searchText.replace( - new RegExp(pattern, 'gi'), '$1<\/strong>'); - } - - filtered_users.push(match); - } - // to not return to many results, use limit of filtered results - if (filtered_users.length > MAX_LIMIT) { - return false; - } - }); - - return filtered_users; - }; - /** * Filter action based on typed in text * @param actions @@ -200,25 +279,6 @@ var initCommentBoxCodeMirror = function( return filtered_actions; }; - var completeAfter = function(cm, pred) { - var options = { - completeSingle: false, - async: true, - closeOnUnfocus: true - }; - var cur = cm.getCursor(); - setTimeout(function() { - if (!cm.state.completionActive) { - cmLog.debug('Trigger mentions hinting'); - CodeMirror.showHint(cm, CodeMirror.hint.mentions, options); - } - }, 100); - - // tell CodeMirror we didn't handle the key - // trick to trigger on a char but still complete it - return CodeMirror.Pass; - }; - var submitForm = function(cm, pred) { $(cm.display.input.textarea.form).submit(); return CodeMirror.Pass; @@ -238,7 +298,7 @@ var initCommentBoxCodeMirror = function( }; var extraKeys = { - "'@'": completeAfter, + "'@'": CodeMirrorCompleteAfter, Tab: function(cm) { // space indent instead of TABS var spaces = new Array(cm.getOption("indentUnit") + 1).join(" "); @@ -285,66 +345,6 @@ var initCommentBoxCodeMirror = function( self.setSize(null, height); }); - var mentionHint = function(editor, callback, options) { - var cur = editor.getCursor(); - var curLine = editor.getLine(cur.line).slice(0, cur.ch); - - // match on @ +1char - var tokenMatch = new RegExp( - '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine); - - var tokenStr = ''; - if (tokenMatch !== null && tokenMatch.length > 0){ - tokenStr = tokenMatch[0].strip(); - } else { - // skip if we didn't match our token - return; - } - - var context = { - start: (cur.ch - tokenStr.length) + 1, - end: cur.ch, - string: tokenStr.slice(1), - type: null - }; - - // case when we put the @sign in fron of a string, - // eg <@ we put it here>sometext then we need to prepend to text - if (context.end > cur.ch) { - context.start = context.start + 1; // we add to the @ sign - context.end = cur.ch; // don't eat front part just append - context.string = context.string.slice(1, cur.ch - context.start); - } - - cmLog.debug('Mention context', context); - - var triggerHints = function(userHints){ - return callback({ - list: filterUsers(userHints, context), - from: CodeMirror.Pos(cur.line, context.start), - to: CodeMirror.Pos(cur.line, context.end) - }); - }; - - var queryBasedHintsCache = undefined; - // if we have something in the cache, try to fetch the query based cache - if (userHintsCache !== {}){ - queryBasedHintsCache = userHintsCache[context.string]; - } - - if (queryBasedHintsCache !== undefined) { - cmLog.debug('Users loaded from cache'); - triggerHints(queryBasedHintsCache); - } else { - // this takes care for async loading, and then displaying results - // and also propagates the userHintsCache - window.clearTimeout(loadUserHintTimer); - loadUserHintTimer = setTimeout(function() { - loadUserHints(context.string, triggerHints); - }, 300); - } - }; - var actionHint = function(editor, options) { var cur = editor.getCursor(); var curLine = editor.getLine(cur.line).slice(0, cur.ch); @@ -408,7 +408,7 @@ var initCommentBoxCodeMirror = function( to: CodeMirror.Pos(cur.line, context.end) }; }; - CodeMirror.registerHelper("hint", "mentions", mentionHint); + CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint); CodeMirror.registerHelper("hint", "actions", actionHint); return cm; };