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');
66 var minLeftSideRatio = 10;
67 var maxLeftSideRatio = 90;
68 var file_diff_being_resized = null;
71 var original_file_contents = {};
72 var patched_file_contents = {};
73 var WEBKIT_BASE_DIR = "http://svn.webkit.org/repository/webkit/trunk/";
74 var SIDE_BY_SIDE_DIFFS_KEY = 'sidebysidediffs';
75 var g_displayed_draft_comments = false;
88 function idForLine(number) {
89 return 'line' + number;
92 function nextLineID() {
93 return idForLine(next_line_id++);
96 function forEachLine(callback) {
97 for (var i = 0; i < next_line_id; ++i) {
98 callback($('#' + idForLine(i)));
103 this.id = nextLineID();
106 function hoverify() {
107 $(this).hover(function() {
108 $(this).addClass('hot');
111 $(this).removeClass('hot');
115 function fileDiffFor(line) {
116 return $(line).parents('.FileDiff');
119 function diffSectionFor(line) {
120 return $(line).parents('.DiffSection');
123 function activeCommentFor(line) {
124 // Scope to the diffSection as a performance improvement.
125 return $('textarea[data-comment-for~="' + line[0].id + '"]', fileDiffFor(line));
128 function previousCommentsFor(line) {
129 // Scope to the diffSection as a performance improvement.
130 return $('div[data-comment-for~="' + line[0].id + '"].previousComment', fileDiffFor(line));
133 function findCommentPositionFor(line) {
134 var previous_comments = previousCommentsFor(line);
135 var num_previous_comments = previous_comments.size();
136 if (num_previous_comments)
137 return $(previous_comments[num_previous_comments - 1])
141 function findCommentBlockFor(line) {
142 var comment_block = findCommentPositionFor(line).next();
143 if (!comment_block.hasClass('comment'))
145 return comment_block;
148 function insertCommentFor(line, block) {
149 findCommentPositionFor(line).after(block);
152 function addDraftComment(start_line_id, end_line_id, contents) {
153 var line = $('#' + end_line_id);
154 var start = numberFrom(start_line_id);
155 var end = numberFrom(end_line_id);
156 for (var i = start; i <= end; i++) {
157 addDataCommentBaseLine($('#line' + i), end_line_id);
160 var comment_block = createCommentFor(line);
161 $(comment_block).children('textarea').val(contents);
162 freezeComment(comment_block);
165 function ensureDraftCommentsDisplayed() {
166 if (g_displayed_draft_comments)
168 g_displayed_draft_comments = true;
170 var comments = g_draftCommentSaver.saved_comments();
172 $(comments.comments).each(function() {
174 addDraftComment(this.start_line_id, this.end_line_id, this.contents);
176 errors.push({'start': this.start_line_id, 'end': this.end_line_id, 'contents': this.contents});
181 console.log('DRAFT COMMENTS WITH ERRORS:', JSON.stringify(errors));
182 alert('Some draft comments failed to be added. See the console to manually resolve.');
185 var overall_comments = comments['overall-comments'];
186 if (overall_comments) {
187 openOverallComments();
188 $('.overallComments textarea').val(overall_comments);
192 function DraftCommentSaver(opt_attachment_id, opt_localStorage) {
193 this._attachment_id = opt_attachment_id || attachment_id;
194 this._localStorage = opt_localStorage || localStorage;
195 this._save_comments = true;
198 DraftCommentSaver.prototype._json = function() {
199 var comments = $('.comment');
200 var comment_store = [];
201 comments.each(function () {
202 var file_diff = fileDiffFor(this);
203 var textarea = $('textarea', this);
205 var contents = textarea.val().trim();
209 var comment_base_line = textarea.attr('data-comment-for');
210 var lines = contextLinesFor(comment_base_line, file_diff);
213 start_line_id: lines.first().attr('id'),
214 end_line_id: comment_base_line,
219 var overall_comments = $('.overallComments textarea').val().trim();
220 return JSON.stringify({'born-on': Date.now(), 'comments': comment_store, 'overall-comments': overall_comments});
223 DraftCommentSaver.prototype.localStorageKey = function() {
224 return DraftCommentSaver._keyPrefix + this._attachment_id;
227 DraftCommentSaver.prototype.saved_comments = function() {
228 var serialized_comments = this._localStorage.getItem(this.localStorageKey());
229 if (!serialized_comments)
234 comments = JSON.parse(serialized_comments);
236 this._erase_corrupt_comments();
240 var individual_comments = comments.comments;
241 if (!comments || !comments['born-on'] || !individual_comments || (individual_comments.length && !individual_comments[0].contents)) {
242 this._erase_corrupt_comments();
248 DraftCommentSaver.prototype._erase_corrupt_comments = function() {
249 // FIXME: Show an error to the user instead of logging.
250 console.log('Draft comments were corrupted. Erasing comments.');
254 DraftCommentSaver.prototype.save = function() {
255 if (!this._save_comments)
258 var key = this.localStorageKey();
259 var value = this._json();
261 if (this._attemptToWrite(key, value))
264 this._eraseOldCommentsForAllReviews();
265 if (this._attemptToWrite(key, value))
268 var remove_comments = this._should_remove_comments();
269 if (!remove_comments) {
270 this._save_comments = false;
274 this._eraseCommentsForAllReviews();
275 if (this._attemptToWrite(key, value))
278 this._save_comments = false;
279 // FIXME: Show an error to the user.
282 DraftCommentSaver.prototype._should_remove_comments = function(message) {
283 return prompt('Local storage quota is full. Remove draft comments from all previous reviews to make room?');
286 DraftCommentSaver.prototype._attemptToWrite = function(key, value) {
288 this._localStorage.setItem(key, value);
295 DraftCommentSaver._keyPrefix = 'draft-comments-for-attachment-';
297 DraftCommentSaver.prototype.erase = function() {
298 this._localStorage.removeItem(this.localStorageKey());
301 DraftCommentSaver.prototype._eraseOldCommentsForAllReviews = function() {
302 this._eraseComments(true);
304 DraftCommentSaver.prototype._eraseCommentsForAllReviews = function() {
305 this._eraseComments(false);
308 var MONTH_IN_MS = 1000 * 60 * 60 * 24 * 30;
310 DraftCommentSaver.prototype._eraseComments = function(only_old_reviews) {
311 var length = this._localStorage.length;
312 var keys_to_delete = [];
313 for (var i = 0; i < length; i++) {
314 var key = this._localStorage.key(i);
315 if (key.indexOf(DraftCommentSaver._keyPrefix) != 0)
318 if (only_old_reviews) {
320 var born_on = JSON.parse(this._localStorage.getItem(key))['born-on'];
321 if (Date.now() - born_on < MONTH_IN_MS)
324 console.log('Deleting JSON. JSON for code review is corrupt: ' + key);
327 keys_to_delete.push(key);
330 for (var i = 0; i < keys_to_delete.length; i++) {
331 this._localStorage.removeItem(keys_to_delete[i]);
335 var g_draftCommentSaver = new DraftCommentSaver();
337 function saveDraftComments() {
338 ensureDraftCommentsDisplayed();
339 g_draftCommentSaver.save();
340 setAutoSaveStateIndicator('saved');
343 function setAutoSaveStateIndicator(state) {
344 var container = $('.autosave-state');
345 container.text(state);
347 if (state == 'saving')
348 container.addClass(state);
350 container.removeClass('saving');
353 function unfreezeCommentFor(line) {
354 // FIXME: This query is overly complex because we place comment blocks
355 // after Lines. Instead, comment blocks should be children of Lines.
356 findCommentPositionFor(line).next().next().filter('.frozenComment').each(handleUnfreezeComment);
359 function createCommentFor(line) {
360 if (line.attr('data-has-comment')) {
361 unfreezeCommentFor(line);
364 line.attr('data-has-comment', 'true');
365 line.addClass('commentContext');
367 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>');
368 $('textarea', comment_block).bind('input', handleOverallCommentsInput);
369 insertCommentFor(line, comment_block);
370 return comment_block;
373 function addCommentFor(line) {
374 var comment_block = createCommentFor(line);
378 comment_block.hide().slideDown('fast', function() {
379 $(this).children('textarea').focus();
381 return comment_block;
384 function addCommentField(comment_block) {
385 var id = $(comment_block).attr('data-comment-for');
387 id = comment_block.id;
388 return addCommentFor($('#' + id));
391 function handleAddCommentField() {
392 addCommentField(this);
395 function addPreviousComment(line, author, comment_text) {
396 var line_id = $(line).attr('id');
397 var comment_block = $('<div data-comment-for="' + line_id + '" class="previousComment"></div>');
398 var author_block = $('<div class="author"></div>').text(author + ':');
399 var text_block = $('<div class="content"></div>').text(comment_text);
400 comment_block.append(author_block).append(text_block).each(hoverify).click(handleAddCommentField);
401 addDataCommentBaseLine($(line), line_id);
402 insertCommentFor($(line), comment_block);
405 function displayPreviousComments(comments) {
406 for (var i = 0; i < comments.length; ++i) {
407 var author = comments[i].author;
408 var file_name = comments[i].file_name;
409 var line_number = comments[i].line_number;
410 var comment_text = comments[i].comment_text;
412 var file = files[file_name];
414 var query = '.Line .to';
415 if (line_number[0] == '-') {
416 // The line_number represent a removal. We need to adjust the query to
417 // look at the "from" lines.
418 query = '.Line .from';
419 // Trim off the '-' control character.
420 line_number = line_number.substr(1);
423 $(file).find(query).each(function() {
424 if ($(this).text() != line_number)
426 var line = $(this).parent();
427 addPreviousComment(line, author, comment_text);
431 if (comments.length == 0) {
435 descriptor = comments.length + ' comment';
436 if (comments.length > 1)
438 $('.help .more').before(' This patch has ' + descriptor + '. Scroll through them with the "n" and "p" keys. ');
441 function showMoreHelp() {
442 $('.more-help').removeClass('inactive');
445 function hideMoreHelp() {
446 $('.more-help').addClass('inactive');
449 function scanForStyleQueueComments(text) {
451 var lines = text.split('\n');
452 for (var i = 0; i < lines.length; ++i) {
453 var parts = lines[i].match(/^([^:]+):(-?\d+):(.*)$/);
457 var file_name = parts[1];
458 var line_number = parts[2];
459 var comment_text = parts[3].trim();
461 if (!file_name in files) {
462 console.log('Filename in style queue output is not in the patch: ' + file_name);
467 'author': 'StyleQueue',
468 'file_name': file_name,
469 'line_number': line_number,
470 'comment_text': comment_text
476 function scanForComments(author, text) {
478 var lines = text.split('\n');
479 for (var i = 0; i < lines.length; ++i) {
480 var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/);
483 var quote_markers = parts[1];
484 var file_name = parts[2];
485 // FIXME: Store multiple lines for multiline comments and correctly import them here.
486 var line_number = parts[3];
487 if (!file_name in files)
489 while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>')
491 var comment_lines = [];
492 while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) {
493 comment_lines.push(lines[i]);
496 --i; // Decrement i because the for loop will increment it again in a second.
497 var comment_text = comment_lines.join('\n').trim();
500 'file_name': file_name,
501 'line_number': line_number,
502 'comment_text': comment_text
508 function isReviewFlag(select) {
509 return $(select).attr('title') == 'Request for patch review.';
512 function isCommitQueueFlag(select) {
513 return $(select).attr('title').match(/commit-queue/);
516 function findControlForFlag(select) {
517 if (isReviewFlag(select))
518 return $('#toolbar .review select');
519 else if (isCommitQueueFlag(select))
520 return $('#toolbar .commitQueue select');
524 function addFlagsForAttachment(details) {
525 var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>";
526 $('#flagContainer').append(
527 $('<span class="review"> r: ' + flag_control + '</span>')).append(
528 $('<span class="commitQueue"> cq: ' + flag_control + '</span>'));
530 details.find('#flags select').each(function() {
531 var requestee = $(this).parent().siblings('td:first-child').text().trim();
532 if (requestee.length) {
533 // Remove trailing ':'.
534 requestee = requestee.substr(0, requestee.length - 1);
535 requestee = ' (' + requestee + ')';
537 var control = findControlForFlag(this)
538 control.attr('selectedIndex', $(this).attr('selectedIndex'));
539 control.parent().prepend(requestee);
543 window.addEventListener('message', function(e) {
544 if (e.origin != 'https://webkit-commit-queue.appspot.com')
548 $('.statusBubble')[0].style.height = e.data.height;
549 $('.statusBubble')[0].style.width = e.data.width;
553 function handleStatusBubbleLoad(e) {
554 e.target.contentWindow.postMessage('containerMetrics', 'https://webkit-commit-queue.appspot.com');
557 function fetchHistory() {
558 $.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) {
559 var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1];
560 $.get('show_bug.cgi?id=' + bug_id, function(data) {
562 $(data).find('.bz_comment').each(function() {
563 var author = $(this).find('.email').text();
564 var text = $(this).find('.bz_comment_text').text();
566 var comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
567 if (text.match(comment_marker))
568 $.merge(comments, scanForComments(author, text));
570 var style_queue_comment_marker = 'Attachment ' + attachment_id + ' .details. did not pass style-queue.'
571 if (text.match(style_queue_comment_marker))
572 $.merge(comments, scanForStyleQueueComments(text));
574 displayPreviousComments(comments);
575 ensureDraftCommentsDisplayed();
578 var details = $(data);
579 addFlagsForAttachment(details);
581 var statusBubble = document.createElement('iframe');
582 statusBubble.className = 'statusBubble';
583 statusBubble.src = 'https://webkit-commit-queue.appspot.com/status-bubble/' + attachment_id;
584 statusBubble.scrolling = 'no';
585 // Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM.
586 statusBubble.onload = handleStatusBubbleLoad;
587 $('#statusBubbleContainer').append(statusBubble);
589 $('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
593 function firstLine(file_diff) {
594 var container = $('.LineContainer:not(.context)', file_diff)[0];
598 var from = fromLineNumber(container);
599 var to = toLineNumber(container);
603 function crawlDiff() {
604 $('.Line').each(idify).each(hoverify);
605 $('.FileDiff').each(function() {
606 var header = $(this).children('h1');
607 var url_hash = '#L' + firstLine(this);
609 var file_name = header.text();
610 files[file_name] = this;
612 addExpandLinks(file_name);
614 var diff_links = $('<div class="FileDiffLinkContainer LinkContainer">' +
618 var file_link = $('a', header)[0];
619 // If the base directory in the file path does not match a WebKit top level directory,
620 // then PrettyPatch.rb doesn't linkify the header.
622 file_link.target = "_blank";
623 file_link.href += url_hash;
624 diff_links.append(tracLinks(file_name, url_hash));
627 $('h1', this).after(diff_links);
628 updateDiffLinkVisibility(this);
632 function tracLinks(file_name, url_hash) {
633 var trac_links = $('<a target="_blank">annotate</a><a target="_blank">revision log</a>');
634 trac_links[0].href = 'http://trac.webkit.org/browser/trunk/' + file_name + '?annotate=blame' + url_hash;
635 trac_links[1].href = 'http://trac.webkit.org/log/trunk/' + file_name;
636 var implementation_suffix_list = ['.cpp', '.mm'];
637 for (var i = 0; i < implementation_suffix_list.length; ++i) {
638 var suffix = implementation_suffix_list[i];
639 if (file_name.lastIndexOf(suffix) == file_name.length - suffix.length) {
640 trac_links.prepend('<a target="_blank">header</a>');
641 var stem = file_name.substr(0, file_name.length - suffix.length);
642 trac_links[0].href= 'http://trac.webkit.org/log/trunk/' + stem + '.h';
648 function isChangeLog(file_name) {
649 return file_name.match(/\/ChangeLog$/) || file_name == 'ChangeLog';
652 function addExpandLinks(file_name) {
653 if (isChangeLog(file_name))
656 var file_diff = files[file_name];
658 // Don't show the links to expand upwards/downwards if the patch starts/ends without context
659 // lines, i.e. starts/ends with add/remove lines.
660 var first_line = file_diff.querySelector('.LineContainer:not(.context)');
662 // If there is no element with a "Line" class, then this is an image diff.
666 var expand_bar_index = 0;
667 if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove'))
668 $('h1', file_diff).after(expandBarHtml(BELOW))
670 $('br', file_diff).replaceWith(expandBarHtml());
672 // jquery doesn't support :last-of-type, so use querySelector instead.
673 var last_line = file_diff.querySelector('.LineContainer:last-of-type');
674 // Some patches for new files somehow end up with an empty context line at the end
675 // with a from line number of 0. Don't show expand links in that case either.
676 if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0)
677 $(file_diff.querySelector('.DiffSection:last-of-type')).after(expandBarHtml(ABOVE));
680 function expandBarHtml(opt_direction) {
681 var html = '<div class="ExpandBar">' +
682 '<div class="ExpandArea Expand' + ABOVE + '"></div>' +
683 '<div class="ExpandLinkContainer LinkContainer"><span class="ExpandText">expand: </span>';
685 // FIXME: If there are <100 line to expand, don't show the expand-100 link.
686 // If there are <20 lines to expand, don't show the expand-20 link.
687 if (!opt_direction || opt_direction == ABOVE) {
688 html += expandLinkHtml(ABOVE, 100) +
689 expandLinkHtml(ABOVE, 20);
692 html += expandLinkHtml(ALL);
694 if (!opt_direction || opt_direction == BELOW) {
695 html += expandLinkHtml(BELOW, 20) +
696 expandLinkHtml(BELOW, 100);
699 html += '</div><div class="ExpandArea Expand' + BELOW + '"></div></div>';
703 function expandLinkHtml(direction, amount) {
704 return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" +
705 (amount ? amount + " " : "") + direction + "</a>";
708 function handleExpandLinkClick() {
709 var expand_bar = $(this).parents('.ExpandBar');
710 var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent;
711 var expand_function = partial(expand, expand_bar[0], file_name, this.getAttribute('data-direction'), Number(this.getAttribute('data-amount')));
712 if (file_name in original_file_contents)
715 getWebKitSourceFile(file_name, expand_function, expand_bar);
718 function handleSideBySideLinkClick() {
719 convertDiff('sidebyside', this);
722 function handleUnifyLinkClick() {
723 convertDiff('unified', this);
726 function convertDiff(difftype, convert_link) {
727 var file_diffs = $(convert_link).parents('.FileDiff');
728 if (!file_diffs.size()) {
729 localStorage.setItem('code-review-diffstate', difftype);
730 file_diffs = $('.FileDiff');
733 convertAllFileDiffs(difftype, file_diffs);
736 function patchRevision() {
737 var revision = $('.revision');
738 return revision[0] ? revision.first().text() : null;
741 function getWebKitSourceFile(file_name, onLoad, expand_bar) {
742 function handleLoad(contents) {
743 original_file_contents[file_name] = contents.split('\n');
744 patched_file_contents[file_name] = applyDiff(original_file_contents[file_name], file_name);
748 var revision = patchRevision();
749 var queryParameters = revision ? '?p=' + revision : '';
752 url: WEBKIT_BASE_DIR + file_name + queryParameters,
753 context: document.body,
754 complete: function(xhr, data) {
756 handleLoadError(expand_bar);
758 handleLoad(xhr.responseText);
763 function replaceExpandLinkContainers(expand_bar, text) {
764 $('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>');
767 function handleLoadError(expand_bar) {
768 replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?");
775 function lineNumbersFromSet(set, is_last) {
779 var size = set.size();
780 var start = is_last ? (size - 1) : 0;
781 var end = is_last ? -1 : size;
782 var offset = is_last ? -1 : 1;
784 for (var i = start; i != end; i += offset) {
785 if (to != -1 && from != -1)
786 return {to: to, from: from};
788 var line_number = set[i];
789 if ($(line_number).hasClass('to')) {
791 to = Number(line_number.textContent);
794 from = Number(line_number.textContent);
799 function removeContextBarBelow(expand_bar) {
800 $('.context', expand_bar.nextElementSibling).detach();
803 function expand(expand_bar, file_name, direction, amount) {
804 if (file_name in original_file_contents && !patched_file_contents[file_name]) {
805 // FIXME: In this case, try fetching the source file at the revision the patch was created at.
806 // Might need to modify webkit-patch to include that data in the diff.
807 replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree.");
811 var above_expansion = expand_bar.querySelector('.Expand' + ABOVE)
812 var below_expansion = expand_bar.querySelector('.Expand' + BELOW)
814 var above_line_numbers = $('.expansionLineNumber', above_expansion);
815 if (!above_line_numbers[0]) {
816 var diff_section = expand_bar.previousElementSibling;
817 above_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
820 var above_last_line_num, above_last_from_line_num;
821 if (above_line_numbers[0]) {
822 var above_numbers = lineNumbersFromSet(above_line_numbers, true);
823 above_last_line_num = above_numbers.to;
824 above_last_from_line_num = above_numbers.from;
826 above_last_from_line_num = above_last_line_num = 0;
828 var below_line_numbers = $('.expansionLineNumber', below_expansion);
829 if (!below_line_numbers[0]) {
830 var diff_section = expand_bar.nextElementSibling;
832 below_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
835 var below_first_line_num, below_first_from_line_num;
836 if (below_line_numbers[0]) {
837 var below_numbers = lineNumbersFromSet(below_line_numbers, false);
838 below_first_line_num = below_numbers.to - 1;
839 below_first_from_line_num = below_numbers.from - 1;
841 below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1;
843 var start_line_num, start_from_line_num;
846 if (direction == ABOVE) {
847 start_from_line_num = above_last_from_line_num;
848 start_line_num = above_last_line_num;
849 end_line_num = Math.min(start_line_num + amount, below_first_line_num);
850 } else if (direction == BELOW) {
851 end_line_num = below_first_line_num;
852 start_line_num = Math.max(end_line_num - amount, above_last_line_num)
853 start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num)
854 } else { // direction == ALL
855 start_line_num = above_last_line_num;
856 start_from_line_num = above_last_from_line_num;
857 end_line_num = below_first_line_num;
860 var lines = expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num);
863 // Filling in all the remaining lines. Overwrite the expand links.
864 if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) {
865 $('.ExpandLinkContainer', expand_bar).detach();
866 below_expansion.insertBefore(lines, below_expansion.firstChild);
867 removeContextBarBelow(expand_bar);
868 } else if (direction == ABOVE) {
869 above_expansion.appendChild(lines);
871 below_expansion.insertBefore(lines, below_expansion.firstChild);
872 removeContextBarBelow(expand_bar);
876 function unifiedLine(from, to, contents, is_expansion_line, opt_className, opt_attributes) {
877 var className = is_expansion_line ? 'ExpansionLine' : 'LineContainer Line';
879 className += ' ' + opt_className;
881 var lineNumberClassName = is_expansion_line ? 'expansionLineNumber' : 'lineNumber';
883 var line = $('<div class="' + className + '" ' + (opt_attributes || '') + '>' +
884 '<span class="from ' + lineNumberClassName + '">' + (from || ' ') +
885 '</span><span class="to ' + lineNumberClassName + '">' + (to || ' ') +
886 '</span><span class="text"></span>' +
889 $('.text', line).replaceWith(contents);
893 function unifiedExpansionLine(from, to, contents) {
894 return unifiedLine(from, to, contents, true);
897 function sideBySideExpansionLine(from, to, contents) {
898 var line = $('<div class="ExpansionLine"></div>');
899 // Clone the contents so we have two copies we can put back in the DOM.
900 line.append(lineSide('from', contents.clone(true), true, from));
901 line.append(lineSide('to', contents, true, to));
905 function lineSide(side, contents, is_expansion_line, opt_line_number, opt_attributes, opt_class) {
907 if (opt_attributes || opt_class) {
908 class_name = 'class="';
910 class_name += is_expansion_line ? 'ExpansionLine' : 'Line';
911 class_name += ' ' + (opt_class || '') + '"';
914 var attributes = opt_attributes || '';
916 var line_side = $('<div class="LineSide">' +
917 '<div ' + attributes + ' ' + class_name + '>' +
918 '<span class="' + side + ' ' + (is_expansion_line ? 'expansionLineNumber' : 'lineNumber') + '">' +
919 (opt_line_number || ' ') +
921 '<span class="text"></span>' +
925 $('.text', line_side).replaceWith(contents);
929 function expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) {
930 var fragment = document.createDocumentFragment();
931 var is_side_by_side = isDiffSideBySide(files[file_name]);
933 for (var i = 0; i < end_line_num - start_line_num; i++) {
934 var from = start_from_line_num + i + 1;
935 var to = start_line_num + i + 1;
936 var contents = $('<span class="text"></span>');
937 contents.text(patched_file_contents[file_name][start_line_num + i]);
938 var line = is_side_by_side ? sideBySideExpansionLine(from, to, contents) : unifiedExpansionLine(from, to, contents);
939 fragment.appendChild(line[0]);
945 function hunkStartingLine(patched_file, context, prev_line, hunk_num) {
946 var current_line = -1;
947 var last_context_line = context[context.length - 1];
948 if (patched_file[prev_line] == last_context_line)
949 current_line = prev_line + 1;
951 console.log('Hunk #' + hunk_num + ' FAILED.');
955 // For paranoia sake, confirm the rest of the context matches;
956 for (var i = 0; i < context.length - 1; i++) {
957 if (patched_file[current_line - context.length + i] != context[i]) {
958 console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.');
966 function fromLineNumber(line) {
967 var node = line.querySelector('.from');
968 return node ? Number(node.textContent) : 0;
971 function toLineNumber(line) {
972 var node = line.querySelector('.to');
973 return node ? Number(node.textContent) : 0;
976 function textContentsFor(line) {
977 // Just get the first match since a side-by-side diff has two lines with text inside them for
978 // unmodified lines in the diff.
979 return $('.text', line).first().text();
982 function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) {
983 if (context.length) {
984 var prev_line_num = fromLineNumber(prev_line) - 1;
985 return hunkStartingLine(patched_file, context, prev_line_num, hunk_num);
988 if (toLineNumber(line) == 1 || fromLineNumber(line) == 1)
991 console.log('Failed to apply patch. Adds or removes lines before any context lines.');
995 function applyDiff(original_file, file_name) {
996 var diff_sections = files[file_name].getElementsByClassName('DiffSection');
997 var patched_file = original_file.concat([]);
999 // Apply diffs in reverse order to avoid needing to keep track of changing line numbers.
1000 for (var i = diff_sections.length - 1; i >= 0; i--) {
1001 var section = diff_sections[i];
1002 var lines = $('.Line:not(.context)', section);
1003 var current_line = -1;
1005 var hunk_num = i + 1;
1007 for (var j = 0, lines_len = lines.length; j < lines_len; j++) {
1008 var line = lines[j];
1009 var line_contents = textContentsFor(line);
1010 if ($(line).hasClass('add')) {
1011 if (current_line == -1) {
1012 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
1013 if (current_line == -1)
1017 patched_file.splice(current_line, 0, line_contents);
1019 } else if ($(line).hasClass('remove')) {
1020 if (current_line == -1) {
1021 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
1022 if (current_line == -1)
1026 if (patched_file[current_line] != line_contents) {
1027 console.log('Hunk #' + hunk_num + ' FAILED.');
1031 patched_file.splice(current_line, 1);
1032 } else if (current_line == -1) {
1033 context.push(line_contents);
1034 } else if (line_contents != patched_file[current_line]) {
1035 console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match');
1043 return patched_file;
1046 function openOverallComments(e) {
1047 $('.overallComments textarea').addClass('open');
1048 $('#statusBubbleContainer').addClass('wrap');
1051 var g_overallCommentsInputTimer;
1053 function handleOverallCommentsInput() {
1054 setAutoSaveStateIndicator('saving');
1055 // Save draft comments after we haven't received an input event in 1 second.
1056 if (g_overallCommentsInputTimer)
1057 clearTimeout(g_overallCommentsInputTimer);
1058 g_overallCommentsInputTimer = setTimeout(saveDraftComments, 1000);
1061 function onBodyResize() {
1062 updateToolbarAnchorState();
1065 function updateToolbarAnchorState() {
1066 // For iPad, we always leave the toolbar at the bottom of the document
1067 // because of the iPad's handling of position:fixed and scrolling.
1068 if (navigator.platform.indexOf("iPad") != -1)
1071 var toolbar = $('#toolbar');
1072 // Unanchor the toolbar and then see if it's bottom is below the body's bottom.
1073 toolbar.toggleClass('anchored', false);
1074 var toolbar_bottom = toolbar.offset().top + toolbar.outerHeight();
1075 var should_anchor = toolbar_bottom >= document.body.clientHeight;
1076 toolbar.toggleClass('anchored', should_anchor);
1079 function diffLinksHtml() {
1080 return '<a href="javascript:" class="unify-link">unified</a>' +
1081 '<a href="javascript:" class="side-by-side-link">side-by-side</a>';
1084 function appendToolbar() {
1085 $(document.body).append('<div id="toolbar">' +
1086 '<div class="overallComments">' +
1087 '<textarea placeholder="Overall comments"></textarea>' +
1090 '<span id="statusBubbleContainer"></span>' +
1091 '<span class="actions">' +
1092 '<span class="links"><span class="bugLink"></span></span>' +
1093 '<span id="flagContainer"></span>' +
1094 '<button id="preview_comments">Preview</button>' +
1095 '<button id="post_comments">Publish</button> ' +
1098 '<div class="autosave-state"></div>' +
1101 $('.overallComments textarea').bind('click', openOverallComments);
1102 $('.overallComments textarea').bind('input', handleOverallCommentsInput);
1105 function handleDocumentReady() {
1108 $(document.body).prepend('<div id="message">' +
1109 '<div class="help">Select line numbers to add a comment. Scroll though diffs with the "j" and "k" keys.' +
1110 '<div class="DiffLinks LinkContainer">' + diffLinksHtml() + '</div>' +
1111 '<a href="javascript:" class="more">[more]</a>' +
1112 '<div class="more-help inactive">' +
1113 '<div class="winter"></div>' +
1114 '<div class="lightbox"><table>' +
1115 '<tr><td>enter</td><td>add/edit comment for focused item</td></tr>' +
1116 '<tr><td>escape</td><td>accept current comment / close preview and help popups</td></tr>' +
1117 '<tr><td>j</td><td>focus next diff</td></tr>' +
1118 '<tr><td>k</td><td>focus previous diff</td></tr>' +
1119 '<tr><td>shift + j</td><td>focus next line</td></tr>' +
1120 '<tr><td>shift + k</td><td>focus previous line</td></tr>' +
1121 '<tr><td>n</td><td>focus next comment</td></tr>' +
1122 '<tr><td>p</td><td>focus previous comment</td></tr>' +
1123 '<tr><td>r</td><td>focus review select element</td></tr>' +
1124 '<tr><td>ctrl + shift + up</td><td>extend context of the focused comment</td></tr>' +
1125 '<tr><td>ctrl + shift + down</td><td>shrink context of the focused comment</td></tr>' +
1133 $(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>');
1134 $('#reviewform').bind('load', handleReviewFormLoad);
1136 // Create a dummy iframe and monitor resizes in it's contentWindow to detect when the top document's body changes size.
1137 // FIXME: Should we setTimeout throttle these?
1138 var resize_iframe = $('<iframe class="pseudo_resize_event_iframe"></iframe>');
1139 $(document.body).append(resize_iframe);
1140 // Handle the event on a timeout to avoid crashing Firefox.
1141 $(resize_iframe[0].contentWindow).bind('resize', function() { setTimeout(onBodyResize, 0)});
1143 updateToolbarAnchorState();
1145 generateFileDiffResizeStyleElement();
1148 function handleReviewFormLoad() {
1149 var review_form_contents = $('#reviewform').contents();
1150 if (review_form_contents[0].querySelector('#form-controls #flags')) {
1151 review_form_contents.bind('keydown', function(e) {
1152 if (e.keyCode == KEY_CODE.escape)
1156 // This is the intial load of the review form iframe.
1157 var form = review_form_contents.find('form')[0];
1158 form.addEventListener('submit', eraseDraftComments);
1163 // Review form iframe have the publish button has been pressed.
1164 var email_sent_to = review_form_contents[0].querySelector('#bugzilla-body dl');
1165 // If the email_send_to DL is not in the tree that means the publish failed for some reason,
1166 // e.g., you're not logged in. Show the comment form to allow you to login.
1167 if (!email_sent_to) {
1172 eraseDraftComments();
1173 // FIXME: Once WebKit supports seamless iframes, we can just make the review-form
1174 // iframe fill the page instead of redirecting back to the bug.
1175 window.location.replace($('#toolbar .bugLink a').attr('href'));
1178 function eraseDraftComments() {
1179 g_draftCommentSaver.erase();
1182 function loadDiffState() {
1183 var diffstate = localStorage.getItem('code-review-diffstate');
1184 if (diffstate != 'sidebyside' && diffstate != 'unified')
1187 convertAllFileDiffs(diffstate, $('.FileDiff'));
1190 function isDiffSideBySide(file_diff) {
1191 return diffState(file_diff) == 'sidebyside';
1194 function diffState(file_diff) {
1195 var diff_state = $(file_diff).attr('data-diffstate');
1196 return diff_state || 'unified';
1199 function unifyLine(line, from, to, contents, classNames, attributes, id) {
1200 var new_line = unifiedLine(from, to, contents, false, classNames, attributes);
1201 var old_line = $(line);
1202 if (!old_line.hasClass('LineContainer'))
1203 old_line = old_line.parents('.LineContainer');
1205 var comments = commentsToTransferFor($(document.getElementById(id)));
1206 old_line.after(comments);
1207 old_line.replaceWith(new_line);
1210 function updateDiffLinkVisibility(file_diff) {
1211 if (diffState(file_diff) == 'unified') {
1212 $('.side-by-side-link', file_diff).show();
1213 $('.unify-link', file_diff).hide();
1215 $('.side-by-side-link', file_diff).hide();
1216 $('.unify-link', file_diff).show();
1220 function convertAllFileDiffs(diff_type, file_diffs) {
1221 file_diffs.each(function() {
1222 convertFileDiff(diff_type, this);
1226 function convertFileDiff(diff_type, file_diff) {
1227 if (diffState(file_diff) == diff_type)
1230 if (!$('.resizeHandle', file_diff).length)
1231 $(file_diff).append('<div class="resizeHandle"></div>');
1233 $(file_diff).removeClass('sidebyside unified');
1234 $(file_diff).addClass(diff_type);
1236 $(file_diff).attr('data-diffstate', diff_type);
1237 updateDiffLinkVisibility(file_diff);
1239 $('.context', file_diff).each(function() {
1240 convertLine(diff_type, this);
1243 $('.shared .Line', file_diff).each(function() {
1244 convertLine(diff_type, this);
1247 $('.ExpansionLine', file_diff).each(function() {
1248 convertExpansionLine(diff_type, this);
1252 function convertLine(diff_type, line) {
1253 var convert_function = diff_type == 'sidebyside' ? sideBySideifyLine : unifyLine;
1254 var from = fromLineNumber(line);
1255 var to = toLineNumber(line);
1256 var contents = $('.text', line).first();
1257 var classNames = classNamesForMovingLine(line);
1258 var attributes = attributesForMovingLine(line);
1260 convert_function(line, from, to, contents, classNames, attributes, id)
1263 function classNamesForMovingLine(line) {
1264 var classParts = line.className.split(' ');
1265 var classBuffer = [];
1266 for (var i = 0; i < classParts.length; i++) {
1267 var part = classParts[i];
1268 if (part != 'LineContainer' && part != 'Line')
1269 classBuffer.push(part);
1271 return classBuffer.join(' ');
1274 function attributesForMovingLine(line) {
1275 var attributesBuffer = ['id=' + line.id];
1276 // Make sure to keep all data- attributes.
1277 $(line.attributes).each(function() {
1278 if (this.name.indexOf('data-') == 0)
1279 attributesBuffer.push(this.name + '=' + this.value);
1281 return attributesBuffer.join(' ');
1284 function sideBySideifyLine(line, from, to, contents, classNames, attributes, id) {
1285 var from_class = '';
1287 var from_attributes = '';
1288 var to_attributes = '';
1289 // Clone the contents so we have two copies we can put back in the DOM.
1290 var from_contents = contents.clone(true);
1291 var to_contents = contents;
1293 var container_class = 'LineContainer';
1294 var container_attributes = '';
1296 if (from && !to) { // This is a remove line.
1297 from_class = classNames;
1298 from_attributes = attributes;
1300 } else if (to && !from) { // This is an add line.
1301 to_class = classNames;
1302 to_attributes = attributes;
1305 container_attributes = attributes;
1306 container_class += ' Line ' + classNames;
1309 var new_line = $('<div ' + container_attributes + ' class="' + container_class + '"></div>');
1310 new_line.append(lineSide('from', from_contents, false, from, from_attributes, from_class));
1311 new_line.append(lineSide('to', to_contents, false, to, to_attributes, to_class));
1313 $(line).replaceWith(new_line);
1315 var line = $(document.getElementById(id));
1316 line.after(commentsToTransferFor(line));
1319 function convertExpansionLine(diff_type, line) {
1320 var convert_function = diff_type == 'sidebyside' ? sideBySideExpansionLine : unifiedExpansionLine;
1321 var contents = $('.text', line).first();
1322 var from = fromLineNumber(line);
1323 var to = toLineNumber(line);
1324 var new_line = convert_function(from, to, contents);
1325 $(line).replaceWith(new_line);
1328 function commentsToTransferFor(line) {
1329 var fragment = document.createDocumentFragment();
1331 previousCommentsFor(line).each(function() {
1332 fragment.appendChild(this);
1335 var active_comments = activeCommentFor(line);
1336 var num_active_comments = active_comments.size();
1337 if (num_active_comments > 0) {
1338 if (num_active_comments > 1)
1339 console.log('ERROR: There is more than one active comment for ' + line.attr('id') + '.');
1341 var parent = active_comments[0].parentNode;
1342 var frozenComment = parent.nextSibling;
1343 fragment.appendChild(parent);
1344 fragment.appendChild(frozenComment);
1350 function discardComment(comment_block) {
1351 var line_id = $(comment_block).find('textarea').attr('data-comment-for');
1352 var line = $('#' + line_id)
1353 $(comment_block).slideUp('fast', function() {
1355 line.removeAttr('data-has-comment');
1356 trimCommentContextToBefore(line, line_id);
1357 saveDraftComments();
1361 function handleUnfreezeComment() {
1362 unfreezeComment(this);
1365 function unfreezeComment(comment) {
1366 var unfrozen_comment = $(comment).prev();
1367 unfrozen_comment.show();
1368 $(comment).remove();
1369 unfrozen_comment.find('textarea')[0].focus();
1372 function showFileDiffLinks() {
1373 $('.LinkContainer', this).each(function() { this.style.opacity = 1; });
1376 function hideFileDiffLinks() {
1377 $('.LinkContainer', this).each(function() { this.style.opacity = 0; });
1380 function handleDiscardComment() {
1381 discardComment($(this).parents('.comment'));
1384 function handleAcceptComment() {
1385 acceptComment($(this).parents('.comment'));
1388 function acceptComment(comment) {
1389 var frozen_comment = freezeComment($(comment));
1390 focusOn(frozen_comment);
1391 saveDraftComments();
1392 return frozen_comment;
1395 $('.FileDiff').live('mouseenter', showFileDiffLinks);
1396 $('.FileDiff').live('mouseleave', hideFileDiffLinks);
1397 $('.side-by-side-link').live('click', handleSideBySideLinkClick);
1398 $('.unify-link').live('click', handleUnifyLinkClick);
1399 $('.ExpandLink').live('click', handleExpandLinkClick);
1400 $('.frozenComment').live('click', handleUnfreezeComment);
1401 $('.comment .discard').live('click', handleDiscardComment);
1402 $('.comment .ok').live('click', handleAcceptComment);
1403 $('.more').live('click', showMoreHelp);
1404 $('.more-help .winter').live('click', hideMoreHelp);
1406 function freezeComment(comment_block) {
1407 var comment_textarea = comment_block.find('textarea');
1408 if (comment_textarea.val().trim() == '') {
1409 discardComment(comment_block);
1412 var line_id = comment_textarea.attr('data-comment-for');
1413 var line = $('#' + line_id)
1414 var frozen_comment = $('<div class="frozenComment"></div>').text(comment_textarea.val());
1415 findCommentBlockFor(line).hide().after(frozen_comment);
1416 return frozen_comment;
1419 function focusOn(node, opt_is_backward) {
1420 if (node.length == 0)
1423 // Give a tabindex so the element can receive actual browser focus.
1424 // -1 makes the element focusable without actually putting in in the tab order.
1425 node.attr('tabindex', -1);
1427 // Remove the tabindex on blur to avoid having the node be mouse-focusable.
1428 node.bind('blur', function() { node.removeAttr('tabindex'); });
1430 var node_top = node.offset().top;
1431 var is_top_offscreen = node_top <= $(document).scrollTop();
1433 var half_way_point = $(document).scrollTop() + window.innerHeight / 2;
1434 var is_top_past_halfway = opt_is_backward ? node_top < half_way_point : node_top > half_way_point;
1436 if (is_top_offscreen || is_top_past_halfway)
1437 $(document).scrollTop(node_top - window.innerHeight / 2);
1440 function visibleNodeFilterFunction(is_backward) {
1441 var y = is_backward ? $('#toolbar')[0].offsetTop - 1 : 0;
1442 var x = window.innerWidth / 2;
1443 var reference_element = document.elementFromPoint(x, y);
1445 if (reference_element.nodeName == 'HTML' || reference_element.nodeName == 'BODY') {
1446 // In case we hit test a margin between file diffs, shift a fudge factor and try again.
1447 // FIXME: Is there a better way to do this?
1448 var file_diffs = $('.FileDiff');
1449 var first_diff = file_diffs.first();
1450 var second_diff = $(file_diffs[1]);
1451 var distance_between_file_diffs = second_diff.position().top - first_diff.position().top - first_diff.height();
1454 y -= distance_between_file_diffs;
1456 y += distance_between_file_diffs;
1458 reference_element = document.elementFromPoint(x, y);
1461 if (reference_element.nodeName == 'HTML' || reference_element.nodeName == 'BODY')
1464 return function(node) {
1465 var compare = reference_element.compareDocumentPosition(node[0]);
1467 return compare & Node.DOCUMENT_POSITION_PRECEDING;
1468 return compare & Node.DOCUMENT_POSITION_FOLLOWING;
1472 function focusNext(filter, direction) {
1473 var focusable_nodes = $('a,.Line,.frozenComment,.previousComment,.DiffBlock,.overallComments').filter(function() {
1474 return !$(this).hasClass('DiffBlock') || $('.add,.remove', this).size();
1477 var is_backward = direction == DIRECTION.BACKWARD;
1478 var index = focusable_nodes.index($(document.activeElement));
1480 var extra_filter = null;
1484 index = focusable_nodes.length;
1485 extra_filter = visibleNodeFilterFunction(is_backward);
1488 var offset = is_backward ? -1 : 1;
1489 var end = is_backward ? -1 : focusable_nodes.size();
1490 for (var i = index + offset; i != end; i = i + offset) {
1491 var node = $(focusable_nodes[i]);
1492 if (filter(node) && (!extra_filter || extra_filter(node))) {
1493 focusOn(node, is_backward);
1500 var DIRECTION = {FORWARD: 1, BACKWARD: 2};
1502 function isComment(node) {
1503 return node.hasClass('frozenComment') || node.hasClass('previousComment') || node.hasClass('overallComments');
1506 function isDiffBlock(node) {
1507 return node.hasClass('DiffBlock');
1510 function isLine(node) {
1511 return node.hasClass('Line');
1514 function commentTextareaForKeyTarget(key_target) {
1515 if (key_target.nodeName == 'TEXTAREA')
1516 return $(key_target);
1518 var comment_textarea = $(document.activeElement).prev().find('textarea');
1519 if (!comment_textarea.size())
1521 return comment_textarea;
1524 function extendCommentContextUp(key_target) {
1525 var comment_textarea = commentTextareaForKeyTarget(key_target);
1526 if (!comment_textarea)
1529 var comment_base_line = comment_textarea.attr('data-comment-for');
1530 var diff_section = diffSectionFor(comment_textarea);
1531 var lines = $('.Line', diff_section);
1532 for (var i = 0; i < lines.length - 1; i++) {
1533 if (hasDataCommentBaseLine(lines[i + 1], comment_base_line)) {
1534 addDataCommentBaseLine(lines[i], comment_base_line);
1540 function shrinkCommentContextDown(key_target) {
1541 var comment_textarea = commentTextareaForKeyTarget(key_target);
1542 if (!comment_textarea)
1545 var comment_base_line = comment_textarea.attr('data-comment-for');
1546 var diff_section = diffSectionFor(comment_textarea);
1547 var lines = contextLinesFor(comment_base_line, diff_section);
1548 if (lines.size() > 1)
1549 removeDataCommentBaseLine(lines[0], comment_base_line);
1552 function handleModifyContextKey(e) {
1553 var handled = false;
1555 if (e.shiftKey && e.ctrlKey) {
1556 switch (e.keyCode) {
1558 extendCommentContextUp(e.target);
1563 shrinkCommentContextDown(e.target);
1575 $('textarea').live('keydown', function(e) {
1576 if (handleModifyContextKey(e))
1579 if (e.keyCode == KEY_CODE.escape)
1580 handleEscapeKeyInTextarea(this);
1583 $('body').live('keydown', function(e) {
1584 // FIXME: There's got to be a better way to avoid seeing these keypress
1586 if (e.target.nodeName == 'TEXTAREA')
1589 // Don't want to override browser shortcuts like ctrl+r.
1590 if (e.metaKey || e.ctrlKey)
1593 if (handleModifyContextKey(e))
1596 var handled = false;
1597 switch (e.keyCode) {
1599 $('.review select').focus();
1604 handled = focusNext(isComment, DIRECTION.FORWARD);
1608 handled = focusNext(isComment, DIRECTION.BACKWARD);
1613 handled = focusNext(isLine, DIRECTION.FORWARD);
1615 handled = focusNext(isDiffBlock, DIRECTION.FORWARD);
1620 handled = focusNext(isLine, DIRECTION.BACKWARD);
1622 handled = focusNext(isDiffBlock, DIRECTION.BACKWARD);
1625 case KEY_CODE.enter:
1626 handled = handleEnterKey();
1629 case KEY_CODE.escape:
1639 function handleEscapeKeyInTextarea(textarea) {
1640 var comment = $(textarea).parents('.comment');
1642 acceptComment(comment);
1645 document.body.focus();
1648 function handleEnterKey() {
1649 if (document.activeElement.nodeName == 'BODY')
1652 var focused = $(document.activeElement);
1654 if (focused.hasClass('frozenComment')) {
1655 unfreezeComment(focused);
1659 if (focused.hasClass('overallComments')) {
1660 openOverallComments();
1661 focused.find('textarea')[0].focus();
1665 if (focused.hasClass('previousComment')) {
1666 addCommentField(focused);
1670 var lines = focused.hasClass('Line') ? focused : $('.Line', focused);
1671 var last = lines.last();
1672 if (last.attr('data-has-comment')) {
1673 unfreezeCommentFor(last);
1677 addCommentForLines(lines);
1681 function contextLinesFor(comment_base_lines, file_diff) {
1682 var base_lines = comment_base_lines.split(' ');
1683 return $('div[data-comment-base-line]', file_diff).filter(function() {
1684 return $(this).attr('data-comment-base-line').split(' ').some(function(item) {
1685 return base_lines.indexOf(item) != -1;
1690 function numberFrom(line_id) {
1691 return Number(line_id.replace('line', ''));
1694 function trimCommentContextToBefore(line, comment_base_line) {
1695 var line_to_trim_to = numberFrom(line.attr('id'));
1696 contextLinesFor(comment_base_line, fileDiffFor(line)).each(function() {
1697 var id = $(this).attr('id');
1698 if (numberFrom(id) > line_to_trim_to)
1701 if (!$('[data-comment-for=' + comment_base_line + ']').length)
1702 removeDataCommentBaseLine(this, comment_base_line);
1706 var drag_select_start_index = -1;
1708 function lineOffsetFrom(line, offset) {
1709 var file_diff = line.parents('.FileDiff');
1710 var all_lines = $('.Line', file_diff);
1711 var index = all_lines.index(line);
1712 return $(all_lines[index + offset]);
1715 function previousLineFor(line) {
1716 return lineOffsetFrom(line, -1);
1719 function nextLineFor(line) {
1720 return lineOffsetFrom(line, 1);
1723 $('.resizeHandle').live('mousedown', function(event) {
1724 file_diff_being_resized = $(this).parent('.FileDiff');
1727 function generateFileDiffResizeStyleElement() {
1728 // FIXME: Once we support calc, we can replace this with something that uses the attribute value.
1730 for (var i = minLeftSideRatio; i <= maxLeftSideRatio; i++) {
1731 // FIXME: Once we support calc, put the resize handle at calc(i% - 5) so it doesn't cover up
1732 // the right-side line numbers.
1733 styleText += '.FileDiff[leftsidewidth="' + i + '"] .resizeHandle {' +
1734 'left: ' + i + '%' +
1736 '.FileDiff[leftsidewidth="' + i + '"] .LineSide:first-child,' +
1737 '.FileDiff[leftsidewidth="' + i + '"].sidebyside .DiffBlockPart.remove {' +
1738 'width:' + i + '%;' +
1740 '.FileDiff[leftsidewidth="' + i + '"] .LineSide:last-child,' +
1741 '.FileDiff[leftsidewidth="' + i + '"].sidebyside .DiffBlockPart.add {' +
1742 'width:' + (100 - i) + '%;' +
1745 var styleElement = document.createElement('style');
1746 styleElement.innerText = styleText;
1747 document.head.appendChild(styleElement);
1750 $(document).bind('mousemove', function(event) {
1751 if (!file_diff_being_resized)
1754 var ratio = event.pageX / window.innerWidth;
1755 var percentage = Math.floor(ratio * 100);
1756 if (percentage < minLeftSideRatio)
1757 percentage = minLeftSideRatio;
1758 if (percentage > maxLeftSideRatio)
1759 percentage = maxLeftSideRatio;
1760 file_diff_being_resized.attr('leftsidewidth', percentage);
1761 event.preventDefault();
1764 $(document).bind('mouseup', function(event) {
1765 file_diff_being_resized = null;
1766 processSelectedLines();
1769 $('.lineNumber').live('click', function(e) {
1770 var line = lineFromLineDescendant($(this));
1771 if (line.hasClass('commentContext'))
1772 trimCommentContextToBefore(previousLineFor(line), line.attr('data-comment-base-line'));
1773 else if (e.shiftKey)
1774 extendCommentContextTo(line);
1775 }).live('mousedown', function(e) {
1776 // preventDefault to avoid selecting text when dragging to select comment context lines.
1777 // FIXME: should we use user-modify CSS instead?
1782 var line = lineFromLineDescendant($(this));
1783 drag_select_start_index = numberFrom(line.attr('id'));
1784 line.addClass('selected');
1787 $('.LineContainer').live('mouseenter', function(e) {
1788 if (drag_select_start_index == -1 || e.shiftKey)
1790 selectToLineContainer(this);
1791 }).live('mouseup', function(e) {
1792 if (drag_select_start_index == -1 || e.shiftKey)
1795 selectToLineContainer(this);
1796 processSelectedLines();
1799 function extendCommentContextTo(line) {
1800 var diff_section = diffSectionFor(line);
1801 var lines = $('.Line', diff_section);
1802 var lines_to_modify = [];
1803 var have_seen_start_line = false;
1804 var data_comment_base_line = null;
1805 lines.each(function() {
1806 if (data_comment_base_line)
1809 have_seen_start_line = have_seen_start_line || this == line[0];
1811 if (have_seen_start_line) {
1812 if ($(this).hasClass('commentContext'))
1813 data_comment_base_line = $(this).attr('data-comment-base-line');
1815 lines_to_modify.push(this);
1819 // There is no comment context to extend.
1820 if (!data_comment_base_line)
1823 $(lines_to_modify).each(function() {
1824 $(this).addClass('commentContext');
1825 $(this).attr('data-comment-base-line', data_comment_base_line);
1829 function selectTo(focus_index) {
1830 var selected = $('.selected').removeClass('selected');
1831 var is_backward = drag_select_start_index > focus_index;
1832 var current_index = is_backward ? focus_index : drag_select_start_index;
1833 var last_index = is_backward ? drag_select_start_index : focus_index;
1834 while (current_index <= last_index) {
1835 $('#line' + current_index).addClass('selected')
1840 function selectToLineContainer(line_container) {
1841 var line = lineFromLineContainer(line_container);
1843 // Ensure that the selected lines are all contained in the same DiffSection.
1844 var selected_lines = $('.selected');
1845 var selected_diff_section = diffSectionFor(selected_lines.first());
1846 var new_diff_section = diffSectionFor(line);
1847 if (new_diff_section[0] != selected_diff_section[0]) {
1848 var lines = $('.Line', selected_diff_section);
1849 if (numberFrom(selected_lines.first().attr('id')) == drag_select_start_index)
1850 line = lines.last();
1852 line = lines.first();
1855 selectTo(numberFrom(line.attr('id')));
1858 function processSelectedLines() {
1859 drag_select_start_index = -1;
1860 addCommentForLines($('.selected'));
1863 function addCommentForLines(lines) {
1867 var already_has_comment = lines.last().hasClass('commentContext');
1869 var comment_base_line;
1870 if (already_has_comment)
1871 comment_base_line = lines.last().attr('data-comment-base-line');
1873 var last = lineFromLineDescendant(lines.last());
1874 addCommentFor($(last));
1875 comment_base_line = last.attr('id');
1878 lines.each(function() {
1879 addDataCommentBaseLine(this, comment_base_line);
1880 $(this).removeClass('selected');
1883 saveDraftComments();
1886 function hasDataCommentBaseLine(line, id) {
1887 var val = $(line).attr('data-comment-base-line');
1891 var parts = val.split(' ');
1892 for (var i = 0; i < parts.length; i++) {
1899 function addDataCommentBaseLine(line, id) {
1900 $(line).addClass('commentContext');
1901 if (hasDataCommentBaseLine(line, id))
1904 var val = $(line).attr('data-comment-base-line');
1905 var parts = val ? val.split(' ') : [];
1907 $(line).attr('data-comment-base-line', parts.join(' '));
1910 function removeDataCommentBaseLine(line, comment_base_lines) {
1911 var val = $(line).attr('data-comment-base-line');
1915 var base_lines = comment_base_lines.split(' ');
1916 var parts = val.split(' ');
1918 for (var i = 0; i < parts.length; i++) {
1919 if (base_lines.indexOf(parts[i]) == -1)
1920 new_parts.push(parts[i]);
1923 var new_comment_base_line = new_parts.join(' ');
1924 if (new_comment_base_line)
1925 $(line).attr('data-comment-base-line', new_comment_base_line);
1927 $(line).removeAttr('data-comment-base-line');
1928 $(line).removeClass('commentContext');
1932 function lineFromLineDescendant(descendant) {
1933 return descendant.hasClass('Line') ? descendant : descendant.parents('.Line');
1936 function lineFromLineContainer(lineContainer) {
1937 var line = $(lineContainer);
1938 if (!line.hasClass('Line'))
1939 line = $('.Line', line);
1943 function contextSnippetFor(line, indent) {
1945 contextLinesFor(line.attr('id'), fileDiffFor(line)).each(function() {
1947 if ($(this).hasClass('add'))
1949 else if ($(this).hasClass('remove'))
1951 snippets.push(indent + action + textContentsFor(this));
1953 return snippets.join('\n');
1956 function fileNameFor(line) {
1957 return fileDiffFor(line).find('h1').text();
1960 function indentFor(depth) {
1961 return (new Array(depth + 1)).join('>') + ' ';
1964 function snippetFor(line, indent) {
1965 var file_name = fileNameFor(line);
1966 var line_number = line.hasClass('remove') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]);
1967 return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent);
1970 function quotePreviousComments(comments) {
1971 var quoted_comments = [];
1972 var depth = comments.size();
1973 comments.each(function() {
1974 var indent = indentFor(depth--);
1975 var text = $(this).children('.content').text();
1976 quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent));
1978 return quoted_comments.join('\n');
1981 $('#comment_form .winter').live('click', hideCommentForm);
1983 function fillInReviewForm() {
1984 var comments_in_context = []
1985 forEachLine(function(line) {
1986 if (line.attr('data-has-comment') != 'true')
1988 var comment = findCommentBlockFor(line).children('textarea').val().trim();
1991 var previous_comments = previousCommentsFor(line);
1992 var snippet = snippetFor(line, indentFor(previous_comments.size() + 1));
1993 var quoted_comments = quotePreviousComments(previous_comments);
1994 var comment_with_context = [];
1995 comment_with_context.push(snippet);
1996 if (quoted_comments != '')
1997 comment_with_context.push(quoted_comments);
1998 comment_with_context.push('\n' + comment);
1999 comments_in_context.push(comment_with_context.join('\n'));
2001 var comment = $('.overallComments textarea').val().trim();
2004 comment += comments_in_context.join('\n\n');
2005 if (comments_in_context.length > 0)
2006 comment = 'View in context: ' + window.location + '\n\n' + comment;
2007 var review_form = $('#reviewform').contents();
2008 review_form.find('#comment').val(comment);
2009 review_form.find('#flags select').each(function() {
2010 var control = findControlForFlag(this);
2011 if (!control.size())
2013 $(this).attr('selectedIndex', control.attr('selectedIndex'));
2017 function showCommentForm() {
2018 $('#comment_form').removeClass('inactive');
2019 $('#reviewform').contents().find('#submitBtn').focus();
2022 function hideCommentForm() {
2023 $('#comment_form').addClass('inactive');
2025 // Make sure the top document has focus so key events don't keep going to the review form.
2026 document.body.tabIndex = -1;
2027 document.body.focus();
2030 $('#preview_comments').live('click', function() {
2035 $('#post_comments').live('click', function() {
2037 $('#reviewform').contents().find('form').submit();
2040 if (CODE_REVIEW_UNITTEST) {
2041 window.DraftCommentSaver = DraftCommentSaver;
2042 window.addPreviousComment = addPreviousComment;
2043 window.tracLinks = tracLinks;
2044 window.crawlDiff = crawlDiff;
2045 window.discardComment = discardComment;
2046 window.addCommentField = addCommentField;
2047 window.acceptComment = acceptComment;
2048 window.appendToolbar = appendToolbar;
2049 window.eraseDraftComments = eraseDraftComments;
2050 window.unfreezeComment = unfreezeComment;
2051 window.g_draftCommentSaver = g_draftCommentSaver;
2052 window.isChangeLog = isChangeLog;
2054 $(document).ready(handleDocumentReady)