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
27 * Create a new function with some of its arguements
29 * Taken from goog.partial in the Closure library.
30 * @param {Function} fn A function to partially apply.
31 * @param {...*} var_args Additional arguments that are partially
33 * @return {!Function} A partially-applied form of the function.
35 function partial(fn, var_args) {
36 var args = Array.prototype.slice.call(arguments, 1);
38 // Prepend the bound arguments to the current arguments.
39 var newArgs = Array.prototype.slice.call(arguments);
40 newArgs.unshift.apply(newArgs, args);
41 return fn.apply(this, newArgs);
45 function determineAttachmentID() {
47 return /id=(\d+)/.exec(window.location.search)[1]
53 // Attempt to activate only in the "Review Patch" context.
54 if (window.top != window)
56 if (!window.location.search.match(/action=review/))
58 var attachment_id = determineAttachmentID();
64 var original_file_contents = {};
65 var patched_file_contents = {};
66 var WEBKIT_BASE_DIR = "http://svn.webkit.org/repository/webkit/trunk/";
68 function idForLine(number) {
69 return 'line' + number;
72 function nextLineID() {
73 return idForLine(next_line_id++);
76 function forEachLine(callback) {
77 for (var i = 0; i < next_line_id; ++i) {
78 callback($('#' + idForLine(i)));
83 this.id = nextLineID();
87 $(this).hover(function() {
88 $(this).addClass('hot');
91 $(this).removeClass('hot');
95 function diffSectionFrom(line) {
96 return line.parents('.FileDiff');
99 function previousCommentsFor(line) {
100 // Scope to the diffSection as a performance improvement.
101 return $('div[data-comment-for~="' + line[0].id + '"].previousComment', diffSectionFrom(line));
104 function findCommentPositionFor(line) {
105 var previous_comments = previousCommentsFor(line);
106 var num_previous_comments = previous_comments.size();
107 if (num_previous_comments)
108 return $(previous_comments[num_previous_comments - 1])
112 function findCommentBlockFor(line) {
113 var comment_block = findCommentPositionFor(line).next();
114 if (!comment_block.hasClass('comment'))
116 return comment_block;
119 function insertCommentFor(line, block) {
120 findCommentPositionFor(line).after(block);
123 function addCommentFor(line) {
124 if (line.attr('data-has-comment')) {
125 // FIXME: This query is overly complex because we place comment blocks
126 // after Lines. Instead, comment blocks should be children of Lines.
127 findCommentPositionFor(line).next().next().filter('.frozenComment').each(unfreezeComment);
130 line.attr('data-has-comment', 'true');
131 line.addClass('commentContext');
133 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>');
134 insertCommentFor(line, comment_block);
135 comment_block.hide().slideDown('fast', function() {
136 $(this).children('textarea').focus();
140 function addCommentField() {
141 var id = $(this).attr('data-comment-for');
144 addCommentFor($('#' + id));
147 function addPreviousComment(line, author, comment_text) {
148 var line_id = line.attr('id');
149 var comment_block = $('<div data-comment-for="' + line_id + '" class="previousComment"></div>'); var author_block = $('<div class="author"></div>').text(author + ':');
150 var text_block = $('<div class="content"></div>').text(comment_text);
151 comment_block.append(author_block).append(text_block).each(hoverify).click(addCommentField);
152 addDataCommentBaseLine(line, line_id);
153 insertCommentFor(line, comment_block);
156 function displayPreviousComments(comments) {
157 for (var i = 0; i < comments.length; ++i) {
158 var author = comments[i].author;
159 var file_name = comments[i].file_name;
160 var line_number = comments[i].line_number;
161 var comment_text = comments[i].comment_text;
163 var file = files[file_name];
165 var query = '.Line .to';
166 if (line_number[0] == '-') {
167 // The line_number represent a removal. We need to adjust the query to
168 // look at the "from" lines.
169 query = '.Line .from';
170 // Trim off the '-' control character.
171 line_number = line_number.substr(1);
174 $(file).find(query).each(function() {
175 if ($(this).text() != line_number)
177 var line = $(this).parent();
178 addPreviousComment(line, author, comment_text);
181 if (comments.length == 0)
183 descriptor = comments.length + ' comment';
184 if (comments.length > 1)
186 $('#message .commentStatus').text('This patch has ' + descriptor + '. Scroll through them with the "n" and "p" keys.');
189 function scanForComments(author, text) {
191 var lines = text.split('\n');
192 for (var i = 0; i < lines.length; ++i) {
193 var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/);
196 var quote_markers = parts[1];
197 var file_name = parts[2];
198 // FIXME: Store multiple lines for multiline comments and correctly import them here.
199 var line_number = parts[3];
200 if (!file_name in files)
202 while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>')
204 var comment_lines = [];
205 while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) {
206 comment_lines.push(lines[i]);
209 --i; // Decrement i because the for loop will increment it again in a second.
210 var comment_text = comment_lines.join('\n').trim();
213 'file_name': file_name,
214 'line_number': line_number,
215 'comment_text': comment_text
221 function isReviewFlag(select) {
222 return $(select).attr('title') == 'Request for patch review.';
225 function isCommitQueueFlag(select) {
226 return $(select).attr('title').match(/commit-queue/);
229 function findControlForFlag(select) {
230 if (isReviewFlag(select))
231 return $('#toolbar .review select');
232 else if (isCommitQueueFlag(select))
233 return $('#toolbar .commitQueue select');
237 function addFlagsForAttachment(details) {
238 var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>";
239 $('#flagContainer').append(
240 $('<span class="review"> r: ' + flag_control + '</span>')).append(
241 $('<span class="commitQueue"> cq: ' + flag_control + '</span>'));
243 details.find('#flags select').each(function() {
244 var requestee = $(this).parent().siblings('td:first-child').text().trim();
245 if (requestee.length) {
246 // Remove trailing ':'.
247 requestee = requestee.substr(0, requestee.length - 1);
248 requestee = ' (' + requestee + ')';
250 var control = findControlForFlag(this)
251 control.attr('selectedIndex', $(this).attr('selectedIndex'));
252 control.parent().prepend(requestee);
256 window.addEventListener('message', function(e) {
257 if (e.origin != 'https://webkit-commit-queue.appspot.com')
261 $('.statusBubble')[0].style.height = e.data.height;
262 $('.statusBubble')[0].style.width = e.data.width;
266 function handleStatusBubbleLoad(e) {
267 e.target.contentWindow.postMessage('containerMetrics', 'https://webkit-commit-queue.appspot.com');
270 function fetchHistory() {
271 $.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) {
272 var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1];
273 $.get('show_bug.cgi?id=' + bug_id, function(data) {
275 $(data).find('.bz_comment').each(function() {
276 var author = $(this).find('.email').text();
277 var text = $(this).find('.bz_comment_text').text();
278 var comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
279 if (text.match(comment_marker))
280 $.merge(comments, scanForComments(author, text));
282 displayPreviousComments(comments);
285 var details = $(data);
286 addFlagsForAttachment(details);
288 var statusBubble = document.createElement('iframe');
289 statusBubble.className = 'statusBubble';
290 statusBubble.src = 'https://webkit-commit-queue.appspot.com/status-bubble/' + attachment_id;
291 statusBubble.scrolling = 'no';
292 // Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM.
293 statusBubble.onload = handleStatusBubbleLoad;
294 $('#statusBubbleContainer').append(statusBubble);
296 $('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
300 function crawlDiff() {
301 $('.Line').each(idify).each(hoverify);
302 $('.FileDiff').each(function() {
303 var file_name = $(this).children('h1').text();
304 files[file_name] = this;
305 addExpandLinks(file_name);
309 function addExpandLinks(file_name) {
310 if (file_name.indexOf('ChangeLog') != -1)
313 var file_diff = files[file_name];
315 // Don't show the links to expand upwards/downwards if the patch starts/ends without context
316 // lines, i.e. starts/ends with add/remove lines.
317 var first_line = file_diff.querySelector('.Line');
319 // If there is no element with a "Line" class, then this is an image diff.
323 $('.context', file_diff).detach();
325 var expand_bar_index = 0;
326 if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove'))
327 $('h1', file_diff).after(expandBarHtml(file_name, BELOW))
329 $('br').replaceWith(expandBarHtml(file_name));
331 var last_line = file_diff.querySelector('.Line:last-of-type');
332 // Some patches for new files somehow end up with an empty context line at the end
333 // with a from line number of 0. Don't show expand links in that case either.
334 if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0)
335 $(file_diff).append(expandBarHtml(file_name, ABOVE));
338 function expandBarHtml(file_name, opt_direction) {
339 var html = '<div class="ExpandBar">' +
340 '<pre class="ExpandArea Expand' + ABOVE + '"></pre>' +
341 '<div class="ExpandLinkContainer"><span class="ExpandText">expand: </span>';
343 // FIXME: If there are <100 line to expand, don't show the expand-100 link.
344 // If there are <20 lines to expand, don't show the expand-20 link.
345 if (!opt_direction || opt_direction == ABOVE) {
346 html += expandLinkHtml(ABOVE, 100) +
347 expandLinkHtml(ABOVE, 20);
350 html += expandLinkHtml(ALL);
352 if (!opt_direction || opt_direction == BELOW) {
353 html += expandLinkHtml(BELOW, 20) +
354 expandLinkHtml(BELOW, 100);
357 html += '</div><pre class="ExpandArea Expand' + BELOW + '"></pre></div>';
361 function expandLinkHtml(direction, amount) {
362 return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" +
363 (amount ? amount + " " : "") + direction + "</a>";
366 function handleExpandLinkClick(target) {
367 var expand_bar = $(target).parents('.ExpandBar');
368 var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent;
369 var expand_function = partial(expand, expand_bar[0], file_name, target.getAttribute('data-direction'), Number(target.getAttribute('data-amount')));
370 if (file_name in original_file_contents)
373 getWebKitSourceFile(file_name, expand_function, expand_bar);
376 $(window).bind('click', function (e) {
377 var target = e.target;
379 switch(target.className) {
381 handleExpandLinkClick(target);
386 function getWebKitSourceFile(file_name, onLoad, expand_bar) {
387 function handleLoad(contents) {
388 original_file_contents[file_name] = contents.split('\n');
389 patched_file_contents[file_name] = applyDiff(original_file_contents[file_name], file_name);
394 url: WEBKIT_BASE_DIR + file_name,
395 context: document.body,
396 complete: function(xhr, data) {
398 handleLoadError(expand_bar);
400 handleLoad(xhr.responseText);
405 function replaceExpandLinkContainers(expand_bar, text) {
406 $('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>');
409 function handleLoadError(expand_bar) {
410 // FIXME: In this case, try fetching the source file at the revision the patch was created at,
411 // in case the file has bee deleted.
412 // Might need to modify webkit-patch to include that data in the diff.
413 replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?");
420 function expand(expand_bar, file_name, direction, amount) {
421 if (file_name in original_file_contents && !patched_file_contents[file_name]) {
422 // FIXME: In this case, try fetching the source file at the revision the patch was created at.
423 // Might need to modify webkit-patch to include that data in the diff.
424 replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree.");
428 var above_expansion = expand_bar.querySelector('.Expand' + ABOVE)
429 var below_expansion = expand_bar.querySelector('.Expand' + BELOW)
431 var above_last_line = above_expansion.querySelector('.ExpansionLine:last-of-type');
432 if (!above_last_line) {
433 var diff_section = expand_bar.previousElementSibling;
434 above_last_line = diff_section.querySelector('.Line:last-of-type');
437 var above_last_line_num, above_last_from_line_num;
438 if (above_last_line) {
439 above_last_line_num = toLineNumber(above_last_line);
440 above_last_from_line_num = fromLineNumber(above_last_line);
442 above_last_from_line_num = above_last_line_num = 0;
444 var below_first_line = below_expansion.querySelector('.ExpansionLine');
445 if (!below_first_line) {
446 var diff_section = expand_bar.nextElementSibling;
448 below_first_line = diff_section.querySelector('.Line');
451 var below_first_line_num, below_first_from_line_num;
452 if (below_first_line) {
453 below_first_line_num = toLineNumber(below_first_line) - 1;
454 below_first_from_line_num = fromLineNumber(below_first_line) - 1;
456 below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1;
458 var start_line_num, start_from_line_num;
461 if (direction == ABOVE) {
462 start_from_line_num = above_last_from_line_num;
463 start_line_num = above_last_line_num;
464 end_line_num = Math.min(start_line_num + amount, below_first_line_num);
465 } else if (direction == BELOW) {
466 end_line_num = below_first_line_num;
467 start_line_num = Math.max(end_line_num - amount, above_last_line_num)
468 start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num)
469 } else { // direction == ALL
470 start_line_num = above_last_line_num;
471 start_from_line_num = above_last_from_line_num;
472 end_line_num = below_first_line_num;
476 // Filling in all the remaining lines. Overwrite the expand links.
477 if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) {
478 expansion_area = expand_bar.querySelector('.ExpandLinkContainer');
479 expansion_area.innerHTML = '';
481 expansion_area = direction == ABOVE ? above_expansion : below_expansion;
484 insertLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num);
487 function insertLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) {
488 var fragment = document.createDocumentFragment();
489 for (var i = 0; i < end_line_num - start_line_num; i++) {
490 var line = document.createElement('div');
491 line.className = 'ExpansionLine';
492 // FIXME: from line numbers are wrong
493 line.innerHTML = '<span class="from expansionlineNumber">' + (start_from_line_num + i + 1) +
494 '</span><span class="to expansionlineNumber">' + (start_line_num + i + 1) +
495 '</span> <span class="text"></span>';
496 line.querySelector('.text').textContent = patched_file_contents[file_name][start_line_num + i];
497 fragment.appendChild(line);
500 if (direction == BELOW)
501 expansion_area.insertBefore(fragment, expansion_area.firstChild);
503 expansion_area.appendChild(fragment);
506 function hunkStartingLine(patched_file, context, prev_line, hunk_num) {
508 var current_line = -1;
509 var last_context_line = context[context.length - 1];
510 if (patched_file[prev_line] == last_context_line)
511 current_line = prev_line + 1;
513 for (var i = prev_line - PATCH_FUZZ; i < prev_line + PATCH_FUZZ; i++) {
514 if (patched_file[i] == last_context_line)
515 current_line = i + 1;
518 if (current_line == -1) {
519 console.log('Hunk #' + hunk_num + ' FAILED.');
524 // For paranoia sake, confirm the rest of the context matches;
525 for (var i = 0; i < context.length - 1; i++) {
526 if (patched_file[current_line - context.length + i] != context[i]) {
527 console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.');
535 function fromLineNumber(line) {
536 return Number(line.querySelector('.from').textContent);
539 function toLineNumber(line) {
540 return Number(line.querySelector('.to').textContent);
543 function textContentsFor(line) {
544 return $('.text', line).text();
547 function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) {
548 if (context.length) {
549 var prev_line_num = fromLineNumber(prev_line) - 1;
550 return hunkStartingLine(patched_file, context, prev_line_num, hunk_num);
553 if (toLineNumber(line) == 1 || fromLineNumber(line) == 1)
556 console.log('Failed to apply patch. Adds or removes lines before any context lines.');
560 function applyDiff(original_file, file_name) {
561 var diff_sections = files[file_name].getElementsByClassName('DiffSection');
562 var patched_file = original_file.concat([]);
564 // Apply diffs in reverse order to avoid needing to keep track of changing line numbers.
565 for (var i = diff_sections.length - 1; i >= 0; i--) {
566 var section = diff_sections[i];
567 var lines = section.getElementsByClassName('Line');
568 var current_line = -1;
570 var hunk_num = i + 1;
572 for (var j = 0, lines_len = lines.length; j < lines_len; j++) {
574 var line_contents = textContentsFor(line);
575 if ($(line).hasClass('add')) {
576 if (current_line == -1) {
577 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
578 if (current_line == -1)
582 patched_file.splice(current_line, 0, line_contents);
584 } else if ($(line).hasClass('remove')) {
585 if (current_line == -1) {
586 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
587 if (current_line == -1)
591 if (patched_file[current_line] != line_contents) {
592 console.log('Hunk #' + hunk_num + ' FAILED.');
596 patched_file.splice(current_line, 1);
597 } else if (current_line == -1) {
598 context.push(line_contents);
599 } else if (line_contents != patched_file[current_line]) {
600 console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match');
611 function openOverallComments(e) {
612 $('.overallComments textarea').addClass('open');
613 $('#statusBubbleContainer').addClass('wrap');
616 function onBodyResize() {
617 updateToolbarAnchorState();
620 function updateToolbarAnchorState() {
621 var has_scrollbar = window.innerWidth > document.documentElement.offsetWidth;
622 $('#toolbar').toggleClass('anchored', has_scrollbar);
625 $(document).ready(function() {
628 $(document.body).prepend('<div id="message"><div class="help">Select line numbers to add a comment.</div><div class="commentStatus"></div></div>');
629 $(document.body).append('<div id="toolbar">' +
630 '<div class="overallComments">' +
631 '<textarea placeholder="Overall comments"></textarea>' +
634 '<span id="statusBubbleContainer"></span>' +
635 '<span class="actions">' +
636 '<span class="links"><span class="bugLink"></span></span>' +
637 '<span id="flagContainer"></span>' +
638 '<button id="preview_comments">Preview</button>' +
639 '<button id="post_comments">Publish</button> ' +
644 $('.overallComments textarea').bind('click', openOverallComments);
646 $(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>');
648 // Create a dummy iframe and monitor resizes in it's contentWindow to detect when the top document's body changes size.
649 // FIXME: Should we setTimeout throttle these?
650 var resize_iframe = $('<iframe class="pseudo_resize_event_iframe"></iframe>');
651 $(document.body).append(resize_iframe);
652 $(resize_iframe[0].contentWindow).bind('resize', onBodyResize);
654 updateToolbarAnchorState();
657 function discardComment() {
658 var line_id = $(this).parentsUntil('.comment').parent().find('textarea').attr('data-comment-for');
659 var line = $('#' + line_id)
660 findCommentBlockFor(line).slideUp('fast', function() {
662 line.removeAttr('data-has-comment');
663 trimCommentContextToBefore(line);
667 function unfreezeComment() {
668 $(this).prev().show();
672 $('.comment .discard').live('click', discardComment);
674 $('.comment .ok').live('click', function() {
675 var comment_textarea = $(this).parentsUntil('.comment').parent().find('textarea');
676 if (comment_textarea.val().trim() == '') {
677 discardComment.call(this);
680 var line_id = comment_textarea.attr('data-comment-for');
681 var line = $('#' + line_id)
682 findCommentBlockFor(line).hide().after($('<div class="frozenComment"></div>').text(comment_textarea.val()));
685 $('.frozenComment').live('click', unfreezeComment);
687 function focusOn(comment) {
688 $('.focused').removeClass('focused');
689 if (comment.length == 0)
691 $(document).scrollTop(comment.addClass('focused').position().top - window.innerHeight/2);
694 function focusNextComment() {
695 var comments = $('.previousComment');
696 if (comments.length == 0)
698 var index = comments.index($('.focused'));
699 // Notice that -1 gets mapped to 0.
700 focusOn($(comments.get(index + 1)));
703 function focusPreviousComment() {
704 var comments = $('.previousComment');
705 if (comments.length == 0)
707 var index = comments.index($('.focused'));
709 index = comments.length;
714 focusOn($(comments.get(index - 1)));
717 var kCharCodeForN = 'n'.charCodeAt(0);
718 var kCharCodeForP = 'p'.charCodeAt(0);
720 $('body').live('keypress', function() {
721 // FIXME: There's got to be a better way to avoid seeing these keypress
723 if (event.target.nodeName == 'TEXTAREA')
725 if (event.charCode == kCharCodeForN)
727 else if (event.charCode == kCharCodeForP)
728 focusPreviousComment();
731 function contextLinesFor(line_id) {
732 return $('div[data-comment-base-line~="' + line_id + '"]');
735 function numberFrom(line_id) {
736 return Number(line_id.replace('line', ''));
739 function trimCommentContextToBefore(line) {
740 var base_line_id = line.attr('data-comment-base-line');
741 var line_to_trim_to = numberFrom(line.attr('id'));
742 contextLinesFor(base_line_id).each(function() {
743 var id = $(this).attr('id');
744 if (numberFrom(id) > line_to_trim_to)
747 removeDataCommentBaseLine(this, base_line_id);
748 if (!$(this).attr('data-comment-base-line'))
749 $(this).removeClass('commentContext');
753 var in_drag_select = false;
755 function stopDragSelect() {
756 $('.selected').removeClass('selected');
757 in_drag_select = false;
760 $('.lineNumber').live('click', function() {
761 var line = $(this).parent();
762 if (line.hasClass('commentContext'))
763 trimCommentContextToBefore(line.prev());
764 }).live('mousedown', function() {
765 in_drag_select = true;
766 $(lineFromLineDescendant(this)).addClass('selected');
767 event.preventDefault();
770 $('.Line').live('mouseenter', function() {
774 var line = lineFromLineContainer(this);
775 line.addClass('selected');
776 }).live('mouseup', function() {
779 var selected = $('.selected');
780 var should_add_comment = !selected.last().next().hasClass('commentContext');
781 selected.addClass('commentContext');
784 if (should_add_comment) {
785 var last = lineFromLineDescendant(selected.last()[0]);
786 addCommentFor($(last));
789 id = selected.last().next()[0].getAttribute('data-comment-base-line');
792 selected.each(function() {
793 addDataCommentBaseLine(this, id);
797 function addDataCommentBaseLine(line, id) {
798 var val = $(line).attr('data-comment-base-line');
800 var parts = val ? val.split(' ') : [];
801 for (var i = 0; i < parts.length; i++) {
807 $(line).attr('data-comment-base-line', parts.join(' '));
810 function removeDataCommentBaseLine(line, id) {
811 var val = $(line).attr('data-comment-base-line');
815 var parts = val.split(' ');
817 for (var i = 0; i < parts.length; i++) {
819 newVal.push(parts[i]);
822 $(line).attr('data-comment-base-line', newVal.join(' '));
825 function lineFromLineDescendant(descendant) {
826 while (descendant && !$(descendant).hasClass('Line')) {
827 descendant = descendant.parentNode;
832 function lineFromLineContainer(lineContainer) {
833 var line = $(lineContainer);
834 if (!line.hasClass('Line'))
835 line = $('.Line', line);
839 $('.DiffSection').live('mouseleave', stopDragSelect).live('mouseup', stopDragSelect);
841 function contextSnippetFor(line, indent) {
843 contextLinesFor(line.attr('id')).each(function() {
845 if ($(this).hasClass('add'))
847 else if ($(this).hasClass('remove'))
849 snippets.push(indent + action + textContentsFor(this));
851 return snippets.join('\n');
854 function fileNameFor(line) {
855 return line.parentsUntil('.FileDiff').parent().find('h1').text();
858 function indentFor(depth) {
859 return (new Array(depth + 1)).join('>') + ' ';
862 function snippetFor(line, indent) {
863 var file_name = fileNameFor(line);
864 var line_number = line.hasClass('remove') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]);
865 return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent);
868 function quotePreviousComments(comments) {
869 var quoted_comments = [];
870 var depth = comments.size();
871 comments.each(function() {
872 var indent = indentFor(depth--);
873 var text = $(this).children('.content').text();
874 quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent));
876 return quoted_comments.join('\n');
879 $('#comment_form .winter').live('click', function() {
880 $('#comment_form').addClass('inactive');
883 function fillInReviewForm() {
884 var comments_in_context = []
885 forEachLine(function(line) {
886 if (line.attr('data-has-comment') != 'true')
888 var comment = findCommentBlockFor(line).children('textarea').val().trim();
891 var previous_comments = previousCommentsFor(line);
892 var snippet = snippetFor(line, indentFor(previous_comments.size() + 1));
893 var quoted_comments = quotePreviousComments(previous_comments);
894 var comment_with_context = [];
895 comment_with_context.push(snippet);
896 if (quoted_comments != '')
897 comment_with_context.push(quoted_comments);
898 comment_with_context.push('\n' + comment);
899 comments_in_context.push(comment_with_context.join('\n'));
901 var comment = $('.overallComments textarea').val().trim();
904 comment += comments_in_context.join('\n\n');
905 if (comments_in_context.length > 0)
906 comment = 'View in context: ' + window.location + '\n\n' + comment;
907 var review_form = $('#reviewform').contents();
908 review_form.find('#comment').val(comment);
909 review_form.find('#flags select').each(function() {
910 var control = findControlForFlag(this);
913 $(this).attr('selectedIndex', control.attr('selectedIndex'));
917 $('#preview_comments').live('click', function() {
919 $('#comment_form').removeClass('inactive');
922 $('#post_comments').live('click', function() {
924 $('#reviewform').contents().find('form').submit();