1 // Copyright (C) 2010 Adam Barth. All rights reserved.
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions are met:
6 // 1. Redistributions of source code must retain the above copyright notice,
7 // this list of conditions and the following disclaimer.
9 // 2. Redistributions in binary form must reproduce the above copyright notice,
10 // this list of conditions and the following disclaimer in the documentation
11 // and/or other materials provided with the distribution.
13 // THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
14 // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 // DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
17 // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
24 var CODE_REVIEW_UNITTEST;
28 * Create a new function with some of its arguements
30 * Taken from goog.partial in the Closure library.
31 * @param {Function} fn A function to partially apply.
32 * @param {...*} var_args Additional arguments that are partially
34 * @return {!Function} A partially-applied form of the function.
36 function partial(fn, var_args) {
37 var args = Array.prototype.slice.call(arguments, 1);
39 // Prepend the bound arguments to the current arguments.
40 var newArgs = Array.prototype.slice.call(arguments);
41 newArgs.unshift.apply(newArgs, args);
42 return fn.apply(this, newArgs);
46 function determineAttachmentID() {
48 return /id=(\d+)/.exec(window.location.search)[1]
54 // Attempt to activate only in the "Review Patch" context.
55 if (window.top != window)
58 if (!CODE_REVIEW_UNITTEST && !window.location.search.match(/action=review/)
59 && !window.location.toString().match(/bugs\.webkit\.org\/PrettyPatch/))
62 var attachment_id = determineAttachmentID();
64 console.log('No attachment ID');
68 var original_file_contents = {};
69 var patched_file_contents = {};
70 var WEBKIT_BASE_DIR = "http://svn.webkit.org/repository/webkit/trunk/";
71 var SIDE_BY_SIDE_DIFFS_KEY = 'sidebysidediffs';
72 var g_displayed_draft_comments = false;
82 function idForLine(number) {
83 return 'line' + number;
86 function nextLineID() {
87 return idForLine(next_line_id++);
90 function forEachLine(callback) {
91 for (var i = 0; i < next_line_id; ++i) {
92 callback($('#' + idForLine(i)));
97 this.id = nextLineID();
100 function hoverify() {
101 $(this).hover(function() {
102 $(this).addClass('hot');
105 $(this).removeClass('hot');
109 function fileDiffFor(line) {
110 return $(line).parents('.FileDiff');
113 function diffSectionFor(line) {
114 return $(line).parents('.DiffSection');
117 function activeCommentFor(line) {
118 // Scope to the diffSection as a performance improvement.
119 return $('textarea[data-comment-for~="' + line[0].id + '"]', fileDiffFor(line));
122 function previousCommentsFor(line) {
123 // Scope to the diffSection as a performance improvement.
124 return $('div[data-comment-for~="' + line[0].id + '"].previousComment', fileDiffFor(line));
127 function findCommentPositionFor(line) {
128 var previous_comments = previousCommentsFor(line);
129 var num_previous_comments = previous_comments.size();
130 if (num_previous_comments)
131 return $(previous_comments[num_previous_comments - 1])
135 function findCommentBlockFor(line) {
136 var comment_block = findCommentPositionFor(line).next();
137 if (!comment_block.hasClass('comment'))
139 return comment_block;
142 function insertCommentFor(line, block) {
143 findCommentPositionFor(line).after(block);
146 function addDraftComment(start_line_id, end_line_id, contents) {
147 var line = $('#' + end_line_id);
148 var start = numberFrom(start_line_id);
149 var end = numberFrom(end_line_id);
150 for (var i = start; i <= end; i++) {
151 var line = $('#line' + i);
152 line.addClass('commentContext');
153 addDataCommentBaseLine(line, end_line_id);
156 var comment_block = createCommentFor(line);
157 $(comment_block).children('textarea').val(contents);
158 freezeComment(comment_block);
161 function ensureDraftCommentsDisplayed() {
162 if (g_displayed_draft_comments)
164 g_displayed_draft_comments = true;
166 var comments = g_draftCommentSaver.saved_comments();
167 $(comments.comments).each(function() {
168 addDraftComment(this.start_line_id, this.end_line_id, this.contents);
171 var overall_comments = comments['overall-comments'];
172 if (overall_comments) {
173 openOverallComments();
174 $('.overallComments textarea').val(overall_comments);
178 function DraftCommentSaver(opt_attachment_id, opt_localStorage) {
179 this._attachment_id = opt_attachment_id || attachment_id;
180 this._localStorage = opt_localStorage || localStorage;
181 this._save_comments = true;
184 if (CODE_REVIEW_UNITTEST)
185 window['DraftCommentSaver'] = DraftCommentSaver;
187 DraftCommentSaver.prototype._json = function() {
188 var comments = $('.comment');
189 var comment_store = [];
190 comments.each(function () {
191 var file_diff = fileDiffFor(this);
192 var textarea = $('textarea', this);
194 var contents = textarea.val().trim();
198 var comment_base_line = textarea.attr('data-comment-for');
199 var lines = contextLinesFor(comment_base_line, file_diff);
202 start_line_id: lines.first().attr('id'),
203 end_line_id: comment_base_line,
208 var overall_comments = $('.overallComments textarea').val().trim();
209 return JSON.stringify({'born-on': Date.now(), 'comments': comment_store, 'overall-comments': overall_comments});
212 DraftCommentSaver.prototype.saved_comments = function() {
213 var serialized_comments = this._localStorage.getItem(DraftCommentSaver._keyPrefix + this._attachment_id);
214 if (!serialized_comments)
219 comments = JSON.parse(serialized_comments);
221 this._erase_corrupt_comments();
225 var individual_comments = comments.comments;
226 if (comments && !individual_comments.length)
229 // Sanity check comments are as expected.
230 if (!comments || !individual_comments[0].contents) {
231 this._erase_corrupt_comments();
238 DraftCommentSaver.prototype._erase_corrupt_comments = function() {
239 // FIXME: Show an error to the user instead of logging.
240 console.log('Draft comments were corrupted. Erasing comments.');
244 DraftCommentSaver.prototype.save = function() {
245 if (!this._save_comments)
248 var key = DraftCommentSaver._keyPrefix + this._attachment_id;
249 var value = this._json();
251 if (this._attemptToWrite(key, value))
254 this._eraseOldCommentsForAllReviews();
255 if (this._attemptToWrite(key, value))
258 var remove_comments = this._should_remove_comments();
259 if (!remove_comments) {
260 this._save_comments = false;
264 this._eraseCommentsForAllReviews();
265 if (this._attemptToWrite(key, value))
268 this._save_comments = false;
269 // FIXME: Show an error to the user.
272 DraftCommentSaver.prototype._should_remove_comments = function(message) {
273 return prompt('Local storage quota is full. Remove draft comments from all previous reviews to make room?');
276 DraftCommentSaver.prototype._attemptToWrite = function(key, value) {
278 this._localStorage.setItem(key, value);
285 DraftCommentSaver._keyPrefix = 'draft-comments-for-attachment-';
287 DraftCommentSaver.prototype.erase = function() {
288 this._localStorage.removeItem(DraftCommentSaver._keyPrefix + this._attachment_id);
291 DraftCommentSaver.prototype._eraseOldCommentsForAllReviews = function() {
292 this._eraseComments(true);
294 DraftCommentSaver.prototype._eraseCommentsForAllReviews = function() {
295 this._eraseComments(false);
298 var MONTH_IN_MS = 1000 * 60 * 60 * 24 * 30;
300 DraftCommentSaver.prototype._eraseComments = function(only_old_reviews) {
301 var length = this._localStorage.length;
302 var keys_to_delete = [];
303 for (var i = 0; i < length; i++) {
304 var key = this._localStorage.key(i);
305 if (key.indexOf(DraftCommentSaver._keyPrefix) != 0)
308 if (only_old_reviews) {
310 var born_on = JSON.parse(this._localStorage.getItem(key))['born-on'];
311 if (Date.now() - born_on < MONTH_IN_MS)
314 console.log('Deleting JSON. JSON for code review is corrupt: ' + key);
317 keys_to_delete.push(key);
320 for (var i = 0; i < keys_to_delete.length; i++) {
321 this._localStorage.removeItem(keys_to_delete[i]);
325 var g_draftCommentSaver = new DraftCommentSaver();
327 function saveDraftComments() {
328 ensureDraftCommentsDisplayed();
329 g_draftCommentSaver.save();
330 setAutoSaveStateIndicator('saved');
333 function setAutoSaveStateIndicator(state) {
334 var container = $('.autosave-state');
335 container.text(state);
337 if (state == 'saving')
338 container.addClass(state);
340 container.removeClass('saving');
343 function unfreezeCommentFor(line) {
344 // FIXME: This query is overly complex because we place comment blocks
345 // after Lines. Instead, comment blocks should be children of Lines.
346 findCommentPositionFor(line).next().next().filter('.frozenComment').each(handleUnfreezeComment);
349 function createCommentFor(line) {
350 if (line.attr('data-has-comment')) {
351 unfreezeCommentFor(line);
354 line.attr('data-has-comment', 'true');
355 line.addClass('commentContext');
357 var comment_block = $('<div class="comment"><textarea data-comment-for="' + line.attr('id') + '"></textarea><div class="actions"><button class="ok">OK</button><button class="discard">Discard</button></div></div>');
358 $('textarea', comment_block).bind('input', handleOverallCommentsInput);
359 insertCommentFor(line, comment_block);
360 return comment_block;
363 function addCommentFor(line) {
364 var comment_block = createCommentFor(line);
368 comment_block.hide().slideDown('fast', function() {
369 $(this).children('textarea').focus();
373 function addCommentField(comment_block) {
374 var id = $(comment_block).attr('data-comment-for');
376 id = comment_block.id;
377 addCommentFor($('#' + id));
380 function handleAddCommentField() {
381 addCommentField(this);
384 function addPreviousComment(line, author, comment_text) {
385 var line_id = line.attr('id');
386 var comment_block = $('<div data-comment-for="' + line_id + '" class="previousComment"></div>');
387 var author_block = $('<div class="author"></div>').text(author + ':');
388 var text_block = $('<div class="content"></div>').text(comment_text);
389 comment_block.append(author_block).append(text_block).each(hoverify).click(handleAddCommentField);
390 addDataCommentBaseLine(line, line_id);
391 insertCommentFor(line, comment_block);
394 function displayPreviousComments(comments) {
395 for (var i = 0; i < comments.length; ++i) {
396 var author = comments[i].author;
397 var file_name = comments[i].file_name;
398 var line_number = comments[i].line_number;
399 var comment_text = comments[i].comment_text;
401 var file = files[file_name];
403 var query = '.Line .to';
404 if (line_number[0] == '-') {
405 // The line_number represent a removal. We need to adjust the query to
406 // look at the "from" lines.
407 query = '.Line .from';
408 // Trim off the '-' control character.
409 line_number = line_number.substr(1);
412 $(file).find(query).each(function() {
413 if ($(this).text() != line_number)
415 var line = $(this).parent();
416 addPreviousComment(line, author, comment_text);
420 if (comments.length == 0) {
424 descriptor = comments.length + ' comment';
425 if (comments.length > 1)
427 $('.help').append(' This patch has ' + descriptor + '. Scroll through them with the "n" and "p" keys.');
430 function scanForStyleQueueComments(text) {
432 var lines = text.split('\n');
433 for (var i = 0; i < lines.length; ++i) {
434 var parts = lines[i].match(/^([^:]+):(-?\d+):(.*)$/);
438 var file_name = parts[1];
439 var line_number = parts[2];
440 var comment_text = parts[3].trim();
442 if (!file_name in files) {
443 console.log('Filename in style queue output is not in the patch: ' + file_name);
448 'author': 'StyleQueue',
449 'file_name': file_name,
450 'line_number': line_number,
451 'comment_text': comment_text
457 function scanForComments(author, text) {
459 var lines = text.split('\n');
460 for (var i = 0; i < lines.length; ++i) {
461 var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/);
464 var quote_markers = parts[1];
465 var file_name = parts[2];
466 // FIXME: Store multiple lines for multiline comments and correctly import them here.
467 var line_number = parts[3];
468 if (!file_name in files)
470 while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>')
472 var comment_lines = [];
473 while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) {
474 comment_lines.push(lines[i]);
477 --i; // Decrement i because the for loop will increment it again in a second.
478 var comment_text = comment_lines.join('\n').trim();
481 'file_name': file_name,
482 'line_number': line_number,
483 'comment_text': comment_text
489 function isReviewFlag(select) {
490 return $(select).attr('title') == 'Request for patch review.';
493 function isCommitQueueFlag(select) {
494 return $(select).attr('title').match(/commit-queue/);
497 function findControlForFlag(select) {
498 if (isReviewFlag(select))
499 return $('#toolbar .review select');
500 else if (isCommitQueueFlag(select))
501 return $('#toolbar .commitQueue select');
505 function addFlagsForAttachment(details) {
506 var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>";
507 $('#flagContainer').append(
508 $('<span class="review"> r: ' + flag_control + '</span>')).append(
509 $('<span class="commitQueue"> cq: ' + flag_control + '</span>'));
511 details.find('#flags select').each(function() {
512 var requestee = $(this).parent().siblings('td:first-child').text().trim();
513 if (requestee.length) {
514 // Remove trailing ':'.
515 requestee = requestee.substr(0, requestee.length - 1);
516 requestee = ' (' + requestee + ')';
518 var control = findControlForFlag(this)
519 control.attr('selectedIndex', $(this).attr('selectedIndex'));
520 control.parent().prepend(requestee);
524 window.addEventListener('message', function(e) {
525 if (e.origin != 'https://webkit-commit-queue.appspot.com')
529 $('.statusBubble')[0].style.height = e.data.height;
530 $('.statusBubble')[0].style.width = e.data.width;
534 function handleStatusBubbleLoad(e) {
535 e.target.contentWindow.postMessage('containerMetrics', 'https://webkit-commit-queue.appspot.com');
538 function fetchHistory() {
539 $.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) {
540 var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1];
541 $.get('show_bug.cgi?id=' + bug_id, function(data) {
543 $(data).find('.bz_comment').each(function() {
544 var author = $(this).find('.email').text();
545 var text = $(this).find('.bz_comment_text').text();
547 var comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
548 if (text.match(comment_marker))
549 $.merge(comments, scanForComments(author, text));
551 var style_queue_comment_marker = 'Attachment ' + attachment_id + ' .details. did not pass style-queue.'
552 if (text.match(style_queue_comment_marker))
553 $.merge(comments, scanForStyleQueueComments(text));
555 displayPreviousComments(comments);
556 ensureDraftCommentsDisplayed();
559 var details = $(data);
560 addFlagsForAttachment(details);
562 var statusBubble = document.createElement('iframe');
563 statusBubble.className = 'statusBubble';
564 statusBubble.src = 'https://webkit-commit-queue.appspot.com/status-bubble/' + attachment_id;
565 statusBubble.scrolling = 'no';
566 // Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM.
567 statusBubble.onload = handleStatusBubbleLoad;
568 $('#statusBubbleContainer').append(statusBubble);
570 $('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
574 function firstLine(file_diff) {
575 var container = $('.LineContainer:not(.context)', file_diff)[0];
579 var from = fromLineNumber(container);
580 var to = toLineNumber(container);
584 function crawlDiff() {
585 $('.Line').each(idify).each(hoverify);
586 $('.FileDiff').each(function() {
587 var header = $(this).children('h1');
588 var url_hash = '#L' + firstLine(this);
590 var file_name = header.text();
591 files[file_name] = this;
593 addExpandLinks(file_name);
595 var diff_links = $('<div class="FileDiffLinkContainer LinkContainer">' +
599 var file_link = $('a', header)[0];
600 // If the base directory in the file path does not match a WebKit top level directory,
601 // then PrettyPatch.rb doesn't linkify the header.
603 file_link.target = "_blank";
604 file_link.href += url_hash;
605 diff_links.append(tracLinks(file_name, url_hash));
608 $('h1', this).after(diff_links);
609 updateDiffLinkVisibility(this);
613 function tracLinks(file_name, url_hash) {
614 var trac_links = $('<a target="_blank">annotate</a><a target="_blank">revision log</a>');
615 trac_links[0].href = 'http://trac.webkit.org/browser/trunk/' + file_name + '?annotate=blame' + url_hash;
616 trac_links[1].href = 'http://trac.webkit.org/log/trunk/' + file_name;
620 function addExpandLinks(file_name) {
621 if (file_name.indexOf('ChangeLog') != -1)
624 var file_diff = files[file_name];
626 // Don't show the links to expand upwards/downwards if the patch starts/ends without context
627 // lines, i.e. starts/ends with add/remove lines.
628 var first_line = file_diff.querySelector('.LineContainer:not(.context)');
630 // If there is no element with a "Line" class, then this is an image diff.
634 var expand_bar_index = 0;
635 if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove'))
636 $('h1', file_diff).after(expandBarHtml(BELOW))
638 $('br', file_diff).replaceWith(expandBarHtml());
640 var last_line = file_diff.querySelector('.LineContainer:last-of-type');
641 // Some patches for new files somehow end up with an empty context line at the end
642 // with a from line number of 0. Don't show expand links in that case either.
643 if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0)
644 $('.revision', file_diff).before(expandBarHtml(ABOVE));
647 function expandBarHtml(opt_direction) {
648 var html = '<div class="ExpandBar">' +
649 '<div class="ExpandArea Expand' + ABOVE + '"></div>' +
650 '<div class="ExpandLinkContainer LinkContainer"><span class="ExpandText">expand: </span>';
652 // FIXME: If there are <100 line to expand, don't show the expand-100 link.
653 // If there are <20 lines to expand, don't show the expand-20 link.
654 if (!opt_direction || opt_direction == ABOVE) {
655 html += expandLinkHtml(ABOVE, 100) +
656 expandLinkHtml(ABOVE, 20);
659 html += expandLinkHtml(ALL);
661 if (!opt_direction || opt_direction == BELOW) {
662 html += expandLinkHtml(BELOW, 20) +
663 expandLinkHtml(BELOW, 100);
666 html += '</div><div class="ExpandArea Expand' + BELOW + '"></div></div>';
670 function expandLinkHtml(direction, amount) {
671 return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" +
672 (amount ? amount + " " : "") + direction + "</a>";
675 function handleExpandLinkClick() {
676 var expand_bar = $(this).parents('.ExpandBar');
677 var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent;
678 var expand_function = partial(expand, expand_bar[0], file_name, this.getAttribute('data-direction'), Number(this.getAttribute('data-amount')));
679 if (file_name in original_file_contents)
682 getWebKitSourceFile(file_name, expand_function, expand_bar);
685 function handleSideBySideLinkClick() {
686 convertDiff('sidebyside', this);
689 function handleUnifyLinkClick() {
690 convertDiff('unified', this);
693 function convertDiff(difftype, convert_link) {
694 var file_diffs = $(convert_link).parents('.FileDiff');
695 if (!file_diffs.size()) {
696 localStorage.setItem('code-review-diffstate', difftype);
697 file_diffs = $('.FileDiff');
700 convertAllFileDiffs(difftype, file_diffs);
703 function patchRevision() {
704 var revision = $('.revision');
705 return revision[0] ? revision.first().text() : null;
708 function getWebKitSourceFile(file_name, onLoad, expand_bar) {
709 function handleLoad(contents) {
710 original_file_contents[file_name] = contents.split('\n');
711 patched_file_contents[file_name] = applyDiff(original_file_contents[file_name], file_name);
715 var revision = patchRevision();
716 var queryParameters = revision ? '?p=' + revision : '';
719 url: WEBKIT_BASE_DIR + file_name + queryParameters,
720 context: document.body,
721 complete: function(xhr, data) {
723 handleLoadError(expand_bar);
725 handleLoad(xhr.responseText);
730 function replaceExpandLinkContainers(expand_bar, text) {
731 $('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>');
734 function handleLoadError(expand_bar) {
735 replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?");
742 function lineNumbersFromSet(set, is_last) {
746 var size = set.size();
747 var start = is_last ? (size - 1) : 0;
748 var end = is_last ? -1 : size;
749 var offset = is_last ? -1 : 1;
751 for (var i = start; i != end; i += offset) {
752 if (to != -1 && from != -1)
753 return {to: to, from: from};
755 var line_number = set[i];
756 if ($(line_number).hasClass('to')) {
758 to = Number(line_number.textContent);
761 from = Number(line_number.textContent);
766 function expand(expand_bar, file_name, direction, amount) {
767 if (file_name in original_file_contents && !patched_file_contents[file_name]) {
768 // FIXME: In this case, try fetching the source file at the revision the patch was created at.
769 // Might need to modify webkit-patch to include that data in the diff.
770 replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree.");
774 var above_expansion = expand_bar.querySelector('.Expand' + ABOVE)
775 var below_expansion = expand_bar.querySelector('.Expand' + BELOW)
777 var above_line_numbers = $('.expansionLineNumber', above_expansion);
778 if (!above_line_numbers[0]) {
779 var diff_section = expand_bar.previousElementSibling;
780 above_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
783 var above_last_line_num, above_last_from_line_num;
784 if (above_line_numbers[0]) {
785 var above_numbers = lineNumbersFromSet(above_line_numbers, true);
786 above_last_line_num = above_numbers.to;
787 above_last_from_line_num = above_numbers.from;
789 above_last_from_line_num = above_last_line_num = 0;
791 var below_line_numbers = $('.expansionLineNumber', below_expansion);
792 if (!below_line_numbers[0]) {
793 var diff_section = expand_bar.nextElementSibling;
795 below_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
798 var below_first_line_num, below_first_from_line_num;
799 if (below_line_numbers[0]) {
800 var below_numbers = lineNumbersFromSet(below_line_numbers, false);
801 below_first_line_num = below_numbers.to - 1;
802 below_first_from_line_num = below_numbers.from - 1;
804 below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1;
806 var start_line_num, start_from_line_num;
809 if (direction == ABOVE) {
810 start_from_line_num = above_last_from_line_num;
811 start_line_num = above_last_line_num;
812 end_line_num = Math.min(start_line_num + amount, below_first_line_num);
813 } else if (direction == BELOW) {
814 end_line_num = below_first_line_num;
815 start_line_num = Math.max(end_line_num - amount, above_last_line_num)
816 start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num)
817 } else { // direction == ALL
818 start_line_num = above_last_line_num;
819 start_from_line_num = above_last_from_line_num;
820 end_line_num = below_first_line_num;
823 var lines = expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num);
826 // Filling in all the remaining lines. Overwrite the expand links.
827 if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) {
828 $('.ExpandLinkContainer', expand_bar).detach();
829 below_expansion.insertBefore(lines, below_expansion.firstChild);
830 // Now that we're filling in all the lines, the context line following this expand bar is no longer needed.
831 $('.context', expand_bar.nextElementSibling).detach();
832 } else if (direction == ABOVE) {
833 above_expansion.appendChild(lines);
835 below_expansion.insertBefore(lines, below_expansion.firstChild);
839 function unifiedLine(from, to, contents, is_expansion_line, opt_className, opt_attributes) {
840 var className = is_expansion_line ? 'ExpansionLine' : 'LineContainer Line';
842 className += ' ' + opt_className;
844 var lineNumberClassName = is_expansion_line ? 'expansionLineNumber' : 'lineNumber';
846 var line = $('<div class="' + className + '" ' + (opt_attributes || '') + '>' +
847 '<span class="from ' + lineNumberClassName + '">' + (from || ' ') +
848 '</span><span class="to ' + lineNumberClassName + '">' + (to || ' ') +
849 '</span><span class="text"></span>' +
852 $('.text', line).replaceWith(contents);
856 function unifiedExpansionLine(from, to, contents) {
857 return unifiedLine(from, to, contents, true);
860 function sideBySideExpansionLine(from, to, contents) {
861 var line = $('<div class="ExpansionLine"></div>');
862 // Clone the contents so we have two copies we can put back in the DOM.
863 line.append(lineSide('from', contents.clone(true), true, from));
864 line.append(lineSide('to', contents, true, to));
868 function lineSide(side, contents, is_expansion_line, opt_line_number, opt_attributes, opt_class) {
870 if (opt_attributes || opt_class) {
871 class_name = 'class="';
873 class_name += is_expansion_line ? 'ExpansionLine' : 'Line';
874 class_name += ' ' + (opt_class || '') + '"';
877 var attributes = opt_attributes || '';
879 var line_side = $('<div class="LineSide">' +
880 '<div ' + attributes + ' ' + class_name + '>' +
881 '<span class="' + side + ' ' + (is_expansion_line ? 'expansionLineNumber' : 'lineNumber') + '">' +
882 (opt_line_number || ' ') +
884 '<span class="text"></span>' +
888 $('.text', line_side).replaceWith(contents);
892 function expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) {
893 var fragment = document.createDocumentFragment();
894 var is_side_by_side = isDiffSideBySide(files[file_name]);
896 for (var i = 0; i < end_line_num - start_line_num; i++) {
897 var from = start_from_line_num + i + 1;
898 var to = start_line_num + i + 1;
899 var contents = $('<span class="text"></span>');
900 contents.text(patched_file_contents[file_name][start_line_num + i]);
901 var line = is_side_by_side ? sideBySideExpansionLine(from, to, contents) : unifiedExpansionLine(from, to, contents);
902 fragment.appendChild(line[0]);
908 function hunkStartingLine(patched_file, context, prev_line, hunk_num) {
910 var current_line = -1;
911 var last_context_line = context[context.length - 1];
912 if (patched_file[prev_line] == last_context_line)
913 current_line = prev_line + 1;
915 for (var i = prev_line - PATCH_FUZZ; i < prev_line + PATCH_FUZZ; i++) {
916 if (patched_file[i] == last_context_line)
917 current_line = i + 1;
920 if (current_line == -1) {
921 console.log('Hunk #' + hunk_num + ' FAILED.');
926 // For paranoia sake, confirm the rest of the context matches;
927 for (var i = 0; i < context.length - 1; i++) {
928 if (patched_file[current_line - context.length + i] != context[i]) {
929 console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.');
937 function fromLineNumber(line) {
938 var node = line.querySelector('.from');
939 return node ? Number(node.textContent) : 0;
942 function toLineNumber(line) {
943 var node = line.querySelector('.to');
944 return node ? Number(node.textContent) : 0;
947 function textContentsFor(line) {
948 // Just get the first match since a side-by-side diff has two lines with text inside them for
949 // unmodified lines in the diff.
950 return $('.text', line).first().text();
953 function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) {
954 if (context.length) {
955 var prev_line_num = fromLineNumber(prev_line) - 1;
956 return hunkStartingLine(patched_file, context, prev_line_num, hunk_num);
959 if (toLineNumber(line) == 1 || fromLineNumber(line) == 1)
962 console.log('Failed to apply patch. Adds or removes lines before any context lines.');
966 function applyDiff(original_file, file_name) {
967 var diff_sections = files[file_name].getElementsByClassName('DiffSection');
968 var patched_file = original_file.concat([]);
970 // Apply diffs in reverse order to avoid needing to keep track of changing line numbers.
971 for (var i = diff_sections.length - 1; i >= 0; i--) {
972 var section = diff_sections[i];
973 var lines = $('.Line:not(.context)', section);
974 var current_line = -1;
976 var hunk_num = i + 1;
978 for (var j = 0, lines_len = lines.length; j < lines_len; j++) {
980 var line_contents = textContentsFor(line);
981 if ($(line).hasClass('add')) {
982 if (current_line == -1) {
983 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
984 if (current_line == -1)
988 patched_file.splice(current_line, 0, line_contents);
990 } else if ($(line).hasClass('remove')) {
991 if (current_line == -1) {
992 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
993 if (current_line == -1)
997 if (patched_file[current_line] != line_contents) {
998 console.log('Hunk #' + hunk_num + ' FAILED.');
1002 patched_file.splice(current_line, 1);
1003 } else if (current_line == -1) {
1004 context.push(line_contents);
1005 } else if (line_contents != patched_file[current_line]) {
1006 console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match');
1014 return patched_file;
1017 function openOverallComments(e) {
1018 $('.overallComments textarea').addClass('open');
1019 $('#statusBubbleContainer').addClass('wrap');
1022 var g_overallCommentsInputTimer;
1024 function handleOverallCommentsInput() {
1025 setAutoSaveStateIndicator('saving');
1026 // Save draft comments after we haven't received an input event in 1 second.
1027 if (g_overallCommentsInputTimer)
1028 clearTimeout(g_overallCommentsInputTimer);
1029 g_overallCommentsInputTimer = setTimeout(saveDraftComments, 1000);
1032 function onBodyResize() {
1033 updateToolbarAnchorState();
1036 function updateToolbarAnchorState() {
1037 var toolbar = $('#toolbar');
1038 // Unanchor the toolbar and then see if it's bottom is below the body's bottom.
1039 toolbar.toggleClass('anchored', false);
1040 var toolbar_bottom = toolbar.offset().top + toolbar.outerHeight();
1041 var should_anchor = toolbar_bottom >= document.body.clientHeight;
1042 toolbar.toggleClass('anchored', should_anchor);
1045 function diffLinksHtml() {
1046 return '<a href="javascript:" class="unify-link">unified</a>' +
1047 '<a href="javascript:" class="side-by-side-link">side-by-side</a>';
1050 $(document).ready(function() {
1053 $(document.body).prepend('<div id="message">' +
1054 '<div class="help">Select line numbers to add a comment. Scroll though diffs with the "j" and "k" keys.' +
1055 '<div class="DiffLinks LinkContainer">' + diffLinksHtml() + '</div>' +
1058 $(document.body).append('<div id="toolbar">' +
1059 '<div class="overallComments">' +
1060 '<textarea placeholder="Overall comments"></textarea>' +
1063 '<span id="statusBubbleContainer"></span>' +
1064 '<span class="actions">' +
1065 '<span class="links"><span class="bugLink"></span></span>' +
1066 '<span id="flagContainer"></span>' +
1067 '<button id="preview_comments">Preview</button>' +
1068 '<button id="post_comments">Publish</button> ' +
1071 '<div class="autosave-state"></div>' +
1074 $('.overallComments textarea').bind('click', openOverallComments);
1075 $('.overallComments textarea').bind('input', handleOverallCommentsInput);
1077 $(document.body).prepend('<div id="comment_form" class="inactive"><div class="winter"></div><div class="lightbox"><iframe id="reviewform" src="attachment.cgi?id=' + attachment_id + '&action=reviewform"></iframe></div></div>');
1078 $('#reviewform').bind('load', handleReviewFormLoad);
1080 // Create a dummy iframe and monitor resizes in it's contentWindow to detect when the top document's body changes size.
1081 // FIXME: Should we setTimeout throttle these?
1082 var resize_iframe = $('<iframe class="pseudo_resize_event_iframe"></iframe>');
1083 $(document.body).append(resize_iframe);
1084 // Handle the event on a timeout to avoid crashing Firefox.
1085 $(resize_iframe[0].contentWindow).bind('resize', function() { setTimeout(onBodyResize, 0)});
1087 updateToolbarAnchorState();
1091 function handleReviewFormLoad() {
1092 var review_form_contents = $('#reviewform').contents();
1093 if (review_form_contents[0].querySelector('#form-controls #flags')) {
1094 review_form_contents.bind('keydown', function(e) {
1095 if (e.keyCode == KEY_CODE.escape)
1099 // This is the intial load of the review form iframe.
1100 var form = review_form_contents.find('form')[0];
1101 form.addEventListener('submit', eraseDraftComments);
1106 // Review form iframe have the publish button has been pressed.
1107 var email_sent_to = review_form_contents[0].querySelector('#bugzilla-body dl');
1108 // If the email_send_to DL is not in the tree that means the publish failed for some reason,
1109 // e.g., you're not logged in. Show the comment form to allow you to login.
1110 if (!email_sent_to) {
1115 eraseDraftComments();
1116 // FIXME: Once WebKit supports seamless iframes, we can just make the review-form
1117 // iframe fill the page instead of redirecting back to the bug.
1118 window.location.replace($('#toolbar .bugLink a').attr('href'));
1121 function eraseDraftComments() {
1122 g_draftCommentSaver.erase();
1125 function loadDiffState() {
1126 var diffstate = localStorage.getItem('code-review-diffstate');
1127 if (diffstate != 'sidebyside' && diffstate != 'unified')
1130 convertAllFileDiffs(diffstate, $('.FileDiff'));
1133 function isDiffSideBySide(file_diff) {
1134 return diffState(file_diff) == 'sidebyside';
1137 function diffState(file_diff) {
1138 var diff_state = $(file_diff).attr('data-diffstate');
1139 return diff_state || 'unified';
1142 function unifyLine(line, from, to, contents, classNames, attributes, id) {
1143 var new_line = unifiedLine(from, to, contents, false, classNames, attributes);
1144 var old_line = $(line);
1145 if (!old_line.hasClass('LineContainer'))
1146 old_line = old_line.parents('.LineContainer');
1148 var comments = commentsToTransferFor($(document.getElementById(id)));
1149 old_line.after(comments);
1150 old_line.replaceWith(new_line);
1153 function updateDiffLinkVisibility(file_diff) {
1154 if (diffState(file_diff) == 'unified') {
1155 $('.side-by-side-link', file_diff).show();
1156 $('.unify-link', file_diff).hide();
1158 $('.side-by-side-link', file_diff).hide();
1159 $('.unify-link', file_diff).show();
1163 function convertAllFileDiffs(diff_type, file_diffs) {
1164 file_diffs.each(function() {
1165 convertFileDiff(diff_type, this);
1169 function convertFileDiff(diff_type, file_diff) {
1170 if (diffState(file_diff) == diff_type)
1173 $(file_diff).removeClass('sidebyside unified');
1174 $(file_diff).addClass(diff_type);
1176 $(file_diff).attr('data-diffstate', diff_type);
1177 updateDiffLinkVisibility(file_diff);
1179 $('.context', file_diff).each(function() {
1180 convertLine(diff_type, this);
1183 $('.shared .Line', file_diff).each(function() {
1184 convertLine(diff_type, this);
1187 $('.ExpansionLine', file_diff).each(function() {
1188 convertExpansionLine(diff_type, this);
1192 function convertLine(diff_type, line) {
1193 var convert_function = diff_type == 'sidebyside' ? sideBySideifyLine : unifyLine;
1194 var from = fromLineNumber(line);
1195 var to = toLineNumber(line);
1196 var contents = $('.text', line).first();
1197 var classNames = classNamesForMovingLine(line);
1198 var attributes = attributesForMovingLine(line);
1200 convert_function(line, from, to, contents, classNames, attributes, id)
1203 function classNamesForMovingLine(line) {
1204 var classParts = line.className.split(' ');
1205 var classBuffer = [];
1206 for (var i = 0; i < classParts.length; i++) {
1207 var part = classParts[i];
1208 if (part != 'LineContainer' && part != 'Line')
1209 classBuffer.push(part);
1211 return classBuffer.join(' ');
1214 function attributesForMovingLine(line) {
1215 var attributesBuffer = ['id=' + line.id];
1216 // Make sure to keep all data- attributes.
1217 $(line.attributes).each(function() {
1218 if (this.name.indexOf('data-') == 0)
1219 attributesBuffer.push(this.name + '=' + this.value);
1221 return attributesBuffer.join(' ');
1224 function sideBySideifyLine(line, from, to, contents, classNames, attributes, id) {
1225 var from_class = '';
1227 var from_attributes = '';
1228 var to_attributes = '';
1229 // Clone the contents so we have two copies we can put back in the DOM.
1230 var from_contents = contents.clone(true);
1231 var to_contents = contents;
1233 var container_class = 'LineContainer';
1234 var container_attributes = '';
1236 if (from && !to) { // This is a remove line.
1237 from_class = classNames;
1238 from_attributes = attributes;
1240 } else if (to && !from) { // This is an add line.
1241 to_class = classNames;
1242 to_attributes = attributes;
1245 container_attributes = attributes;
1246 container_class += ' Line ' + classNames;
1249 var new_line = $('<div ' + container_attributes + ' class="' + container_class + '"></div>');
1250 new_line.append(lineSide('from', from_contents, false, from, from_attributes, from_class));
1251 new_line.append(lineSide('to', to_contents, false, to, to_attributes, to_class));
1253 $(line).replaceWith(new_line);
1255 var line = $(document.getElementById(id));
1256 line.after(commentsToTransferFor(line));
1259 function convertExpansionLine(diff_type, line) {
1260 var convert_function = diff_type == 'sidebyside' ? sideBySideExpansionLine : unifiedExpansionLine;
1261 var contents = $('.text', line).first();
1262 var from = fromLineNumber(line);
1263 var to = toLineNumber(line);
1264 var new_line = convert_function(from, to, contents);
1265 $(line).replaceWith(new_line);
1268 function commentsToTransferFor(line) {
1269 var fragment = document.createDocumentFragment();
1271 previousCommentsFor(line).each(function() {
1272 fragment.appendChild(this);
1275 var active_comments = activeCommentFor(line);
1276 var num_active_comments = active_comments.size();
1277 if (num_active_comments > 0) {
1278 if (num_active_comments > 1)
1279 console.log('ERROR: There is more than one active comment for ' + line.attr('id') + '.');
1281 var parent = active_comments[0].parentNode;
1282 var frozenComment = parent.nextSibling;
1283 fragment.appendChild(parent);
1284 fragment.appendChild(frozenComment);
1290 function discardComment(comment_block) {
1291 var line_id = comment_block.find('textarea').attr('data-comment-for');
1292 var line = $('#' + line_id)
1293 findCommentBlockFor(line).slideUp('fast', function() {
1295 line.removeAttr('data-has-comment');
1296 trimCommentContextToBefore(line, line.attr('data-comment-base-line'));
1297 saveDraftComments();
1301 function handleUnfreezeComment() {
1302 unfreezeComment(this);
1305 function unfreezeComment(comment) {
1306 var unfrozen_comment = $(comment).prev();
1307 unfrozen_comment.show();
1308 $(comment).remove();
1309 unfrozen_comment.find('textarea')[0].focus();
1312 function showFileDiffLinks() {
1313 $('.LinkContainer', this).each(function() { this.style.opacity = 1; });
1316 function hideFileDiffLinks() {
1317 $('.LinkContainer', this).each(function() { this.style.opacity = 0; });
1320 function handleDiscardComment() {
1321 discardComment($(this).parents('.comment'));
1324 function handleAcceptComment() {
1325 acceptComment($(this).parents('.comment'));
1328 function acceptComment(comment) {
1329 freezeComment(comment);
1330 saveDraftComments();
1333 $('.FileDiff').live('mouseenter', showFileDiffLinks);
1334 $('.FileDiff').live('mouseleave', hideFileDiffLinks);
1335 $('.side-by-side-link').live('click', handleSideBySideLinkClick);
1336 $('.unify-link').live('click', handleUnifyLinkClick);
1337 $('.ExpandLink').live('click', handleExpandLinkClick);
1338 $('.frozenComment').live('click', handleUnfreezeComment);
1339 $('.comment .discard').live('click', handleDiscardComment);
1340 $('.comment .ok').live('click', handleAcceptComment);
1342 function freezeComment(comment_block) {
1343 var comment_textarea = comment_block.find('textarea');
1344 if (comment_textarea.val().trim() == '') {
1345 discardComment(comment_block);
1348 var line_id = comment_textarea.attr('data-comment-for');
1349 var line = $('#' + line_id)
1350 findCommentBlockFor(line).hide().after($('<div class="frozenComment"></div>').text(comment_textarea.val()));
1353 function focusOn(node) {
1354 if (node.length == 0)
1357 // Give a tabindex so the element can receive actual browser focus.
1358 // -1 makes the element focusable without actually putting in in the tab order.
1359 node.attr('tabindex', -1);
1361 $(document).scrollTop(node.position().top - window.innerHeight / 2);
1364 function focusNext(filter, direction) {
1365 var focusable_nodes = $('a,.Line,.frozenComment,.previousComment,.DiffBlock,.overallComments').filter(function() {
1366 return !$(this).hasClass('DiffBlock') || $('.add,.remove', this).size();
1369 var is_backward = direction == DIRECTION.BACKWARD;
1370 var index = focusable_nodes.index($(document.activeElement));
1371 if (index == -1 && is_backward)
1372 index = focusable_nodes.length;
1374 var offset = is_backward ? -1 : 1;
1375 var end = is_backward ? -1 : focusable_nodes.size();
1376 for (var i = index + offset; i != end; i = i + offset) {
1377 var node = $(focusable_nodes[i]);
1386 var DIRECTION = {FORWARD: 1, BACKWARD: 2};
1388 function isComment(node) {
1389 return node.hasClass('frozenComment') || node.hasClass('previousComment') || node.hasClass('overallComments');
1392 function isDiffBlock(node) {
1393 return node.hasClass('DiffBlock');
1396 function isLine(node) {
1397 return node.hasClass('Line');
1400 $('textarea').live('keydown', function(e) {
1401 if (e.keyCode == KEY_CODE.escape)
1402 handleEscapeKeyInTextarea(this);
1405 $('body').live('keydown', function(e) {
1406 // FIXME: There's got to be a better way to avoid seeing these keypress
1408 if (e.target.nodeName == 'TEXTAREA')
1411 var handled = false;
1413 switch (e.keyCode) {
1415 handled = focusNext(isComment, DIRECTION.FORWARD);
1419 handled = focusNext(isComment, DIRECTION.BACKWARD);
1424 handled = focusNext(isLine, DIRECTION.FORWARD);
1426 handled = focusNext(isDiffBlock, DIRECTION.FORWARD);
1431 handled = focusNext(isLine, DIRECTION.BACKWARD);
1433 handled = focusNext(isDiffBlock, DIRECTION.BACKWARD);
1436 case KEY_CODE.enter:
1437 handled = handleEnterKeyPress();
1445 function handleEscapeKeyInTextarea(textarea) {
1446 var comment = $(textarea).parents('.comment');
1448 acceptComment(comment);
1451 document.body.focus();
1454 function handleEnterKeyPress() {
1455 if (document.activeElement.nodeName == 'BODY')
1458 var focused = $(document.activeElement);
1460 if (focused.hasClass('frozenComment')) {
1461 unfreezeComment(focused);
1465 if (focused.hasClass('overallComments')) {
1466 openOverallComments();
1467 focused.find('textarea')[0].focus();
1471 if (focused.hasClass('previousComment')) {
1472 addCommentField(focused);
1476 var lines = $('.Line', focused);
1477 var last = lines.last();
1478 if (last.attr('data-has-comment')) {
1479 unfreezeCommentFor(last);
1483 addCommentForLines(lines);
1487 function contextLinesFor(comment_base_lines, file_diff) {
1488 var base_lines = comment_base_lines.split(' ');
1489 return $('div[data-comment-base-line]', file_diff).filter(function() {
1490 return $(this).attr('data-comment-base-line').split(' ').some(function(item) {
1491 return base_lines.indexOf(item) != -1;
1496 function numberFrom(line_id) {
1497 return Number(line_id.replace('line', ''));
1500 function trimCommentContextToBefore(line, comment_base_line) {
1501 var line_to_trim_to = numberFrom(line.attr('id'));
1502 contextLinesFor(comment_base_line, fileDiffFor(line)).each(function() {
1503 var id = $(this).attr('id');
1504 if (numberFrom(id) > line_to_trim_to)
1507 removeDataCommentBaseLine(this, comment_base_line);
1508 if (!$(this).attr('data-comment-base-line'))
1509 $(this).removeClass('commentContext');
1513 var drag_select_start_index = -1;
1515 function lineOffsetFrom(line, offset) {
1516 var file_diff = line.parents('.FileDiff');
1517 var all_lines = $('.Line', file_diff);
1518 var index = all_lines.index(line);
1519 return $(all_lines[index + offset]);
1522 function previousLineFor(line) {
1523 return lineOffsetFrom(line, -1);
1526 function nextLineFor(line) {
1527 return lineOffsetFrom(line, 1);
1530 $(document.body).bind('mouseup', processSelectedLines);
1532 $('.lineNumber').live('click', function(e) {
1533 var line = lineFromLineDescendant($(this));
1534 if (line.hasClass('commentContext'))
1535 trimCommentContextToBefore(previousLineFor(line), line.attr('data-comment-base-line'));
1536 else if (e.shiftKey)
1537 extendCommentContextTo(line);
1538 }).live('mousedown', function(e) {
1539 // preventDefault to avoid selecting text when dragging to select comment context lines.
1540 // FIXME: should we use user-modify CSS instead?
1545 var line = lineFromLineDescendant($(this));
1546 drag_select_start_index = numberFrom(line.attr('id'));
1547 line.addClass('selected');
1550 $('.LineContainer').live('mouseenter', function(e) {
1551 if (drag_select_start_index == -1 || e.shiftKey)
1553 selectToLineContainer(this);
1554 }).live('mouseup', function(e) {
1555 if (drag_select_start_index == -1 || e.shiftKey)
1558 selectToLineContainer(this);
1559 processSelectedLines();
1562 function extendCommentContextTo(line) {
1563 var diff_section = diffSectionFor(line);
1564 var lines = $('.Line', diff_section);
1565 var lines_to_modify = [];
1566 var have_seen_start_line = false;
1567 var data_comment_base_line = null;
1568 lines.each(function() {
1569 if (data_comment_base_line)
1572 have_seen_start_line = have_seen_start_line || this == line[0];
1574 if (have_seen_start_line) {
1575 if ($(this).hasClass('commentContext'))
1576 data_comment_base_line = $(this).attr('data-comment-base-line');
1578 lines_to_modify.push(this);
1582 // There is no comment context to extend.
1583 if (!data_comment_base_line)
1586 $(lines_to_modify).each(function() {
1587 $(this).addClass('commentContext');
1588 $(this).attr('data-comment-base-line', data_comment_base_line);
1592 function selectTo(focus_index) {
1593 var selected = $('.selected').removeClass('selected');
1594 var is_backward = drag_select_start_index > focus_index;
1595 var current_index = is_backward ? focus_index : drag_select_start_index;
1596 var last_index = is_backward ? drag_select_start_index : focus_index;
1597 while (current_index <= last_index) {
1598 $('#line' + current_index).addClass('selected')
1603 function selectToLineContainer(line_container) {
1604 var line = lineFromLineContainer(line_container);
1606 // Ensure that the selected lines are all contained in the same DiffSection.
1607 var selected_lines = $('.selected');
1608 var selected_diff_section = diffSectionFor(selected_lines.first());
1609 var new_diff_section = diffSectionFor(line);
1610 if (new_diff_section[0] != selected_diff_section[0]) {
1611 var lines = $('.Line', selected_diff_section);
1612 if (numberFrom(selected_lines.first().attr('id')) == drag_select_start_index)
1613 line = lines.last();
1615 line = lines.first();
1618 selectTo(numberFrom(line.attr('id')));
1621 function processSelectedLines() {
1622 drag_select_start_index = -1;
1623 addCommentForLines($('.selected'));
1626 function addCommentForLines(lines) {
1630 var already_has_comment = lines.last().hasClass('commentContext');
1631 lines.addClass('commentContext');
1633 var comment_base_line;
1634 if (already_has_comment)
1635 comment_base_line = lines.last().attr('data-comment-base-line');
1637 var last = lineFromLineDescendant(lines.last());
1638 addCommentFor($(last));
1639 comment_base_line = last.attr('id');
1642 lines.each(function() {
1643 addDataCommentBaseLine(this, comment_base_line);
1644 $(this).removeClass('selected');
1647 saveDraftComments();
1650 function addDataCommentBaseLine(line, id) {
1651 var val = $(line).attr('data-comment-base-line');
1653 var parts = val ? val.split(' ') : [];
1654 for (var i = 0; i < parts.length; i++) {
1660 $(line).attr('data-comment-base-line', parts.join(' '));
1663 function removeDataCommentBaseLine(line, comment_base_lines) {
1664 var val = $(line).attr('data-comment-base-line');
1668 var base_lines = comment_base_lines.split(' ');
1669 var parts = val.split(' ');
1671 for (var i = 0; i < parts.length; i++) {
1672 if (base_lines.indexOf(parts[i]) == -1)
1673 newVal.push(parts[i]);
1676 $(line).attr('data-comment-base-line', newVal.join(' '));
1679 function lineFromLineDescendant(descendant) {
1680 return descendant.hasClass('Line') ? descendant : descendant.parents('.Line');
1683 function lineFromLineContainer(lineContainer) {
1684 var line = $(lineContainer);
1685 if (!line.hasClass('Line'))
1686 line = $('.Line', line);
1690 function contextSnippetFor(line, indent) {
1692 contextLinesFor(line.attr('id'), fileDiffFor(line)).each(function() {
1694 if ($(this).hasClass('add'))
1696 else if ($(this).hasClass('remove'))
1698 snippets.push(indent + action + textContentsFor(this));
1700 return snippets.join('\n');
1703 function fileNameFor(line) {
1704 return fileDiffFor(line).find('h1').text();
1707 function indentFor(depth) {
1708 return (new Array(depth + 1)).join('>') + ' ';
1711 function snippetFor(line, indent) {
1712 var file_name = fileNameFor(line);
1713 var line_number = line.hasClass('remove') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]);
1714 return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent);
1717 function quotePreviousComments(comments) {
1718 var quoted_comments = [];
1719 var depth = comments.size();
1720 comments.each(function() {
1721 var indent = indentFor(depth--);
1722 var text = $(this).children('.content').text();
1723 quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent));
1725 return quoted_comments.join('\n');
1728 $('#comment_form .winter').live('click', hideCommentForm);
1730 function fillInReviewForm() {
1731 var comments_in_context = []
1732 forEachLine(function(line) {
1733 if (line.attr('data-has-comment') != 'true')
1735 var comment = findCommentBlockFor(line).children('textarea').val().trim();
1738 var previous_comments = previousCommentsFor(line);
1739 var snippet = snippetFor(line, indentFor(previous_comments.size() + 1));
1740 var quoted_comments = quotePreviousComments(previous_comments);
1741 var comment_with_context = [];
1742 comment_with_context.push(snippet);
1743 if (quoted_comments != '')
1744 comment_with_context.push(quoted_comments);
1745 comment_with_context.push('\n' + comment);
1746 comments_in_context.push(comment_with_context.join('\n'));
1748 var comment = $('.overallComments textarea').val().trim();
1751 comment += comments_in_context.join('\n\n');
1752 if (comments_in_context.length > 0)
1753 comment = 'View in context: ' + window.location + '\n\n' + comment;
1754 var review_form = $('#reviewform').contents();
1755 review_form.find('#comment').val(comment);
1756 review_form.find('#flags select').each(function() {
1757 var control = findControlForFlag(this);
1758 if (!control.size())
1760 $(this).attr('selectedIndex', control.attr('selectedIndex'));
1764 function showCommentForm() {
1765 $('#comment_form').removeClass('inactive');
1766 $('#reviewform').contents().find('#submitBtn').focus();
1769 function hideCommentForm() {
1770 $('#comment_form').addClass('inactive');
1772 // Make sure the top document has focus so key events don't keep going to the review form.
1773 document.body.tabIndex = -1;
1774 document.body.focus();
1777 $('#preview_comments').live('click', function() {
1782 $('#post_comments').live('click', function() {
1784 $('#reviewform').contents().find('form').submit();