##// END OF EJS Templates
code-mirror: implement slash actions instead of ctrl+space....
marcink -
r1361:9c1873b2 default
parent child Browse files
Show More
@@ -1,530 +1,582 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Code Mirror
21 21 */
22 22 // global code-mirror logger;, to enable run
23 23 // Logger.get('CodeMirror').setLevel(Logger.DEBUG)
24 24
25 25 cmLog = Logger.get('CodeMirror');
26 26 cmLog.setLevel(Logger.OFF);
27 27
28 28
29 29 //global cache for inline forms
30 30 var userHintsCache = {};
31 31
32 32 // global timer, used to cancel async loading
33 33 var CodeMirrorLoadUserHintTimer;
34 34
35 35 var escapeRegExChars = function(value) {
36 36 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
37 37 };
38 38
39 39 /**
40 40 * Load hints from external source returns an array of objects in a format
41 41 * that hinting lib requires
42 42 * @returns {Array}
43 43 */
44 44 var CodeMirrorLoadUserHints = function(query, triggerHints) {
45 45 cmLog.debug('Loading mentions users via AJAX');
46 46 var _users = [];
47 47 $.ajax({
48 48 type: 'GET',
49 49 data: {query: query},
50 50 url: pyroutes.url('user_autocomplete_data'),
51 51 headers: {'X-PARTIAL-XHR': true},
52 52 async: true
53 53 })
54 54 .done(function(data) {
55 55 var tmpl = '<img class="gravatar" src="{0}"/>{1}';
56 56 $.each(data.suggestions, function(i) {
57 57 var userObj = data.suggestions[i];
58 58
59 59 if (userObj.username !== "default") {
60 60 _users.push({
61 61 text: userObj.username + " ",
62 62 org_text: userObj.username,
63 63 displayText: userObj.value_display, // search that field
64 64 // internal caches
65 65 _icon_link: userObj.icon_link,
66 66 _text: userObj.value_display,
67 67
68 68 render: function(elt, data, completion) {
69 69 var el = document.createElement('div');
70 70 el.className = "CodeMirror-hint-entry";
71 71 el.innerHTML = tmpl.format(
72 72 completion._icon_link, completion._text);
73 73 elt.appendChild(el);
74 74 }
75 75 });
76 76 }
77 77 });
78 78 cmLog.debug('Mention users loaded');
79 79 // set to global cache
80 80 userHintsCache[query] = _users;
81 81 triggerHints(userHintsCache[query]);
82 82 })
83 83 .fail(function(data, textStatus, xhr) {
84 84 alert("error processing request: " + textStatus);
85 85 });
86 86 };
87 87
88 88 /**
89 89 * filters the results based on the current context
90 90 * @param users
91 91 * @param context
92 92 * @returns {Array}
93 93 */
94 94 var CodeMirrorFilterUsers = function(users, context) {
95 95 var MAX_LIMIT = 10;
96 96 var filtered_users = [];
97 97 var curWord = context.string;
98 98
99 99 cmLog.debug('Filtering users based on query:', curWord);
100 100 $.each(users, function(i) {
101 101 var match = users[i];
102 102 var searchText = match.displayText;
103 103
104 104 if (!curWord ||
105 105 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
106 106 // reset state
107 107 match._text = match.displayText;
108 108 if (curWord) {
109 109 // do highlighting
110 110 var pattern = '(' + escapeRegExChars(curWord) + ')';
111 111 match._text = searchText.replace(
112 112 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
113 113 }
114 114
115 115 filtered_users.push(match);
116 116 }
117 117 // to not return to many results, use limit of filtered results
118 118 if (filtered_users.length > MAX_LIMIT) {
119 119 return false;
120 120 }
121 121 });
122 122
123 123 return filtered_users;
124 124 };
125 125
126 126 var CodeMirrorMentionHint = function(editor, callback, options) {
127 127 var cur = editor.getCursor();
128 128 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
129 129
130 130 // match on @ +1char
131 131 var tokenMatch = new RegExp(
132 132 '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine);
133 133
134 134 var tokenStr = '';
135 135 if (tokenMatch !== null && tokenMatch.length > 0){
136 136 tokenStr = tokenMatch[0].strip();
137 137 } else {
138 138 // skip if we didn't match our token
139 139 return;
140 140 }
141 141
142 142 var context = {
143 143 start: (cur.ch - tokenStr.length) + 1,
144 144 end: cur.ch,
145 145 string: tokenStr.slice(1),
146 146 type: null
147 147 };
148 148
149 149 // case when we put the @sign in fron of a string,
150 150 // eg <@ we put it here>sometext then we need to prepend to text
151 151 if (context.end > cur.ch) {
152 152 context.start = context.start + 1; // we add to the @ sign
153 153 context.end = cur.ch; // don't eat front part just append
154 154 context.string = context.string.slice(1, cur.ch - context.start);
155 155 }
156 156
157 157 cmLog.debug('Mention context', context);
158 158
159 159 var triggerHints = function(userHints){
160 160 return callback({
161 161 list: CodeMirrorFilterUsers(userHints, context),
162 162 from: CodeMirror.Pos(cur.line, context.start),
163 163 to: CodeMirror.Pos(cur.line, context.end)
164 164 });
165 165 };
166 166
167 167 var queryBasedHintsCache = undefined;
168 168 // if we have something in the cache, try to fetch the query based cache
169 169 if (userHintsCache !== {}){
170 170 queryBasedHintsCache = userHintsCache[context.string];
171 171 }
172 172
173 173 if (queryBasedHintsCache !== undefined) {
174 174 cmLog.debug('Users loaded from cache');
175 175 triggerHints(queryBasedHintsCache);
176 176 } else {
177 177 // this takes care for async loading, and then displaying results
178 178 // and also propagates the userHintsCache
179 179 window.clearTimeout(CodeMirrorLoadUserHintTimer);
180 180 CodeMirrorLoadUserHintTimer = setTimeout(function() {
181 181 CodeMirrorLoadUserHints(context.string, triggerHints);
182 182 }, 300);
183 183 }
184 184 };
185 185
186 186 var CodeMirrorCompleteAfter = function(cm, pred) {
187 187 var options = {
188 188 completeSingle: false,
189 189 async: true,
190 190 closeOnUnfocus: true
191 191 };
192 192 var cur = cm.getCursor();
193 193 setTimeout(function() {
194 194 if (!cm.state.completionActive) {
195 195 cmLog.debug('Trigger mentions hinting');
196 196 CodeMirror.showHint(cm, CodeMirror.hint.mentions, options);
197 197 }
198 198 }, 100);
199 199
200 200 // tell CodeMirror we didn't handle the key
201 201 // trick to trigger on a char but still complete it
202 202 return CodeMirror.Pass;
203 203 };
204 204
205 205 var initCodeMirror = function(textAreadId, resetUrl, focus, options) {
206 206 var ta = $('#' + textAreadId).get(0);
207 207 if (focus === undefined) {
208 208 focus = true;
209 209 }
210 210
211 211 // default options
212 212 var codeMirrorOptions = {
213 213 mode: "null",
214 214 lineNumbers: true,
215 215 indentUnit: 4,
216 216 autofocus: focus
217 217 };
218 218
219 219 if (options !== undefined) {
220 220 // extend with custom options
221 221 codeMirrorOptions = $.extend(true, codeMirrorOptions, options);
222 222 }
223 223
224 224 var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions);
225 225
226 226 $('#reset').on('click', function(e) {
227 227 window.location = resetUrl;
228 228 });
229 229
230 230 return myCodeMirror;
231 231 };
232 232
233 233 var initCommentBoxCodeMirror = function(textAreaId, triggerActions){
234 234 var initialHeight = 100;
235 235
236 236 if (typeof userHintsCache === "undefined") {
237 237 userHintsCache = {};
238 238 cmLog.debug('Init empty cache for mentions');
239 239 }
240 240 if (!$(textAreaId).get(0)) {
241 241 cmLog.debug('Element for textarea not found', textAreaId);
242 242 return;
243 243 }
244 244 /**
245 245 * Filter action based on typed in text
246 246 * @param actions
247 247 * @param context
248 248 * @returns {Array}
249 249 */
250 250
251 251 var filterActions = function(actions, context){
252
252 253 var MAX_LIMIT = 10;
253 var filtered_actions= [];
254 var filtered_actions = [];
254 255 var curWord = context.string;
255 256
256 257 cmLog.debug('Filtering actions based on query:', curWord);
257 258 $.each(actions, function(i) {
258 259 var match = actions[i];
259 var searchText = match.displayText;
260 var searchText = match.searchText;
260 261
261 262 if (!curWord ||
262 263 searchText.toLowerCase().lastIndexOf(curWord) !== -1) {
263 264 // reset state
264 265 match._text = match.displayText;
265 266 if (curWord) {
266 267 // do highlighting
267 268 var pattern = '(' + escapeRegExChars(curWord) + ')';
268 269 match._text = searchText.replace(
269 270 new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
270 271 }
271 272
272 273 filtered_actions.push(match);
273 274 }
274 275 // to not return to many results, use limit of filtered results
275 276 if (filtered_actions.length > MAX_LIMIT) {
276 277 return false;
277 278 }
278 279 });
280
279 281 return filtered_actions;
280 282 };
281 283
282 284 var submitForm = function(cm, pred) {
283 285 $(cm.display.input.textarea.form).submit();
284 286 return CodeMirror.Pass;
285 287 };
286 288
287 var completeActions = function(cm, pred) {
288 var cur = cm.getCursor();
289 var options = {
290 closeOnUnfocus: true
291 };
292 setTimeout(function() {
293 if (!cm.state.completionActive) {
294 cmLog.debug('Trigger actions hinting');
295 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
289 var completeActions = function(actions){
290
291 return function(cm, pred) {
292 var cur = cm.getCursor();
293 var options = {
294 closeOnUnfocus: true
295 };
296 setTimeout(function() {
297 if (!cm.state.completionActive) {
298 cmLog.debug('Trigger actions hinting');
299 CodeMirror.showHint(cm, CodeMirror.hint.actions, options);
300 }
301 }, 100);
302
303 // tell CodeMirror we didn't handle the key
304 // trick to trigger on a char but still complete it
305 return CodeMirror.Pass;
296 306 }
297 }, 100);
298 307 };
299 308
300 309 var extraKeys = {
301 310 "'@'": CodeMirrorCompleteAfter,
302 311 Tab: function(cm) {
303 312 // space indent instead of TABS
304 313 var spaces = new Array(cm.getOption("indentUnit") + 1).join(" ");
305 314 cm.replaceSelection(spaces);
306 315 }
307 316 };
308 317 // submit form on Meta-Enter
309 318 if (OSType === "mac") {
310 319 extraKeys["Cmd-Enter"] = submitForm;
311 320 }
312 321 else {
313 322 extraKeys["Ctrl-Enter"] = submitForm;
314 323 }
315 324
316 325 if (triggerActions) {
317 extraKeys["Ctrl-Space"] = completeActions;
326 // register triggerActions for this instance
327 extraKeys["'/'"] = completeActions(triggerActions);
318 328 }
319 329
320 330 var cm = CodeMirror.fromTextArea($(textAreaId).get(0), {
321 331 lineNumbers: false,
322 332 indentUnit: 4,
323 333 viewportMargin: 30,
324 334 // this is a trick to trigger some logic behind codemirror placeholder
325 335 // it influences styling and behaviour.
326 336 placeholder: " ",
327 337 extraKeys: extraKeys,
328 338 lineWrapping: true
329 339 });
330 340
331 341 cm.setSize(null, initialHeight);
332 342 cm.setOption("mode", DEFAULT_RENDERER);
333 343 CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode
334 344 cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER);
335 345 // start listening on changes to make auto-expanded editor
336 346 cm.on("change", function(self) {
337 347 var height = initialHeight;
338 348 var lines = self.lineCount();
339 349 if ( lines > 6 && lines < 20) {
340 350 height = "auto";
341 351 }
342 352 else if (lines >= 20){
343 353 zheight = 20*15;
344 354 }
345 355 self.setSize(null, height);
346 356 });
347 357
348 358 var actionHint = function(editor, options) {
349 359 var cur = editor.getCursor();
350 360 var curLine = editor.getLine(cur.line).slice(0, cur.ch);
351 361
352 var tokenMatch = new RegExp('[a-zA-Z]{1}[a-zA-Z]*$').exec(curLine);
362 // match only on /+1 character minimum
363 var tokenMatch = new RegExp('(^/\|/\)([a-zA-Z]*)$').exec(curLine);
353 364
354 365 var tokenStr = '';
355 366 if (tokenMatch !== null && tokenMatch.length > 0){
356 tokenStr = tokenMatch[0].strip();
367 tokenStr = tokenMatch[2].strip();
357 368 }
358 369
359 370 var context = {
360 start: cur.ch - tokenStr.length,
371 start: (cur.ch - tokenStr.length) - 1,
361 372 end: cur.ch,
362 373 string: tokenStr,
363 374 type: null
364 375 };
365 376
366 377 var actions = [
367 378 {
368 379 text: "approve",
380 searchText: "status approved",
369 381 displayText: _gettext('Set status to Approved'),
370 382 hint: function(CodeMirror, data, completion) {
371 383 CodeMirror.replaceRange("", completion.from || data.from,
372 384 completion.to || data.to, "complete");
373 385 $('#change_status_general').select2("val", 'approved').trigger('change');
374 386 },
375 387 render: function(elt, data, completion) {
376 388 var el = document.createElement('div');
377 389 el.className = "flag_status flag_status_comment_box approved pull-left";
378 390 elt.appendChild(el);
379 391
380 392 el = document.createElement('span');
381 393 el.innerHTML = completion.displayText;
382 394 elt.appendChild(el);
383 395 }
384 396 },
385 397 {
386 398 text: "reject",
399 searchText: "status rejected",
387 400 displayText: _gettext('Set status to Rejected'),
388 401 hint: function(CodeMirror, data, completion) {
389 402 CodeMirror.replaceRange("", completion.from || data.from,
390 403 completion.to || data.to, "complete");
391 404 $('#change_status_general').select2("val", 'rejected').trigger('change');
392 405 },
393 406 render: function(elt, data, completion) {
394 407 var el = document.createElement('div');
395 408 el.className = "flag_status flag_status_comment_box rejected pull-left";
396 409 elt.appendChild(el);
397 410
398 411 el = document.createElement('span');
399 412 el.innerHTML = completion.displayText;
400 413 elt.appendChild(el);
401 414 }
415 },
416 {
417 text: "as_todo",
418 searchText: "todo comment",
419 displayText: _gettext('TODO comment'),
420 hint: function(CodeMirror, data, completion) {
421 CodeMirror.replaceRange("", completion.from || data.from,
422 completion.to || data.to, "complete");
423 $('#comment_type_general').val('todo')
424 },
425 render: function(elt, data, completion) {
426 var el = document.createElement('div');
427 el.className = "pull-left";
428 elt.appendChild(el);
429
430 el = document.createElement('span');
431 el.innerHTML = completion.displayText;
432 elt.appendChild(el);
433 }
434 },
435 {
436 text: "as_note",
437 searchText: "note comment",
438 displayText: _gettext('Note Comment'),
439 hint: function(CodeMirror, data, completion) {
440 CodeMirror.replaceRange("", completion.from || data.from,
441 completion.to || data.to, "complete");
442 $('#comment_type_general').val('note')
443 },
444 render: function(elt, data, completion) {
445 var el = document.createElement('div');
446 el.className = "pull-left";
447 elt.appendChild(el);
448
449 el = document.createElement('span');
450 el.innerHTML = completion.displayText;
451 elt.appendChild(el);
452 }
402 453 }
403 454 ];
404 455
405 456 return {
406 457 list: filterActions(actions, context),
407 458 from: CodeMirror.Pos(cur.line, context.start),
408 459 to: CodeMirror.Pos(cur.line, context.end)
409 460 };
461
410 462 };
411 463 CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint);
412 464 CodeMirror.registerHelper("hint", "actions", actionHint);
413 465 return cm;
414 466 };
415 467
416 468 var setCodeMirrorMode = function(codeMirrorInstance, mode) {
417 469 CodeMirror.autoLoadMode(codeMirrorInstance, mode);
418 470 codeMirrorInstance.setOption("mode", mode);
419 471 };
420 472
421 473 var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) {
422 474 codeMirrorInstance.setOption("lineWrapping", line_wrap);
423 475 };
424 476
425 477 var setCodeMirrorModeFromSelect = function(
426 478 targetSelect, targetFileInput, codeMirrorInstance, callback){
427 479
428 480 $(targetSelect).on('change', function(e) {
429 481 cmLog.debug('codemirror select2 mode change event !');
430 482 var selected = e.currentTarget;
431 483 var node = selected.options[selected.selectedIndex];
432 484 var mimetype = node.value;
433 485 cmLog.debug('picked mimetype', mimetype);
434 486 var new_mode = $(node).attr('mode');
435 487 setCodeMirrorMode(codeMirrorInstance, new_mode);
436 488 cmLog.debug('set new mode', new_mode);
437 489
438 490 //propose filename from picked mode
439 491 cmLog.debug('setting mimetype', mimetype);
440 492 var proposed_ext = getExtFromMimeType(mimetype);
441 493 cmLog.debug('file input', $(targetFileInput).val());
442 494 var file_data = getFilenameAndExt($(targetFileInput).val());
443 495 var filename = file_data.filename || 'filename1';
444 496 $(targetFileInput).val(filename + proposed_ext);
445 497 cmLog.debug('proposed file', filename + proposed_ext);
446 498
447 499
448 500 if (typeof(callback) === 'function') {
449 501 try {
450 502 cmLog.debug('running callback', callback);
451 503 callback(filename, mimetype, new_mode);
452 504 } catch (err) {
453 505 console.log('failed to run callback', callback, err);
454 506 }
455 507 }
456 508 cmLog.debug('finish iteration...');
457 509 });
458 510 };
459 511
460 512 var setCodeMirrorModeFromInput = function(
461 513 targetSelect, targetFileInput, codeMirrorInstance, callback) {
462 514
463 515 // on type the new filename set mode
464 516 $(targetFileInput).on('keyup', function(e) {
465 517 var file_data = getFilenameAndExt(this.value);
466 518 if (file_data.ext === null) {
467 519 return;
468 520 }
469 521
470 522 var mimetypes = getMimeTypeFromExt(file_data.ext, true);
471 523 cmLog.debug('mimetype from file', file_data, mimetypes);
472 524 var detected_mode;
473 525 var detected_option;
474 526 for (var i in mimetypes) {
475 527 var mt = mimetypes[i];
476 528 if (!detected_mode) {
477 529 detected_mode = detectCodeMirrorMode(this.value, mt);
478 530 }
479 531
480 532 if (!detected_option) {
481 533 cmLog.debug('#mimetype option[value="{0}"]'.format(mt));
482 534 if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) {
483 535 detected_option = mt;
484 536 }
485 537 }
486 538 }
487 539
488 540 cmLog.debug('detected mode', detected_mode);
489 541 cmLog.debug('detected option', detected_option);
490 542 if (detected_mode && detected_option){
491 543
492 544 $(targetSelect).select2("val", detected_option);
493 545 setCodeMirrorMode(codeMirrorInstance, detected_mode);
494 546
495 547 if(typeof(callback) === 'function'){
496 548 try{
497 549 cmLog.debug('running callback', callback);
498 550 var filename = file_data.filename + "." + file_data.ext;
499 551 callback(filename, detected_option, detected_mode);
500 552 }catch (err){
501 553 console.log('failed to run callback', callback, err);
502 554 }
503 555 }
504 556 }
505 557
506 558 });
507 559 };
508 560
509 561 var fillCodeMirrorOptions = function(targetSelect) {
510 562 //inject new modes, based on codeMirrors modeInfo object
511 563 var modes_select = $(targetSelect);
512 564 for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
513 565 var m = CodeMirror.modeInfo[i];
514 566 var opt = new Option(m.name, m.mime);
515 567 $(opt).attr('mode', m.mode);
516 568 modes_select.append(opt);
517 569 }
518 570 };
519 571
520 572 var CodeMirrorPreviewEnable = function(edit_mode) {
521 573 // in case it a preview enabled mode enable the button
522 574 if (['markdown', 'rst', 'gfm'].indexOf(edit_mode) !== -1) {
523 575 $('#render_preview').removeClass('hidden');
524 576 }
525 577 else {
526 578 if (!$('#render_preview').hasClass('hidden')) {
527 579 $('#render_preview').addClass('hidden');
528 580 }
529 581 }
530 582 };
General Comments 0
You need to be logged in to leave comments. Login now