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/";
67 var SIDE_BY_SIDE_DIFFS_KEY = 'sidebysidediffs';
69 function idForLine(number) {
70 return 'line' + number;
73 function nextLineID() {
74 return idForLine(next_line_id++);
77 function forEachLine(callback) {
78 for (var i = 0; i < next_line_id; ++i) {
79 callback($('#' + idForLine(i)));
84 this.id = nextLineID();
87 function containerify() {
88 $(this).addClass('LineContainer');
92 $(this).hover(function() {
93 $(this).addClass('hot');
96 $(this).removeClass('hot');
100 function diffSectionFrom(line) {
101 return line.parents('.FileDiff');
104 function activeCommentFor(line) {
105 // Scope to the diffSection as a performance improvement.
106 return $('textarea[data-comment-for~="' + line[0].id + '"]', diffSectionFrom(line));
109 function previousCommentsFor(line) {
110 // Scope to the diffSection as a performance improvement.
111 return $('div[data-comment-for~="' + line[0].id + '"].previousComment', diffSectionFrom(line));
114 function findCommentPositionFor(line) {
115 var previous_comments = previousCommentsFor(line);
116 var num_previous_comments = previous_comments.size();
117 if (num_previous_comments)
118 return $(previous_comments[num_previous_comments - 1])
122 function findCommentBlockFor(line) {
123 var comment_block = findCommentPositionFor(line).next();
124 if (!comment_block.hasClass('comment'))
126 return comment_block;
129 function insertCommentFor(line, block) {
130 findCommentPositionFor(line).after(block);
133 function addCommentFor(line) {
134 if (line.attr('data-has-comment')) {
135 // FIXME: This query is overly complex because we place comment blocks
136 // after Lines. Instead, comment blocks should be children of Lines.
137 findCommentPositionFor(line).next().next().filter('.frozenComment').each(unfreezeComment);
140 line.attr('data-has-comment', 'true');
141 line.addClass('commentContext');
143 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>');
144 insertCommentFor(line, comment_block);
145 comment_block.hide().slideDown('fast', function() {
146 $(this).children('textarea').focus();
150 function addCommentField() {
151 var id = $(this).attr('data-comment-for');
154 addCommentFor($('#' + id));
157 function addPreviousComment(line, author, comment_text) {
158 var line_id = line.attr('id');
159 var comment_block = $('<div data-comment-for="' + line_id + '" class="previousComment"></div>');
160 var author_block = $('<div class="author"></div>').text(author + ':');
161 var text_block = $('<div class="content"></div>').text(comment_text);
162 comment_block.append(author_block).append(text_block).each(hoverify).click(addCommentField);
163 addDataCommentBaseLine(line, line_id);
164 insertCommentFor(line, comment_block);
167 function displayPreviousComments(comments) {
168 for (var i = 0; i < comments.length; ++i) {
169 var author = comments[i].author;
170 var file_name = comments[i].file_name;
171 var line_number = comments[i].line_number;
172 var comment_text = comments[i].comment_text;
174 var file = files[file_name];
176 var query = '.Line .to';
177 if (line_number[0] == '-') {
178 // The line_number represent a removal. We need to adjust the query to
179 // look at the "from" lines.
180 query = '.Line .from';
181 // Trim off the '-' control character.
182 line_number = line_number.substr(1);
185 $(file).find(query).each(function() {
186 if ($(this).text() != line_number)
188 var line = $(this).parent();
189 addPreviousComment(line, author, comment_text);
192 if (comments.length == 0)
194 descriptor = comments.length + ' comment';
195 if (comments.length > 1)
197 $('#message .commentStatus').text('This patch has ' + descriptor + '. Scroll through them with the "n" and "p" keys.');
200 function scanForComments(author, text) {
202 var lines = text.split('\n');
203 for (var i = 0; i < lines.length; ++i) {
204 var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/);
207 var quote_markers = parts[1];
208 var file_name = parts[2];
209 // FIXME: Store multiple lines for multiline comments and correctly import them here.
210 var line_number = parts[3];
211 if (!file_name in files)
213 while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>')
215 var comment_lines = [];
216 while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) {
217 comment_lines.push(lines[i]);
220 --i; // Decrement i because the for loop will increment it again in a second.
221 var comment_text = comment_lines.join('\n').trim();
224 'file_name': file_name,
225 'line_number': line_number,
226 'comment_text': comment_text
232 function isReviewFlag(select) {
233 return $(select).attr('title') == 'Request for patch review.';
236 function isCommitQueueFlag(select) {
237 return $(select).attr('title').match(/commit-queue/);
240 function findControlForFlag(select) {
241 if (isReviewFlag(select))
242 return $('#toolbar .review select');
243 else if (isCommitQueueFlag(select))
244 return $('#toolbar .commitQueue select');
248 function addFlagsForAttachment(details) {
249 var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>";
250 $('#flagContainer').append(
251 $('<span class="review"> r: ' + flag_control + '</span>')).append(
252 $('<span class="commitQueue"> cq: ' + flag_control + '</span>'));
254 details.find('#flags select').each(function() {
255 var requestee = $(this).parent().siblings('td:first-child').text().trim();
256 if (requestee.length) {
257 // Remove trailing ':'.
258 requestee = requestee.substr(0, requestee.length - 1);
259 requestee = ' (' + requestee + ')';
261 var control = findControlForFlag(this)
262 control.attr('selectedIndex', $(this).attr('selectedIndex'));
263 control.parent().prepend(requestee);
267 window.addEventListener('message', function(e) {
268 if (e.origin != 'https://webkit-commit-queue.appspot.com')
272 $('.statusBubble')[0].style.height = e.data.height;
273 $('.statusBubble')[0].style.width = e.data.width;
277 function handleStatusBubbleLoad(e) {
278 e.target.contentWindow.postMessage('containerMetrics', 'https://webkit-commit-queue.appspot.com');
281 function fetchHistory() {
282 $.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) {
283 var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1];
284 $.get('show_bug.cgi?id=' + bug_id, function(data) {
286 $(data).find('.bz_comment').each(function() {
287 var author = $(this).find('.email').text();
288 var text = $(this).find('.bz_comment_text').text();
289 var comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
290 if (text.match(comment_marker))
291 $.merge(comments, scanForComments(author, text));
293 displayPreviousComments(comments);
296 var details = $(data);
297 addFlagsForAttachment(details);
299 var statusBubble = document.createElement('iframe');
300 statusBubble.className = 'statusBubble';
301 statusBubble.src = 'https://webkit-commit-queue.appspot.com/status-bubble/' + attachment_id;
302 statusBubble.scrolling = 'no';
303 // Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM.
304 statusBubble.onload = handleStatusBubbleLoad;
305 $('#statusBubbleContainer').append(statusBubble);
307 $('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
311 function crawlDiff() {
312 $('.Line').each(idify).each(hoverify).each(containerify);
313 $('.FileDiff').each(function() {
314 var file_name = $(this).children('h1').text();
315 files[file_name] = this;
316 addExpandLinks(file_name);
320 function addExpandLinks(file_name) {
321 if (file_name.indexOf('ChangeLog') != -1)
324 var file_diff = files[file_name];
326 // Don't show the links to expand upwards/downwards if the patch starts/ends without context
327 // lines, i.e. starts/ends with add/remove lines.
328 var first_line = file_diff.querySelector('.LineContainer');
330 // If there is no element with a "Line" class, then this is an image diff.
334 $('.context', file_diff).detach();
336 var expand_bar_index = 0;
337 if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove'))
338 $('h1', file_diff).after(expandBarHtml(file_name, BELOW))
340 $('br').replaceWith(expandBarHtml(file_name));
342 var last_line = file_diff.querySelector('.LineContainer:last-of-type');
343 // Some patches for new files somehow end up with an empty context line at the end
344 // with a from line number of 0. Don't show expand links in that case either.
345 if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0)
346 $(file_diff).append(expandBarHtml(file_name, ABOVE));
349 function expandBarHtml(file_name, opt_direction) {
350 var html = '<div class="ExpandBar">' +
351 '<pre class="ExpandArea Expand' + ABOVE + '"></pre>' +
352 '<div class="ExpandLinkContainer LinkContainer"><span class="ExpandText">expand: </span>';
354 // FIXME: If there are <100 line to expand, don't show the expand-100 link.
355 // If there are <20 lines to expand, don't show the expand-20 link.
356 if (!opt_direction || opt_direction == ABOVE) {
357 html += expandLinkHtml(ABOVE, 100) +
358 expandLinkHtml(ABOVE, 20);
361 html += expandLinkHtml(ALL);
363 if (!opt_direction || opt_direction == BELOW) {
364 html += expandLinkHtml(BELOW, 20) +
365 expandLinkHtml(BELOW, 100);
368 html += '</div><pre class="ExpandArea Expand' + BELOW + '"></pre></div>';
372 function expandLinkHtml(direction, amount) {
373 return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" +
374 (amount ? amount + " " : "") + direction + "</a>";
377 function handleExpandLinkClick() {
378 var expand_bar = $(this).parents('.ExpandBar');
379 var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent;
380 var expand_function = partial(expand, expand_bar[0], file_name, this.getAttribute('data-direction'), Number(this.getAttribute('data-amount')));
381 if (file_name in original_file_contents)
384 getWebKitSourceFile(file_name, expand_function, expand_bar);
387 function handleSideBySideLinkClick() {
388 $('.FileDiff').each(function() {
389 convertFileDiff('sidebyside', this);
393 function handleUnifyLinkClick() {
394 $('.FileDiff').each(function() {
395 convertFileDiff('unified', this);
399 function getWebKitSourceFile(file_name, onLoad, expand_bar) {
400 function handleLoad(contents) {
401 original_file_contents[file_name] = contents.split('\n');
402 patched_file_contents[file_name] = applyDiff(original_file_contents[file_name], file_name);
407 url: WEBKIT_BASE_DIR + file_name,
408 context: document.body,
409 complete: function(xhr, data) {
411 handleLoadError(expand_bar);
413 handleLoad(xhr.responseText);
418 function replaceExpandLinkContainers(expand_bar, text) {
419 $('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>');
422 function handleLoadError(expand_bar) {
423 // FIXME: In this case, try fetching the source file at the revision the patch was created at,
424 // in case the file has bee deleted.
425 // Might need to modify webkit-patch to include that data in the diff.
426 replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?");
433 function expand(expand_bar, file_name, direction, amount) {
434 if (file_name in original_file_contents && !patched_file_contents[file_name]) {
435 // FIXME: In this case, try fetching the source file at the revision the patch was created at.
436 // Might need to modify webkit-patch to include that data in the diff.
437 replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree.");
441 var above_expansion = expand_bar.querySelector('.Expand' + ABOVE)
442 var below_expansion = expand_bar.querySelector('.Expand' + BELOW)
444 var above_last_line = above_expansion.querySelector('.ExpansionLine:last-of-type');
445 if (!above_last_line) {
446 var diff_section = expand_bar.previousElementSibling;
447 above_last_line = diff_section.querySelector('.LineContainer:last-of-type');
450 var above_last_line_num, above_last_from_line_num;
451 if (above_last_line) {
452 above_last_line_num = toLineNumber(above_last_line);
453 above_last_from_line_num = fromLineNumber(above_last_line);
455 above_last_from_line_num = above_last_line_num = 0;
457 var below_first_line = below_expansion.querySelector('.ExpansionLine');
458 if (!below_first_line) {
459 var diff_section = expand_bar.nextElementSibling;
461 below_first_line = diff_section.querySelector('.LineContainer');
464 var below_first_line_num, below_first_from_line_num;
465 if (below_first_line) {
466 below_first_line_num = toLineNumber(below_first_line) - 1;
467 below_first_from_line_num = fromLineNumber(below_first_line) - 1;
469 below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1;
471 var start_line_num, start_from_line_num;
474 if (direction == ABOVE) {
475 start_from_line_num = above_last_from_line_num;
476 start_line_num = above_last_line_num;
477 end_line_num = Math.min(start_line_num + amount, below_first_line_num);
478 } else if (direction == BELOW) {
479 end_line_num = below_first_line_num;
480 start_line_num = Math.max(end_line_num - amount, above_last_line_num)
481 start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num)
482 } else { // direction == ALL
483 start_line_num = above_last_line_num;
484 start_from_line_num = above_last_from_line_num;
485 end_line_num = below_first_line_num;
489 // Filling in all the remaining lines. Overwrite the expand links.
490 if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) {
491 expansion_area = expand_bar.querySelector('.ExpandLinkContainer');
492 expansion_area.innerHTML = '';
494 expansion_area = direction == ABOVE ? above_expansion : below_expansion;
497 insertLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num);
500 function unifiedLine(from, to, contents, is_expansion_line, opt_className, opt_attributes) {
501 var className = is_expansion_line ? 'ExpansionLine' : 'LineContainer Line';
503 className += ' ' + opt_className;
505 var lineNumberClassName = is_expansion_line ? 'expansionLineNumber' : 'lineNumber';
507 var line = $('<div class="' + className + '" ' + (opt_attributes || '') + '>' +
508 '<span class="from ' + lineNumberClassName + '">' + (from || ' ') +
509 '</span><span class="to ' + lineNumberClassName + '">' + (to || ' ') +
510 '</span> <span class="text"></span>' +
512 // Use text instead of innerHTML to avoid evaluting HTML.
513 $('.text', line).text(contents);
517 function unifiedExpansionLine(line_number, contents) {
518 return unifiedLine(line_number, line_number, contents, true);
521 function sideBySideExpansionLine(line_number, contents) {
522 var line = $('<div class="ExpansionLine"></div>');
523 line.append(lineSide('from', contents, true, line_number));
524 line.append(lineSide('to', contents, true, line_number));
528 function lineSide(side, contents, is_expansion_line, opt_line_number, opt_attributes, opt_class) {
530 if (opt_attributes || opt_class) {
531 class_name = 'class="';
533 class_name += is_expansion_line ? 'ExpansionLine' : 'Line';
534 class_name += ' ' + (opt_class || '') + '"';
537 var attributes = opt_attributes || '';
539 var line_side = $('<div class="LineSide">' +
540 '<div ' + attributes + ' ' + class_name + '>' +
541 '<span class="' + side + ' ' + (is_expansion_line ? 'expansionLineNumber' : 'lineNumber') + '">' +
542 (opt_line_number || ' ') +
544 '<span class="text"></span>' +
548 // Use text instead of innerHTML to avoid evaluting HTML.
549 $('.text', line_side).text(contents);
553 function insertLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) {
554 var fragment = document.createDocumentFragment();
555 var is_side_by_side = isDiffSideBySide(files[file_name]);
557 for (var i = 0; i < end_line_num - start_line_num; i++) {
558 // FIXME: from line numbers are wrong
559 var line_number = start_from_line_num + i + 1;
560 var contents = patched_file_contents[file_name][start_line_num + i];
561 var line = is_side_by_side ? sideBySideExpansionLine(line_number, contents) : unifiedExpansionLine(line_number, contents);
562 fragment.appendChild(line[0]);
565 if (direction == BELOW)
566 expansion_area.insertBefore(fragment, expansion_area.firstChild);
568 expansion_area.appendChild(fragment);
571 function hunkStartingLine(patched_file, context, prev_line, hunk_num) {
573 var current_line = -1;
574 var last_context_line = context[context.length - 1];
575 if (patched_file[prev_line] == last_context_line)
576 current_line = prev_line + 1;
578 for (var i = prev_line - PATCH_FUZZ; i < prev_line + PATCH_FUZZ; i++) {
579 if (patched_file[i] == last_context_line)
580 current_line = i + 1;
583 if (current_line == -1) {
584 console.log('Hunk #' + hunk_num + ' FAILED.');
589 // For paranoia sake, confirm the rest of the context matches;
590 for (var i = 0; i < context.length - 1; i++) {
591 if (patched_file[current_line - context.length + i] != context[i]) {
592 console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.');
600 function fromLineNumber(line) {
601 var node = line.querySelector('.from');
602 return node ? Number(node.textContent) : 0;
605 function toLineNumber(line) {
606 var node = line.querySelector('.to');
607 return node ? Number(node.textContent) : 0;
610 function textContentsFor(line) {
611 // Just get the first match since a side-by-side diff has two lines with text inside them for
612 // unmodified lines in the diff.
613 return $('.text', line).first().text();
616 function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) {
617 if (context.length) {
618 var prev_line_num = fromLineNumber(prev_line) - 1;
619 return hunkStartingLine(patched_file, context, prev_line_num, hunk_num);
622 if (toLineNumber(line) == 1 || fromLineNumber(line) == 1)
625 console.log('Failed to apply patch. Adds or removes lines before any context lines.');
629 function applyDiff(original_file, file_name) {
630 var diff_sections = files[file_name].getElementsByClassName('DiffSection');
631 var patched_file = original_file.concat([]);
633 // Apply diffs in reverse order to avoid needing to keep track of changing line numbers.
634 for (var i = diff_sections.length - 1; i >= 0; i--) {
635 var section = diff_sections[i];
636 var lines = section.getElementsByClassName('Line');
637 var current_line = -1;
639 var hunk_num = i + 1;
641 for (var j = 0, lines_len = lines.length; j < lines_len; j++) {
643 var line_contents = textContentsFor(line);
644 if ($(line).hasClass('add')) {
645 if (current_line == -1) {
646 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
647 if (current_line == -1)
651 patched_file.splice(current_line, 0, line_contents);
653 } else if ($(line).hasClass('remove')) {
654 if (current_line == -1) {
655 current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
656 if (current_line == -1)
660 if (patched_file[current_line] != line_contents) {
661 console.log('Hunk #' + hunk_num + ' FAILED.');
665 patched_file.splice(current_line, 1);
666 } else if (current_line == -1) {
667 context.push(line_contents);
668 } else if (line_contents != patched_file[current_line]) {
669 console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match');
680 function openOverallComments(e) {
681 $('.overallComments textarea').addClass('open');
682 $('#statusBubbleContainer').addClass('wrap');
685 function onBodyResize() {
686 updateToolbarAnchorState();
689 function updateToolbarAnchorState() {
690 var has_scrollbar = window.innerWidth > document.documentElement.offsetWidth;
691 $('#toolbar').toggleClass('anchored', has_scrollbar);
694 $(document).ready(function() {
697 $(document.body).prepend('<div id="message">' +
698 '<div class="help">Select line numbers to add a comment.' +
699 '<div class="DiffLinks LinkContainer">' +
700 '<a href="javascript:" class="unify-link">unified</a>' +
701 '<a href="javascript:" class="side-by-side-link">side-by-side</a>' +
704 '<div class="commentStatus"></div>' +
706 $(document.body).append('<div id="toolbar">' +
707 '<div class="overallComments">' +
708 '<textarea placeholder="Overall comments"></textarea>' +
711 '<span id="statusBubbleContainer"></span>' +
712 '<span class="actions">' +
713 '<span class="links"><span class="bugLink"></span></span>' +
714 '<span id="flagContainer"></span>' +
715 '<button id="preview_comments">Preview</button>' +
716 '<button id="post_comments">Publish</button> ' +
721 $('.overallComments textarea').bind('click', openOverallComments);
723 $(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>');
725 // Create a dummy iframe and monitor resizes in it's contentWindow to detect when the top document's body changes size.
726 // FIXME: Should we setTimeout throttle these?
727 var resize_iframe = $('<iframe class="pseudo_resize_event_iframe"></iframe>');
728 $(document.body).append(resize_iframe);
729 $(resize_iframe[0].contentWindow).bind('resize', onBodyResize);
731 updateToolbarAnchorState();
734 function isDiffSideBySide(file_diff) {
735 return diffState(file_diff) == 'sidebyside';
738 function diffState(file_diff) {
739 var diff_state = $(file_diff).attr('data-diffstate');
740 return diff_state || 'unified';
743 function unifyLine(line, from, to, contents, classNames, attributes, id) {
744 var new_line = unifiedLine(from, to, contents, false, classNames, attributes);
745 var old_line = $(line);
746 if (!old_line.hasClass('LineContainer'))
747 old_line = old_line.parents('.LineContainer');
749 var comments = commentsToTransferFor($(document.getElementById(id)));
750 old_line.after(comments);
751 old_line.replaceWith(new_line);
754 function convertFileDiff(diff_type, file_diff) {
755 if (diffState(file_diff) == diff_type)
758 $(file_diff).attr('data-diffstate', diff_type);
760 $('.Line', file_diff).each(function() {
761 convertLine(diff_type, this);
764 $('.ExpansionLine', file_diff).each(function() {
765 convertExpansionLine(diff_type, this);
769 function convertLine(diff_type, line) {
770 var convert_function = diff_type == 'sidebyside' ? sideBySideifyLine : unifyLine;
771 var from = fromLineNumber(line);
772 var to = toLineNumber(line);
773 var contents = textContentsFor(line);
774 var classNames = classNamesForMovingLine(line);
775 var attributes = attributesForMovingLine(line);
777 convert_function(line, from, to, contents, classNames, attributes, id)
780 function classNamesForMovingLine(line) {
781 var classParts = line.className.split(' ');
782 var classBuffer = [];
783 for (var i = 0; i < classParts.length; i++) {
784 var part = classParts[i];
785 if (part != 'LineContainer' && part != 'Line')
786 classBuffer.push(part);
788 return classBuffer.join(' ');
791 function attributesForMovingLine(line) {
792 var attributesBuffer = ['id=' + line.id];
793 // Make sure to keep all data- attributes.
794 $(line.attributes).each(function() {
795 if (this.name.indexOf('data-') == 0)
796 attributesBuffer.push(this.name + '=' + this.value);
798 return attributesBuffer.join(' ');
801 // FIXME: Put removed lines to the left of their corresponding added lines.
802 // FIXME: Allow for converting an individual file to side-by-side.
803 function sideBySideifyLine(line, from, to, contents, classNames, attributes, id) {
806 var from_attributes = '';
807 var to_attributes = '';
808 var from_contents = contents;
809 var to_contents = contents;
811 if (from && !to) { // This is a remove line.
812 from_class = classNames;
813 from_attributes = attributes;
815 } else if (to && !from) { // This is an add line.
816 to_class = classNames;
817 to_attributes = attributes;
821 var container_class = 'LineContainer';
822 var container_attributes = '';
823 if (!to_attributes && !from_attributes) {
824 container_attributes = attributes;
825 container_class += ' Line ' + classNames;
828 var new_line = $('<div ' + container_attributes + ' class="' + container_class + '"></div>');
829 new_line.append(lineSide('from', from_contents, false, from, from_attributes, from_class));
830 new_line.append(lineSide('to', to_contents, false, to, to_attributes, to_class));
832 $(line).replaceWith(new_line);
834 var line = $(document.getElementById(id));
835 line.after(commentsToTransferFor(line));
838 function convertExpansionLine(diff_type, line) {
839 var convert_function = diff_type == 'sidebyside' ? sideBySideExpansionLine : unifiedExpansionLine;
840 var contents = textContentsFor(line);
841 var line_number = fromLineNumber(line);
842 var new_line = convert_function(line_number, contents);
843 $(line).replaceWith(new_line);
846 function commentsToTransferFor(line) {
847 var fragment = document.createDocumentFragment();
849 previousCommentsFor(line).each(function() {
850 fragment.appendChild(this);
853 var active_comments = activeCommentFor(line);
854 var num_active_comments = active_comments.size();
855 if (num_active_comments > 0) {
856 if (num_active_comments > 1)
857 console.log('ERROR: There is more than one active comment for ' + line.attr('id') + '.');
859 var parent = active_comments[0].parentNode;
860 var frozenComment = parent.nextSibling;
861 fragment.appendChild(parent);
862 fragment.appendChild(frozenComment);
868 function discardComment() {
869 var line_id = $(this).parentsUntil('.comment').parent().find('textarea').attr('data-comment-for');
870 var line = $('#' + line_id)
871 findCommentBlockFor(line).slideUp('fast', function() {
873 line.removeAttr('data-has-comment');
874 trimCommentContextToBefore(line);
878 function unfreezeComment() {
879 $(this).prev().show();
883 $('.side-by-side-link').live('click', handleSideBySideLinkClick);
884 $('.unify-link').live('click', handleUnifyLinkClick);
885 $('.ExpandLink').live('click', handleExpandLinkClick);
886 $('.comment .discard').live('click', discardComment);
887 $('.frozenComment').live('click', unfreezeComment);
889 $('.comment .ok').live('click', function() {
890 var comment_textarea = $(this).parentsUntil('.comment').parent().find('textarea');
891 if (comment_textarea.val().trim() == '') {
892 discardComment.call(this);
895 var line_id = comment_textarea.attr('data-comment-for');
896 var line = $('#' + line_id)
897 findCommentBlockFor(line).hide().after($('<div class="frozenComment"></div>').text(comment_textarea.val()));
900 function focusOn(comment) {
901 $('.focused').removeClass('focused');
902 if (comment.length == 0)
904 $(document).scrollTop(comment.addClass('focused').position().top - window.innerHeight/2);
907 function focusNextComment() {
908 var comments = $('.previousComment');
909 if (comments.length == 0)
911 var index = comments.index($('.focused'));
912 // Notice that -1 gets mapped to 0.
913 focusOn($(comments.get(index + 1)));
916 function focusPreviousComment() {
917 var comments = $('.previousComment');
918 if (comments.length == 0)
920 var index = comments.index($('.focused'));
922 index = comments.length;
927 focusOn($(comments.get(index - 1)));
930 var kCharCodeForN = 'n'.charCodeAt(0);
931 var kCharCodeForP = 'p'.charCodeAt(0);
933 $('body').live('keypress', function() {
934 // FIXME: There's got to be a better way to avoid seeing these keypress
936 if (event.target.nodeName == 'TEXTAREA')
938 if (event.charCode == kCharCodeForN)
940 else if (event.charCode == kCharCodeForP)
941 focusPreviousComment();
944 function contextLinesFor(line_id) {
945 return $('div[data-comment-base-line~="' + line_id + '"]');
948 function numberFrom(line_id) {
949 return Number(line_id.replace('line', ''));
952 function trimCommentContextToBefore(line) {
953 var base_line_id = line.attr('data-comment-base-line');
954 var line_to_trim_to = numberFrom(line.attr('id'));
955 contextLinesFor(base_line_id).each(function() {
956 var id = $(this).attr('id');
957 if (numberFrom(id) > line_to_trim_to)
960 removeDataCommentBaseLine(this, base_line_id);
961 if (!$(this).attr('data-comment-base-line'))
962 $(this).removeClass('commentContext');
966 var in_drag_select = false;
968 function stopDragSelect() {
969 $('.selected').removeClass('selected');
970 in_drag_select = false;
973 $('.lineNumber').live('click', function() {
974 var line = $(this).parent();
975 if (line.hasClass('commentContext'))
976 trimCommentContextToBefore(line.prev());
977 }).live('mousedown', function() {
978 in_drag_select = true;
979 $(lineFromLineDescendant(this)).addClass('selected');
980 event.preventDefault();
983 $('.LineContainer').live('mouseenter', function() {
987 var line = lineFromLineContainer(this);
988 line.addClass('selected');
989 }).live('mouseup', function() {
992 var selected = $('.selected');
993 var should_add_comment = !selected.last().next().hasClass('commentContext');
994 selected.addClass('commentContext');
997 if (should_add_comment) {
998 var last = lineFromLineDescendant(selected.last()[0]);
999 addCommentFor($(last));
1002 id = selected.last().next()[0].getAttribute('data-comment-base-line');
1005 selected.each(function() {
1006 addDataCommentBaseLine(this, id);
1010 function addDataCommentBaseLine(line, id) {
1011 var val = $(line).attr('data-comment-base-line');
1013 var parts = val ? val.split(' ') : [];
1014 for (var i = 0; i < parts.length; i++) {
1020 $(line).attr('data-comment-base-line', parts.join(' '));
1023 function removeDataCommentBaseLine(line, id) {
1024 var val = $(line).attr('data-comment-base-line');
1028 var parts = val.split(' ');
1030 for (var i = 0; i < parts.length; i++) {
1032 newVal.push(parts[i]);
1035 $(line).attr('data-comment-base-line', newVal.join(' '));
1038 function lineFromLineDescendant(descendant) {
1039 while (descendant && !$(descendant).hasClass('Line')) {
1040 descendant = descendant.parentNode;
1045 function lineFromLineContainer(lineContainer) {
1046 var line = $(lineContainer);
1047 if (!line.hasClass('Line'))
1048 line = $('.Line', line);
1052 $('.DiffSection').live('mouseleave', stopDragSelect).live('mouseup', stopDragSelect);
1054 function contextSnippetFor(line, indent) {
1056 contextLinesFor(line.attr('id')).each(function() {
1058 if ($(this).hasClass('add'))
1060 else if ($(this).hasClass('remove'))
1062 snippets.push(indent + action + textContentsFor(this));
1064 return snippets.join('\n');
1067 function fileNameFor(line) {
1068 return line.parentsUntil('.FileDiff').parent().find('h1').text();
1071 function indentFor(depth) {
1072 return (new Array(depth + 1)).join('>') + ' ';
1075 function snippetFor(line, indent) {
1076 var file_name = fileNameFor(line);
1077 var line_number = line.hasClass('remove') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]);
1078 return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent);
1081 function quotePreviousComments(comments) {
1082 var quoted_comments = [];
1083 var depth = comments.size();
1084 comments.each(function() {
1085 var indent = indentFor(depth--);
1086 var text = $(this).children('.content').text();
1087 quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent));
1089 return quoted_comments.join('\n');
1092 $('#comment_form .winter').live('click', function() {
1093 $('#comment_form').addClass('inactive');
1096 function fillInReviewForm() {
1097 var comments_in_context = []
1098 forEachLine(function(line) {
1099 if (line.attr('data-has-comment') != 'true')
1101 var comment = findCommentBlockFor(line).children('textarea').val().trim();
1104 var previous_comments = previousCommentsFor(line);
1105 var snippet = snippetFor(line, indentFor(previous_comments.size() + 1));
1106 var quoted_comments = quotePreviousComments(previous_comments);
1107 var comment_with_context = [];
1108 comment_with_context.push(snippet);
1109 if (quoted_comments != '')
1110 comment_with_context.push(quoted_comments);
1111 comment_with_context.push('\n' + comment);
1112 comments_in_context.push(comment_with_context.join('\n'));
1114 var comment = $('.overallComments textarea').val().trim();
1117 comment += comments_in_context.join('\n\n');
1118 if (comments_in_context.length > 0)
1119 comment = 'View in context: ' + window.location + '\n\n' + comment;
1120 var review_form = $('#reviewform').contents();
1121 review_form.find('#comment').val(comment);
1122 review_form.find('#flags select').each(function() {
1123 var control = findControlForFlag(this);
1124 if (!control.size())
1126 $(this).attr('selectedIndex', control.attr('selectedIndex'));
1130 $('#preview_comments').live('click', function() {
1132 $('#comment_form').removeClass('inactive');
1135 $('#post_comments').live('click', function() {
1137 $('#reviewform').contents().find('form').submit();