2011-02-07 Ojan Vafai <ojan@chromium.org>
[WebKit-https.git] / Websites / bugs.webkit.org / code-review.js
1 // Copyright (C) 2010 Adam Barth. All rights reserved.
2 //
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions are met:
5 //
6 // 1. Redistributions of source code must retain the above copyright notice,
7 // this list of conditions and the following disclaimer.
8 //
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.
12 //
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
23 // DAMAGE.
24 var CODE_REVIEW_UNITTEST;
25
26 (function() {
27   /**
28    * Create a new function with some of its arguements
29    * pre-filled.
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
33    *     applied to fn.
34    * @return {!Function} A partially-applied form of the function.
35    */
36   function partial(fn, var_args) {
37     var args = Array.prototype.slice.call(arguments, 1);
38     return function() {
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);
43     };
44   };
45
46   function determineAttachmentID() {
47     try {
48       return /id=(\d+)/.exec(window.location.search)[1]
49     } catch (ex) {
50       return;
51     }
52   }
53
54   // Attempt to activate only in the "Review Patch" context.
55   if (window.top != window)
56     return;
57
58   if (!CODE_REVIEW_UNITTEST && !window.location.search.match(/action=review/)
59       && !window.location.toString().match(/bugs\.webkit\.org\/PrettyPatch/))
60     return;
61
62   var attachment_id = determineAttachmentID();
63   if (!attachment_id)
64     console.log('No attachment ID');
65
66   var next_line_id = 0;
67   var files = {};
68   var original_file_contents = {};
69   var patched_file_contents = {};
70   var WEBKIT_BASE_DIR = "http://svn.webkit.org/repository/webkit/trunk/";
71   var SIDE_BY_SIDE_DIFFS_KEY = 'sidebysidediffs';
72   var g_displayed_draft_comments = false;
73
74   function idForLine(number) {
75     return 'line' + number;
76   }
77
78   function nextLineID() {
79     return idForLine(next_line_id++);
80   }
81
82   function forEachLine(callback) {
83     for (var i = 0; i < next_line_id; ++i) {
84       callback($('#' + idForLine(i)));
85     }
86   }
87
88   function idify() {
89     this.id = nextLineID();
90   }
91
92   function hoverify() {
93     $(this).hover(function() {
94       $(this).addClass('hot');
95     },
96     function () {
97       $(this).removeClass('hot');
98     });
99   }
100
101   function fileDiffFor(line) {
102     return $(line).parents('.FileDiff');
103   }
104
105   function activeCommentFor(line) {
106     // Scope to the diffSection as a performance improvement.
107     return $('textarea[data-comment-for~="' + line[0].id + '"]', fileDiffFor(line));
108   }
109
110   function previousCommentsFor(line) {
111     // Scope to the diffSection as a performance improvement.
112     return $('div[data-comment-for~="' + line[0].id + '"].previousComment', fileDiffFor(line));
113   }
114
115   function findCommentPositionFor(line) {
116     var previous_comments = previousCommentsFor(line);
117     var num_previous_comments = previous_comments.size();
118     if (num_previous_comments)
119       return $(previous_comments[num_previous_comments - 1])
120     return line;
121   }
122
123   function findCommentBlockFor(line) {
124     var comment_block = findCommentPositionFor(line).next();
125     if (!comment_block.hasClass('comment'))
126       return;
127     return comment_block;
128   }
129
130   function insertCommentFor(line, block) {
131     findCommentPositionFor(line).after(block);
132   }
133
134   function addDraftComment(start_line_id, end_line_id, contents) {
135     var line = $('#' + end_line_id);
136     var start = numberFrom(start_line_id);
137     var end = numberFrom(end_line_id);
138     for (var i = start; i <= end; i++) {
139       var line = $('#line' + i);
140       line.addClass('commentContext');
141       addDataCommentBaseLine(line, end_line_id);
142     }
143
144     var comment_block = createCommentFor(line);
145     $(comment_block).children('textarea').val(contents);
146     freezeComment(comment_block);
147   }
148
149   function ensureDraftCommentsDisplayed() {
150     if (g_displayed_draft_comments)
151       return;
152     g_displayed_draft_comments = true;
153
154     var comments = g_draftCommentSaver.saved_comments();
155     $(comments).each(function() {
156       addDraftComment(this.start_line_id, this.end_line_id, this.contents);
157     });
158   }
159
160   function DraftCommentSaver(opt_attachment_id, opt_localStorage) {
161     this._attachment_id = opt_attachment_id || attachment_id;
162     this._localStorage = opt_localStorage || localStorage;
163     this._save_comments = true;
164   }
165
166   if (CODE_REVIEW_UNITTEST)
167     window['DraftCommentSaver'] = DraftCommentSaver;
168   
169   DraftCommentSaver.prototype._json = function() {
170     var comments = $('.comment');
171     var comment_store = [];
172     comments.each(function () {
173       var file_diff = fileDiffFor(this);
174       var textarea = $('textarea', this);
175
176       var contents = textarea.val().trim();
177       if (!contents)
178         return;
179
180       var comment_base_line = textarea.attr('data-comment-for');
181       var lines = contextLinesFor(comment_base_line, file_diff);
182
183       comment_store.push({
184         start_line_id: lines.first().attr('id'),
185         end_line_id: comment_base_line,
186         contents: contents
187       });
188     });
189
190     return JSON.stringify({'born-on': Date.now(), 'comments': comment_store});
191   }
192   
193   DraftCommentSaver.prototype.saved_comments = function() {
194     var serialized_comments = this._localStorage.getItem(DraftCommentSaver._keyPrefix + this._attachment_id);
195     if (!serialized_comments)
196       return [];
197
198     var comments = [];
199     try {
200       comments = JSON.parse(serialized_comments).comments;
201     } catch (e) {
202       this._erase_corrupt_comments();
203       return [];
204     }
205     
206     if (comments && !comments.length)
207       return comments;
208     
209     // Sanity check comments are as expected.
210     if (!comments || !comments[0].contents) {
211       this._erase_corrupt_comments();
212       return [];
213     }
214     
215     return comments;
216   }
217   
218   DraftCommentSaver.prototype._erase_corrupt_comments = function() {
219     // FIXME: Show an error to the user instead of logging.
220     console.log('Draft comments were corrupted. Erasing comments.');
221     this.erase();
222   }
223   
224   DraftCommentSaver.prototype.save = function() {
225     if (!this._save_comments)
226       return;
227
228     var key = DraftCommentSaver._keyPrefix + this._attachment_id;
229     var value = this._json();
230
231     if (this._attemptToWrite(key, value))
232       return;
233
234     this._eraseOldCommentsForAllReviews();
235     if (this._attemptToWrite(key, value))
236       return;
237
238     var remove_comments = this._should_remove_comments();
239     if (!remove_comments) {
240       this._save_comments = false;
241       return;
242     }
243
244     this._eraseCommentsForAllReviews();
245     if (this._attemptToWrite(key, value))
246       return;
247
248     this._save_comments = false;
249     // FIXME: Show an error to the user.
250   }
251
252   DraftCommentSaver.prototype._should_remove_comments = function(message) {
253     return prompt('Local storage quota is full. Remove draft comments from all previous reviews to make room?');
254   }
255
256   DraftCommentSaver.prototype._attemptToWrite = function(key, value) {
257     try {
258       this._localStorage.setItem(key, value);
259       return true;
260     } catch (e) {
261       return false;
262     }
263   }
264
265   DraftCommentSaver._keyPrefix = 'draft-comments-for-attachment-';
266
267   DraftCommentSaver.prototype.erase = function() {
268     this._localStorage.removeItem(DraftCommentSaver._keyPrefix + this._attachment_id);
269   }
270
271   DraftCommentSaver.prototype._eraseOldCommentsForAllReviews = function() {
272     this._eraseComments(true);
273   }
274   DraftCommentSaver.prototype._eraseCommentsForAllReviews = function() {
275     this._eraseComments(false);
276   }
277
278   var MONTH_IN_MS = 1000 * 60 * 60 * 24 * 30;
279
280   DraftCommentSaver.prototype._eraseComments = function(only_old_reviews) {
281     var length = this._localStorage.length;
282     var keys_to_delete = [];
283     for (var i = 0; i < length; i++) {
284       var key = this._localStorage.key(i);
285       if (key.indexOf(DraftCommentSaver._keyPrefix) != 0)
286         continue;
287         
288       if (only_old_reviews) {
289         try {
290           var born_on = JSON.parse(this._localStorage.getItem(key))['born-on'];
291           if (Date.now() - born_on < MONTH_IN_MS)
292             continue;
293         } catch (e) {
294           console.log('Deleting JSON. JSON for code review is corrupt: ' + key);
295         }        
296       }
297       keys_to_delete.push(key);
298     }
299
300     for (var i = 0; i < keys_to_delete.length; i++) {
301       this._localStorage.removeItem(keys_to_delete[i]);
302     }
303   }
304   
305   var g_draftCommentSaver = new DraftCommentSaver();
306
307   function saveDraftComments() {
308     ensureDraftCommentsDisplayed();
309     g_draftCommentSaver.save();
310   }
311
312   function createCommentFor(line) {
313     if (line.attr('data-has-comment')) {
314       // FIXME: This query is overly complex because we place comment blocks
315       // after Lines.  Instead, comment blocks should be children of Lines.
316       findCommentPositionFor(line).next().next().filter('.frozenComment').each(unfreezeComment);
317       return;
318     }
319     line.attr('data-has-comment', 'true');
320     line.addClass('commentContext');
321
322     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>');
323     insertCommentFor(line, comment_block);
324     return comment_block;
325   }
326
327   function addCommentFor(line) {
328     var comment_block = createCommentFor(line);
329     comment_block.hide().slideDown('fast', function() {
330       $(this).children('textarea').focus();
331     });
332   }
333
334   function addCommentField() {
335     var id = $(this).attr('data-comment-for');
336     if (!id)
337       id = this.id;
338     addCommentFor($('#' + id));
339   }
340
341   function addPreviousComment(line, author, comment_text) {
342     var line_id = line.attr('id');
343     var comment_block = $('<div data-comment-for="' + line_id + '" class="previousComment"></div>');
344     var author_block = $('<div class="author"></div>').text(author + ':');
345     var text_block = $('<div class="content"></div>').text(comment_text);
346     comment_block.append(author_block).append(text_block).each(hoverify).click(addCommentField);
347     addDataCommentBaseLine(line, line_id);
348     insertCommentFor(line, comment_block);
349   }
350
351   function displayPreviousComments(comments) {
352     for (var i = 0; i < comments.length; ++i) {
353       var author = comments[i].author;
354       var file_name = comments[i].file_name;
355       var line_number = comments[i].line_number;
356       var comment_text = comments[i].comment_text;
357
358       var file = files[file_name];
359
360       var query = '.Line .to';
361       if (line_number[0] == '-') {
362         // The line_number represent a removal.  We need to adjust the query to
363         // look at the "from" lines.
364         query = '.Line .from';
365         // Trim off the '-' control character.
366         line_number = line_number.substr(1);
367       }
368
369       $(file).find(query).each(function() {
370         if ($(this).text() != line_number)
371           return;
372         var line = $(this).parent();
373         addPreviousComment(line, author, comment_text);
374       });
375     }
376
377     if (comments.length == 0) {
378       return;
379     }
380
381     descriptor = comments.length + ' comment';
382     if (comments.length > 1)
383       descriptor += 's';
384     $('.help').append(' This patch has ' + descriptor + '.  Scroll through them with the "n" and "p" keys.');
385   }
386
387   function scanForStyleQueueComments(text) {
388     var comments = []
389     var lines = text.split('\n');
390     for (var i = 0; i < lines.length; ++i) {
391       var parts = lines[i].match(/^([^:]+):(-?\d+):(.*)$/);
392       if (!parts)
393         continue;
394
395       var file_name = parts[1];
396       var line_number = parts[2];
397       var comment_text = parts[3].trim();
398
399       if (!file_name in files) {
400         console.log('Filename in style queue output is not in the patch: ' + file_name);
401         continue;
402       }
403
404       comments.push({
405         'author': 'StyleQueue',
406         'file_name': file_name,
407         'line_number': line_number,
408         'comment_text': comment_text
409       });
410     }
411     return comments;
412   }
413
414   function scanForComments(author, text) {
415     var comments = []
416     var lines = text.split('\n');
417     for (var i = 0; i < lines.length; ++i) {
418       var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/);
419       if (!parts)
420         continue;
421       var quote_markers = parts[1];
422       var file_name = parts[2];
423       // FIXME: Store multiple lines for multiline comments and correctly import them here.
424       var line_number = parts[3];
425       if (!file_name in files)
426         continue;
427       while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>')
428         ++i;
429       var comment_lines = [];
430       while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) {
431         comment_lines.push(lines[i]);
432         ++i;
433       }
434       --i; // Decrement i because the for loop will increment it again in a second.
435       var comment_text = comment_lines.join('\n').trim();
436       comments.push({
437         'author': author,
438         'file_name': file_name,
439         'line_number': line_number,
440         'comment_text': comment_text
441       });
442     }
443     return comments;
444   }
445
446   function isReviewFlag(select) {
447     return $(select).attr('title') == 'Request for patch review.';
448   }
449
450   function isCommitQueueFlag(select) {
451     return $(select).attr('title').match(/commit-queue/);
452   }
453
454   function findControlForFlag(select) {
455     if (isReviewFlag(select))
456       return $('#toolbar .review select');
457     else if (isCommitQueueFlag(select))
458       return $('#toolbar .commitQueue select');
459     return $();
460   }
461
462   function addFlagsForAttachment(details) {
463     var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>";
464     $('#flagContainer').append(
465       $('<span class="review"> r: ' + flag_control + '</span>')).append(
466       $('<span class="commitQueue"> cq: ' + flag_control + '</span>'));
467
468     details.find('#flags select').each(function() {
469       var requestee = $(this).parent().siblings('td:first-child').text().trim();
470       if (requestee.length) {
471         // Remove trailing ':'.
472         requestee = requestee.substr(0, requestee.length - 1);
473         requestee = ' (' + requestee + ')';
474       }
475       var control = findControlForFlag(this)
476       control.attr('selectedIndex', $(this).attr('selectedIndex'));
477       control.parent().prepend(requestee);
478     });
479   }
480
481   window.addEventListener('message', function(e) {
482     if (e.origin != 'https://webkit-commit-queue.appspot.com')
483       return;
484
485     if (e.data.height) {
486       $('.statusBubble')[0].style.height = e.data.height;
487       $('.statusBubble')[0].style.width = e.data.width;
488     }
489   }, false);
490
491   function handleStatusBubbleLoad(e) {
492     e.target.contentWindow.postMessage('containerMetrics', 'https://webkit-commit-queue.appspot.com');
493   }
494
495   function fetchHistory() {
496     $.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) {
497       var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1];
498       $.get('show_bug.cgi?id=' + bug_id, function(data) {
499         var comments = [];
500         $(data).find('.bz_comment').each(function() {
501           var author = $(this).find('.email').text();
502           var text = $(this).find('.bz_comment_text').text();
503
504           var comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
505           if (text.match(comment_marker))
506             $.merge(comments, scanForComments(author, text));
507
508           var style_queue_comment_marker = 'Attachment ' + attachment_id + ' .details. did not pass style-queue.'
509           if (text.match(style_queue_comment_marker))
510             $.merge(comments, scanForStyleQueueComments(text));
511         });
512         displayPreviousComments(comments);
513         ensureDraftCommentsDisplayed();
514       });
515
516       var details = $(data);
517       addFlagsForAttachment(details);
518
519       var statusBubble = document.createElement('iframe');
520       statusBubble.className = 'statusBubble';
521       statusBubble.src  = 'https://webkit-commit-queue.appspot.com/status-bubble/' + attachment_id;
522       statusBubble.scrolling = 'no';
523       // Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM.
524       statusBubble.onload = handleStatusBubbleLoad;
525       $('#statusBubbleContainer').append(statusBubble);
526
527       $('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
528     });
529   }
530
531   function firstLine(file_diff) {
532     var container = $('.LineContainer:not(.context)', file_diff)[0];
533     if (!container)
534       return 0;
535
536     var from = fromLineNumber(container);
537     var to = toLineNumber(container);
538     return from || to;
539   }
540
541   function crawlDiff() {
542     $('.Line').each(idify).each(hoverify);
543     $('.FileDiff').each(function() {
544       var header = $(this).children('h1');
545       var url_hash = '#L' + firstLine(this);
546
547       var file_name = header.text();
548       files[file_name] = this;
549
550       addExpandLinks(file_name);
551
552       var diff_links = $('<div class="FileDiffLinkContainer LinkContainer">' +
553           diffLinksHtml() +
554           '</div>');
555
556       var file_link = $('a', header)[0];
557       // If the base directory in the file path does not match a WebKit top level directory,
558       // then PrettyPatch.rb doesn't linkify the header.
559       if (file_link) {
560         file_link.target = "_blank";
561         file_link.href += url_hash;
562         diff_links.append(tracLinks(file_name, url_hash));
563       }
564
565       $('h1', this).after(diff_links);
566       updateDiffLinkVisibility(this);
567     });
568   }
569
570   function tracLinks(file_name, url_hash) {
571     var trac_links = $('<a target="_blank">annotate</a><a target="_blank">revision log</a>');
572     trac_links[0].href = 'http://trac.webkit.org/browser/trunk/' + file_name + '?annotate=blame' + url_hash;
573     trac_links[1].href = 'http://trac.webkit.org/log/trunk/' + file_name;
574     return trac_links;
575   }
576
577   function addExpandLinks(file_name) {
578     if (file_name.indexOf('ChangeLog') != -1)
579       return;
580
581     var file_diff = files[file_name];
582
583     // Don't show the links to expand upwards/downwards if the patch starts/ends without context
584     // lines, i.e. starts/ends with add/remove lines.
585     var first_line = file_diff.querySelector('.LineContainer:not(.context)');
586
587     // If there is no element with a "Line" class, then this is an image diff.
588     if (!first_line)
589       return;
590
591     var expand_bar_index = 0;
592     if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove'))
593       $('h1', file_diff).after(expandBarHtml(BELOW))
594
595     $('br', file_diff).replaceWith(expandBarHtml());
596
597     var last_line = file_diff.querySelector('.LineContainer:last-of-type');
598     // Some patches for new files somehow end up with an empty context line at the end
599     // with a from line number of 0. Don't show expand links in that case either.
600     if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0)
601       $('.revision', file_diff).before(expandBarHtml(ABOVE));
602   }
603
604   function expandBarHtml(opt_direction) {
605     var html = '<div class="ExpandBar">' +
606         '<div class="ExpandArea Expand' + ABOVE + '"></div>' +
607         '<div class="ExpandLinkContainer LinkContainer"><span class="ExpandText">expand: </span>';
608
609     // FIXME: If there are <100 line to expand, don't show the expand-100 link.
610     // If there are <20 lines to expand, don't show the expand-20 link.
611     if (!opt_direction || opt_direction == ABOVE) {
612       html += expandLinkHtml(ABOVE, 100) +
613           expandLinkHtml(ABOVE, 20);
614     }
615
616     html += expandLinkHtml(ALL);
617
618     if (!opt_direction || opt_direction == BELOW) {
619       html += expandLinkHtml(BELOW, 20) +
620         expandLinkHtml(BELOW, 100);
621     }
622
623     html += '</div><div class="ExpandArea Expand' + BELOW + '"></div></div>';
624     return html;
625   }
626
627   function expandLinkHtml(direction, amount) {
628     return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" +
629         (amount ? amount + " " : "") + direction + "</a>";
630   }
631
632   function handleExpandLinkClick() {
633     var expand_bar = $(this).parents('.ExpandBar');
634     var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent;
635     var expand_function = partial(expand, expand_bar[0], file_name, this.getAttribute('data-direction'), Number(this.getAttribute('data-amount')));
636     if (file_name in original_file_contents)
637       expand_function();
638     else
639       getWebKitSourceFile(file_name, expand_function, expand_bar);
640   }
641
642   function handleSideBySideLinkClick() {
643     convertDiff('sidebyside', this);
644   }
645
646   function handleUnifyLinkClick() {
647     convertDiff('unified', this);
648   }
649
650   function convertDiff(difftype, convert_link) {
651     var file_diffs = $(convert_link).parents('.FileDiff');
652     if (!file_diffs.size()) {
653       localStorage.setItem('code-review-diffstate', difftype);
654       file_diffs = $('.FileDiff');
655     }
656
657     convertAllFileDiffs(difftype, file_diffs);
658   }
659
660   function patchRevision() {
661     var revision = $('.revision');
662     return revision[0] ? revision.first().text() : null;
663   }
664
665   function getWebKitSourceFile(file_name, onLoad, expand_bar) {
666     function handleLoad(contents) {
667       original_file_contents[file_name] = contents.split('\n');
668       patched_file_contents[file_name] = applyDiff(original_file_contents[file_name], file_name);
669       onLoad();
670     };
671
672     var revision = patchRevision();
673     var queryParameters = revision ? '?p=' + revision : '';
674
675     $.ajax({
676       url: WEBKIT_BASE_DIR + file_name + queryParameters,
677       context: document.body,
678       complete: function(xhr, data) {
679               if (xhr.status == 0)
680                   handleLoadError(expand_bar);
681               else
682                   handleLoad(xhr.responseText);
683       }
684     });
685   }
686
687   function replaceExpandLinkContainers(expand_bar, text) {
688     $('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>');
689   }
690
691   function handleLoadError(expand_bar) {
692     replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?");
693   }
694
695   var ABOVE = 'above';
696   var BELOW = 'below';
697   var ALL = 'all';
698
699   function lineNumbersFromSet(set, is_last) {
700     var to = -1;
701     var from = -1;
702
703     var size = set.size();
704     var start = is_last ? (size - 1) : 0;
705     var end = is_last ? -1 : size;
706     var offset = is_last ? -1 : 1;
707
708     for (var i = start; i != end; i += offset) {
709       if (to != -1 && from != -1)
710         return {to: to, from: from};
711
712       var line_number = set[i];
713       if ($(line_number).hasClass('to')) {
714         if (to == -1)
715           to = Number(line_number.textContent);
716       } else {
717         if (from == -1)
718           from = Number(line_number.textContent);
719       }
720     }
721   }
722
723   function expand(expand_bar, file_name, direction, amount) {
724     if (file_name in original_file_contents && !patched_file_contents[file_name]) {
725       // FIXME: In this case, try fetching the source file at the revision the patch was created at.
726       // Might need to modify webkit-patch to include that data in the diff.
727       replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree.");
728       return;
729     }
730
731     var above_expansion = expand_bar.querySelector('.Expand' + ABOVE)
732     var below_expansion = expand_bar.querySelector('.Expand' + BELOW)
733
734     var above_line_numbers = $('.expansionLineNumber', above_expansion);
735     if (!above_line_numbers[0]) {
736       var diff_section = expand_bar.previousElementSibling;
737       above_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
738     }
739
740     var above_last_line_num, above_last_from_line_num;
741     if (above_line_numbers[0]) {
742       var above_numbers = lineNumbersFromSet(above_line_numbers, true);
743       above_last_line_num = above_numbers.to;
744       above_last_from_line_num = above_numbers.from;
745     } else
746       above_last_from_line_num = above_last_line_num = 0;
747
748     var below_line_numbers = $('.expansionLineNumber', below_expansion);
749     if (!below_line_numbers[0]) {
750       var diff_section = expand_bar.nextElementSibling;
751       if (diff_section)
752         below_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
753     }
754
755     var below_first_line_num, below_first_from_line_num;
756     if (below_line_numbers[0]) {
757       var below_numbers = lineNumbersFromSet(below_line_numbers, false);
758       below_first_line_num = below_numbers.to - 1;
759       below_first_from_line_num = below_numbers.from - 1;
760     } else
761       below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1;
762
763     var start_line_num, start_from_line_num;
764     var end_line_num;
765
766     if (direction == ABOVE) {
767       start_from_line_num = above_last_from_line_num;
768       start_line_num = above_last_line_num;
769       end_line_num = Math.min(start_line_num + amount, below_first_line_num);
770     } else if (direction == BELOW) {
771       end_line_num = below_first_line_num;
772       start_line_num = Math.max(end_line_num - amount, above_last_line_num)
773       start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num)
774     } else { // direction == ALL
775       start_line_num = above_last_line_num;
776       start_from_line_num = above_last_from_line_num;
777       end_line_num = below_first_line_num;
778     }
779
780     var lines = expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num);
781
782     var expansion_area;
783     // Filling in all the remaining lines. Overwrite the expand links.
784     if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) {
785       $('.ExpandLinkContainer', expand_bar).detach();
786       below_expansion.insertBefore(lines, below_expansion.firstChild);
787       // Now that we're filling in all the lines, the context line following this expand bar is no longer needed.
788       $('.context', expand_bar.nextElementSibling).detach();
789     } else if (direction == ABOVE) {
790       above_expansion.appendChild(lines);
791     } else {
792       below_expansion.insertBefore(lines, below_expansion.firstChild);
793     }
794   }
795
796   function unifiedLine(from, to, contents, is_expansion_line, opt_className, opt_attributes) {
797     var className = is_expansion_line ? 'ExpansionLine' : 'LineContainer Line';
798     if (opt_className)
799       className += ' ' + opt_className;
800
801     var lineNumberClassName = is_expansion_line ? 'expansionLineNumber' : 'lineNumber';
802
803     var line = $('<div class="' + className + '" ' + (opt_attributes || '') + '>' +
804         '<span class="from ' + lineNumberClassName + '">' + (from || '&nbsp;') +
805         '</span><span class="to ' + lineNumberClassName + '">' + (to || '&nbsp;') +
806         '</span> <span class="text"></span>' +
807         '</div>');
808
809     $('.text', line).replaceWith(contents);
810     return line;
811   }
812
813   function unifiedExpansionLine(from, to, contents) {
814     return unifiedLine(from, to, contents, true);
815   }
816
817   function sideBySideExpansionLine(from, to, contents) {
818     var line = $('<div class="ExpansionLine"></div>');
819     // Clone the contents so we have two copies we can put back in the DOM.
820     line.append(lineSide('from', contents.clone(true), true, from));
821     line.append(lineSide('to', contents, true, to));
822     return line;
823   }
824
825   function lineSide(side, contents, is_expansion_line, opt_line_number, opt_attributes, opt_class) {
826     var class_name = '';
827     if (opt_attributes || opt_class) {
828       class_name = 'class="';
829       if (opt_attributes)
830         class_name += is_expansion_line ? 'ExpansionLine' : 'Line';
831       class_name += ' ' + (opt_class || '') + '"';
832     }
833
834     var attributes = opt_attributes || '';
835
836     var line_side = $('<div class="LineSide">' +
837         '<div ' + attributes + ' ' + class_name + '>' +
838           '<span class="' + side + ' ' + (is_expansion_line ? 'expansionLineNumber' : 'lineNumber') + '">' +
839               (opt_line_number || '&nbsp;') +
840           '</span>' +
841           '<span class="text"></span>' +
842         '</div>' +
843         '</div>');
844
845     $('.text', line_side).replaceWith(contents);
846     return line_side;
847   }
848
849   function expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) {
850     var fragment = document.createDocumentFragment();
851     var is_side_by_side = isDiffSideBySide(files[file_name]);
852
853     for (var i = 0; i < end_line_num - start_line_num; i++) {
854       var from = start_from_line_num + i + 1;
855       var to = start_line_num + i + 1;
856       var contents = $('<span class="text"></span>');
857       contents.text(patched_file_contents[file_name][start_line_num + i]);
858       var line = is_side_by_side ? sideBySideExpansionLine(from, to, contents) : unifiedExpansionLine(from, to, contents);
859       fragment.appendChild(line[0]);
860     }
861
862     return fragment;
863   }
864
865   function hunkStartingLine(patched_file, context, prev_line, hunk_num) {
866     var PATCH_FUZZ = 2;
867     var current_line = -1;
868     var last_context_line = context[context.length - 1];
869     if (patched_file[prev_line] == last_context_line)
870       current_line = prev_line + 1;
871     else {
872       for (var i = prev_line - PATCH_FUZZ; i < prev_line + PATCH_FUZZ; i++) {
873         if (patched_file[i] == last_context_line)
874           current_line = i + 1;
875       }
876
877       if (current_line == -1) {
878         console.log('Hunk #' + hunk_num + ' FAILED.');
879         return -1;
880       }
881     }
882
883     // For paranoia sake, confirm the rest of the context matches;
884     for (var i = 0; i < context.length - 1; i++) {
885       if (patched_file[current_line - context.length + i] != context[i]) {
886         console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.');
887         return -1;
888       }
889     }
890
891     return current_line;
892   }
893
894   function fromLineNumber(line) {
895     var node = line.querySelector('.from');
896     return node ? Number(node.textContent) : 0;
897   }
898
899   function toLineNumber(line) {
900     var node = line.querySelector('.to');
901     return node ? Number(node.textContent) : 0;
902   }
903
904   function textContentsFor(line) {
905     // Just get the first match since a side-by-side diff has two lines with text inside them for
906     // unmodified lines in the diff.
907     return $('.text', line).first().text();
908   }
909
910   function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) {
911     if (context.length) {
912       var prev_line_num = fromLineNumber(prev_line) - 1;
913       return hunkStartingLine(patched_file, context, prev_line_num, hunk_num);
914     }
915
916     if (toLineNumber(line) == 1 || fromLineNumber(line) == 1)
917       return 0;
918
919     console.log('Failed to apply patch. Adds or removes lines before any context lines.');
920     return -1;
921   }
922
923   function applyDiff(original_file, file_name) {
924     var diff_sections = files[file_name].getElementsByClassName('DiffSection');
925     var patched_file = original_file.concat([]);
926
927     // Apply diffs in reverse order to avoid needing to keep track of changing line numbers.
928     for (var i = diff_sections.length - 1; i >= 0; i--) {
929       var section = diff_sections[i];
930       var lines = $('.Line:not(.context)', section);
931       var current_line = -1;
932       var context = [];
933       var hunk_num = i + 1;
934
935       for (var j = 0, lines_len = lines.length; j < lines_len; j++) {
936         var line = lines[j];
937         var line_contents = textContentsFor(line);
938         if ($(line).hasClass('add')) {
939           if (current_line == -1) {
940             current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
941             if (current_line == -1)
942               return null;
943           }
944
945           patched_file.splice(current_line, 0, line_contents);
946           current_line++;
947         } else if ($(line).hasClass('remove')) {
948           if (current_line == -1) {
949             current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
950             if (current_line == -1)
951               return null;
952           }
953
954           if (patched_file[current_line] != line_contents) {
955             console.log('Hunk #' + hunk_num + ' FAILED.');
956             return null;
957           }
958
959           patched_file.splice(current_line, 1);
960         } else if (current_line == -1) {
961           context.push(line_contents);
962         } else if (line_contents != patched_file[current_line]) {
963           console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match');
964           return null;
965         } else {
966           current_line++;
967         }
968       }
969     }
970
971     return patched_file;
972   }
973
974   function openOverallComments(e) {
975     $('.overallComments textarea').addClass('open');
976     $('#statusBubbleContainer').addClass('wrap');
977   }
978
979   function onBodyResize() {
980     updateToolbarAnchorState();
981   }
982
983   function updateToolbarAnchorState() {
984     var has_scrollbar = window.innerWidth > document.documentElement.offsetWidth;
985     $('#toolbar').toggleClass('anchored', has_scrollbar);
986   }
987
988   function diffLinksHtml() {
989     return '<a href="javascript:" class="unify-link">unified</a>' +
990       '<a href="javascript:" class="side-by-side-link">side-by-side</a>';
991   }
992
993
994   $(document).ready(function() {
995     crawlDiff();
996     fetchHistory();
997     $(document.body).prepend('<div id="message">' +
998         '<div class="help">Select line numbers to add a comment. Scroll though diffs with the "j" and "k" keys.' +
999           '<div class="DiffLinks LinkContainer">' + diffLinksHtml() + '</div>' +
1000         '</div>' +
1001         '</div>');
1002     $(document.body).append('<div id="toolbar">' +
1003         '<div class="overallComments">' +
1004             '<textarea placeholder="Overall comments"></textarea>' +
1005         '</div>' +
1006         '<div>' +
1007           '<span id="statusBubbleContainer"></span>' +
1008           '<span class="actions">' +
1009               '<span class="links"><span class="bugLink"></span></span>' +
1010               '<span id="flagContainer"></span>' +
1011               '<button id="preview_comments">Preview</button>' +
1012               '<button id="post_comments">Publish</button> ' +
1013           '</span></div>' +
1014         '</div>' +
1015         '</div>');
1016
1017     $('.overallComments textarea').bind('click', openOverallComments);
1018
1019     $(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>');
1020     $('#reviewform').bind('load', handleReviewFormLoad);
1021
1022     // Create a dummy iframe and monitor resizes in it's contentWindow to detect when the top document's body changes size.
1023     // FIXME: Should we setTimeout throttle these?
1024     var resize_iframe = $('<iframe class="pseudo_resize_event_iframe"></iframe>');
1025     $(document.body).append(resize_iframe);
1026     $(resize_iframe[0].contentWindow).bind('resize', onBodyResize);
1027
1028     updateToolbarAnchorState();
1029     loadDiffState();
1030   });
1031
1032   function handleReviewFormLoad() {
1033     var form = $('#reviewform').contents().find('form')[0];
1034     form.addEventListener('submit', eraseDraftComments);
1035   }
1036   
1037   function eraseDraftComments() {
1038     g_draftCommentSaver.erase();
1039   }
1040
1041   function loadDiffState() {
1042     var diffstate = localStorage.getItem('code-review-diffstate');
1043     if (diffstate != 'sidebyside' && diffstate != 'unified')
1044       return;
1045
1046     convertAllFileDiffs(diffstate, $('.FileDiff'));
1047   }
1048
1049   function isDiffSideBySide(file_diff) {
1050     return diffState(file_diff) == 'sidebyside';
1051   }
1052
1053   function diffState(file_diff) {
1054     var diff_state = $(file_diff).attr('data-diffstate');
1055     return diff_state || 'unified';
1056   }
1057
1058   function unifyLine(line, from, to, contents, classNames, attributes, id) {
1059     var new_line = unifiedLine(from, to, contents, false, classNames, attributes);
1060     var old_line = $(line);
1061     if (!old_line.hasClass('LineContainer'))
1062       old_line = old_line.parents('.LineContainer');
1063
1064     var comments = commentsToTransferFor($(document.getElementById(id)));
1065     old_line.after(comments);
1066     old_line.replaceWith(new_line);
1067   }
1068
1069   function updateDiffLinkVisibility(file_diff) {
1070     if (diffState(file_diff) == 'unified') {
1071       $('.side-by-side-link', file_diff).show();
1072       $('.unify-link', file_diff).hide();
1073     } else {
1074       $('.side-by-side-link', file_diff).hide();
1075       $('.unify-link', file_diff).show();
1076     }
1077   }
1078
1079   function convertAllFileDiffs(diff_type, file_diffs) {
1080     file_diffs.each(function() {
1081       convertFileDiff(diff_type, this);
1082     });
1083   }
1084
1085   function convertFileDiff(diff_type, file_diff) {
1086     if (diffState(file_diff) == diff_type)
1087       return;
1088
1089     $(file_diff).removeClass('sidebyside unified');
1090     $(file_diff).addClass(diff_type);
1091
1092     $(file_diff).attr('data-diffstate', diff_type);
1093     updateDiffLinkVisibility(file_diff);
1094
1095     $('.context', file_diff).each(function() {
1096       convertLine(diff_type, this);
1097     });
1098
1099     $('.shared .Line', file_diff).each(function() {
1100       convertLine(diff_type, this);
1101     });
1102
1103     $('.ExpansionLine', file_diff).each(function() {
1104       convertExpansionLine(diff_type, this);
1105     });
1106   }
1107
1108   function convertLine(diff_type, line) {
1109     var convert_function = diff_type == 'sidebyside' ? sideBySideifyLine : unifyLine;
1110     var from = fromLineNumber(line);
1111     var to = toLineNumber(line);
1112     var contents = $('.text', line).first();
1113     var classNames = classNamesForMovingLine(line);
1114     var attributes = attributesForMovingLine(line);
1115     var id = line.id;
1116     convert_function(line, from, to, contents, classNames, attributes, id)
1117   }
1118
1119   function classNamesForMovingLine(line) {
1120     var classParts = line.className.split(' ');
1121     var classBuffer = [];
1122     for (var i = 0; i < classParts.length; i++) {
1123       var part = classParts[i];
1124       if (part != 'LineContainer' && part != 'Line')
1125         classBuffer.push(part);
1126     }
1127     return classBuffer.join(' ');
1128   }
1129
1130   function attributesForMovingLine(line) {
1131     var attributesBuffer = ['id=' + line.id];
1132     // Make sure to keep all data- attributes.
1133     $(line.attributes).each(function() {
1134       if (this.name.indexOf('data-') == 0)
1135         attributesBuffer.push(this.name + '=' + this.value);
1136     });
1137     return attributesBuffer.join(' ');
1138   }
1139
1140   function sideBySideifyLine(line, from, to, contents, classNames, attributes, id) {
1141     var from_class = '';
1142     var to_class = '';
1143     var from_attributes = '';
1144     var to_attributes = '';
1145     // Clone the contents so we have two copies we can put back in the DOM.
1146     var from_contents = contents.clone(true);
1147     var to_contents = contents;
1148
1149     var container_class = 'LineContainer';
1150     var container_attributes = '';
1151
1152     if (from && !to) { // This is a remove line.
1153       from_class = classNames;
1154       from_attributes = attributes;
1155       to_contents = '';
1156     } else if (to && !from) { // This is an add line.
1157       to_class = classNames;
1158       to_attributes = attributes;
1159       from_contents = '';
1160     } else {
1161       container_attributes = attributes;
1162       container_class += ' Line ' + classNames;
1163     }
1164
1165     var new_line = $('<div ' + container_attributes + ' class="' + container_class + '"></div>');
1166     new_line.append(lineSide('from', from_contents, false, from, from_attributes, from_class));
1167     new_line.append(lineSide('to', to_contents, false, to, to_attributes, to_class));
1168
1169     $(line).replaceWith(new_line);
1170
1171     var line = $(document.getElementById(id));
1172     line.after(commentsToTransferFor(line));
1173   }
1174
1175   function convertExpansionLine(diff_type, line) {
1176     var convert_function = diff_type == 'sidebyside' ? sideBySideExpansionLine : unifiedExpansionLine;
1177     var contents = $('.text', line).first();
1178     var from = fromLineNumber(line);
1179     var to = toLineNumber(line);
1180     var new_line = convert_function(from, to, contents);
1181     $(line).replaceWith(new_line);
1182   }
1183
1184   function commentsToTransferFor(line) {
1185     var fragment = document.createDocumentFragment();
1186
1187     previousCommentsFor(line).each(function() {
1188       fragment.appendChild(this);
1189     });
1190
1191     var active_comments = activeCommentFor(line);
1192     var num_active_comments = active_comments.size();
1193     if (num_active_comments > 0) {
1194       if (num_active_comments > 1)
1195         console.log('ERROR: There is more than one active comment for ' + line.attr('id') + '.');
1196
1197       var parent = active_comments[0].parentNode;
1198       var frozenComment = parent.nextSibling;
1199       fragment.appendChild(parent);
1200       fragment.appendChild(frozenComment);
1201     }
1202
1203     return fragment;
1204   }
1205
1206   function discardComment(comment_block) {
1207     var line_id = comment_block.find('textarea').attr('data-comment-for');
1208     var line = $('#' + line_id)
1209     findCommentBlockFor(line).slideUp('fast', function() {
1210       $(this).remove();
1211       line.removeAttr('data-has-comment');
1212       trimCommentContextToBefore(line, line.attr('data-comment-base-line'));
1213       saveDraftComments();
1214     });
1215   }
1216
1217   function unfreezeComment() {
1218     $(this).prev().show();
1219     $(this).remove();
1220   }
1221
1222   function showFileDiffLinks() {
1223     $('.LinkContainer', this).each(function() { this.style.opacity = 1; });
1224   }
1225
1226   function hideFileDiffLinks() {
1227     $('.LinkContainer', this).each(function() { this.style.opacity = 0; });
1228   }
1229
1230   function handleDiscardComment() {
1231     discardComment($(this).parents('.comment'));
1232   }
1233   
1234   function handleAcceptComment() {
1235     freezeComment($(this).parents('.comment'));
1236     saveDraftComments();
1237   }
1238
1239   $('.FileDiff').live('mouseenter', showFileDiffLinks);
1240   $('.FileDiff').live('mouseleave', hideFileDiffLinks);
1241   $('.side-by-side-link').live('click', handleSideBySideLinkClick);
1242   $('.unify-link').live('click', handleUnifyLinkClick);
1243   $('.ExpandLink').live('click', handleExpandLinkClick);
1244   $('.frozenComment').live('click', unfreezeComment);
1245   $('.comment .discard').live('click', handleDiscardComment);
1246   $('.comment .ok').live('click', handleAcceptComment);
1247
1248   function freezeComment(comment_block) {
1249     var comment_textarea = comment_block.find('textarea');
1250     if (comment_textarea.val().trim() == '') {
1251       discardComment(comment_block);
1252       return;
1253     }
1254     var line_id = comment_textarea.attr('data-comment-for');
1255     var line = $('#' + line_id)
1256     findCommentBlockFor(line).hide().after($('<div class="frozenComment"></div>').text(comment_textarea.val()));
1257   }
1258
1259   function focusOn(node) {
1260     $('.focused').removeClass('focused');
1261     if (node.length == 0)
1262       return;
1263     $(document).scrollTop(node.addClass('focused').position().top - window.innerHeight / 2);
1264   }
1265
1266   function focusNext(filter, direction) {
1267     var focusable_nodes = $('.frozenComment,.previousComment,.DiffBlock').filter(function() {
1268       return ($(this).hasClass('frozenComment') || $(this).hasClass('previousComment') || $('.add,.remove', this).size());
1269     });
1270
1271     var is_backward = direction == DIRECTION.BACKWARD;
1272     var index = focusable_nodes.index($('.focused'));
1273     if (index == -1 && is_backward)
1274       index = focusable_nodes.length;
1275
1276     var offset = is_backward ? -1 : 1;
1277     var end = is_backward ? -1 : focusable_nodes.size();
1278     for (var i = index + offset; i != end; i = i + offset) {
1279       var node = $(focusable_nodes[i]);
1280       if (filter(node)) {
1281         focusOn(node);
1282         return;
1283       }
1284     }
1285   }
1286
1287   var DIRECTION = {FORWARD: 1, BACKWARD: 2};
1288
1289   var kCharCodeForN = 'n'.charCodeAt(0);
1290   var kCharCodeForP = 'p'.charCodeAt(0);
1291   var kCharCodeForJ = 'j'.charCodeAt(0);
1292   var kCharCodeForK = 'k'.charCodeAt(0);
1293
1294   function isComment(node) {
1295     return node.hasClass('frozenComment') || node.hasClass('previousComment');
1296   }
1297   
1298   function isDiffBlock(node) {
1299     return node.hasClass('DiffBlock');
1300   }
1301
1302   $('body').live('keypress', function() {
1303     // FIXME: There's got to be a better way to avoid seeing these keypress
1304     // events.
1305     if (event.target.nodeName == 'TEXTAREA')
1306       return;
1307
1308     switch (event.charCode) {
1309     case kCharCodeForN:
1310       focusNext(isComment, DIRECTION.FORWARD);
1311       break;
1312
1313     case kCharCodeForP:
1314       focusNext(isComment, DIRECTION.BACKWARD);
1315       break;
1316
1317     case kCharCodeForJ:
1318       focusNext(isDiffBlock, DIRECTION.FORWARD);
1319       break;
1320
1321     case kCharCodeForK:
1322       focusNext(isDiffBlock, DIRECTION.BACKWARD);
1323       break;
1324     }
1325   });
1326
1327   function contextLinesFor(comment_base_lines, file_diff) {
1328     var base_lines = comment_base_lines.split(' ');
1329     return $('div[data-comment-base-line]', file_diff).filter(function() {
1330       return $(this).attr('data-comment-base-line').split(' ').some(function(item) {
1331         return base_lines.indexOf(item) != -1;
1332       });
1333     });
1334   }
1335
1336   function numberFrom(line_id) {
1337     return Number(line_id.replace('line', ''));
1338   }
1339
1340   function trimCommentContextToBefore(line, comment_base_line) {
1341     var line_to_trim_to = numberFrom(line.attr('id'));
1342     contextLinesFor(comment_base_line, fileDiffFor(line)).each(function() {
1343       var id = $(this).attr('id');
1344       if (numberFrom(id) > line_to_trim_to)
1345         return;
1346
1347       removeDataCommentBaseLine(this, comment_base_line);
1348       if (!$(this).attr('data-comment-base-line'))
1349         $(this).removeClass('commentContext');
1350     });
1351   }
1352
1353   var drag_select_start_index = -1;
1354
1355   function stopDragSelect() {
1356     $('.selected').removeClass('selected');
1357     drag_select_start_index = -1;
1358   }
1359
1360   function lineOffsetFrom(line, offset) {
1361     var file_diff = line.parents('.FileDiff');
1362     var all_lines = $('.Line', file_diff);
1363     var index = all_lines.index(line);
1364     return $(all_lines[index + offset]);
1365   }
1366
1367   function previousLineFor(line) {
1368     return lineOffsetFrom(line, -1);
1369   }
1370
1371   function nextLineFor(line) {
1372     return lineOffsetFrom(line, 1);
1373   }
1374
1375   $('.lineNumber').live('click', function() {
1376     var line = lineFromLineDescendant($(this));
1377     if (line.hasClass('commentContext'))
1378       trimCommentContextToBefore(previousLineFor(line), line.attr('data-comment-base-line'));
1379   }).live('mousedown', function() {
1380     var line = lineFromLineDescendant($(this));
1381     drag_select_start_index = numberFrom(line.attr('id'));
1382     line.addClass('selected');
1383     event.preventDefault();
1384   });
1385
1386   function selectTo(focus_index) {
1387     var selected = $('.selected').removeClass('selected');
1388     var is_backward = drag_select_start_index > focus_index;
1389     var current_index = is_backward ? focus_index : drag_select_start_index;
1390     var last_index = is_backward ? drag_select_start_index : focus_index;
1391     while (current_index <= last_index) {
1392       $('#line' + current_index).addClass('selected')
1393       current_index++;
1394     }
1395   }
1396
1397   function selectToLineContainer(line_container) {
1398     var line = lineFromLineContainer(line_container);
1399     selectTo(numberFrom(line.attr('id')));
1400   }
1401
1402   $('.LineContainer').live('mouseenter', function() {
1403     if (drag_select_start_index == -1)
1404       return;
1405     selectToLineContainer(this);
1406   }).live('mouseup', function() {
1407     if (drag_select_start_index == -1)
1408       return;
1409
1410     selectToLineContainer(this);
1411
1412     var selected = $('.selected');
1413     var already_has_comment = selected.last().hasClass('commentContext');
1414     selected.addClass('commentContext');
1415
1416     var comment_base_line;
1417     if (already_has_comment)
1418       comment_base_line = selected.last().attr('data-comment-base-line');
1419     else {
1420       var last = lineFromLineDescendant(selected.last());
1421       addCommentFor($(last));
1422       comment_base_line = last.attr('id');
1423     }
1424
1425     selected.each(function() {
1426       addDataCommentBaseLine(this, comment_base_line);
1427     });
1428
1429     saveDraftComments();
1430   });
1431
1432   function addDataCommentBaseLine(line, id) {
1433     var val = $(line).attr('data-comment-base-line');
1434
1435     var parts = val ? val.split(' ') : [];
1436     for (var i = 0; i < parts.length; i++) {
1437       if (parts[i] == id)
1438         return;
1439     }
1440
1441     parts.push(id);
1442     $(line).attr('data-comment-base-line', parts.join(' '));
1443   }
1444
1445   function removeDataCommentBaseLine(line, comment_base_lines) {
1446     var val = $(line).attr('data-comment-base-line');
1447     if (!val)
1448       return;
1449
1450     var base_lines = comment_base_lines.split(' ');
1451     var parts = val.split(' ');
1452     var newVal = [];
1453     for (var i = 0; i < parts.length; i++) {
1454       if (base_lines.indexOf(parts[i]) == -1)
1455         newVal.push(parts[i]);
1456     }
1457
1458     $(line).attr('data-comment-base-line', newVal.join(' '));
1459   }
1460
1461   function lineFromLineDescendant(descendant) {
1462     return descendant.hasClass('Line') ? descendant : descendant.parents('.Line');
1463   }
1464
1465   function lineFromLineContainer(lineContainer) {
1466     var line = $(lineContainer);
1467     if (!line.hasClass('Line'))
1468       line = $('.Line', line);
1469     return line;
1470   }
1471
1472   $('.DiffSection').live('mouseleave', stopDragSelect).live('mouseup', stopDragSelect);
1473
1474   function contextSnippetFor(line, indent) {
1475     var snippets = []
1476     contextLinesFor(line.attr('id'), fileDiffFor(line)).each(function() {
1477       var action = ' ';
1478       if ($(this).hasClass('add'))
1479         action = '+';
1480       else if ($(this).hasClass('remove'))
1481         action = '-';
1482       snippets.push(indent + action + textContentsFor(this));
1483     });
1484     return snippets.join('\n');
1485   }
1486
1487   function fileNameFor(line) {
1488     return fileDiffFor(line).find('h1').text();
1489   }
1490
1491   function indentFor(depth) {
1492     return (new Array(depth + 1)).join('>') + ' ';
1493   }
1494
1495   function snippetFor(line, indent) {
1496     var file_name = fileNameFor(line);
1497     var line_number = line.hasClass('remove') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]);
1498     return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent);
1499   }
1500
1501   function quotePreviousComments(comments) {
1502     var quoted_comments = [];
1503     var depth = comments.size();
1504     comments.each(function() {
1505       var indent = indentFor(depth--);
1506       var text = $(this).children('.content').text();
1507       quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent));
1508     });
1509     return quoted_comments.join('\n');
1510   }
1511
1512   $('#comment_form .winter').live('click', function() {
1513     $('#comment_form').addClass('inactive');
1514   });
1515
1516   function fillInReviewForm() {
1517     var comments_in_context = []
1518     forEachLine(function(line) {
1519       if (line.attr('data-has-comment') != 'true')
1520         return;
1521       var comment = findCommentBlockFor(line).children('textarea').val().trim();
1522       if (comment == '')
1523         return;
1524       var previous_comments = previousCommentsFor(line);
1525       var snippet = snippetFor(line, indentFor(previous_comments.size() + 1));
1526       var quoted_comments = quotePreviousComments(previous_comments);
1527       var comment_with_context = [];
1528       comment_with_context.push(snippet);
1529       if (quoted_comments != '')
1530         comment_with_context.push(quoted_comments);
1531       comment_with_context.push('\n' + comment);
1532       comments_in_context.push(comment_with_context.join('\n'));
1533     });
1534     var comment = $('.overallComments textarea').val().trim();
1535     if (comment != '')
1536       comment += '\n\n';
1537     comment += comments_in_context.join('\n\n');
1538     if (comments_in_context.length > 0)
1539       comment = 'View in context: ' + window.location + '\n\n' + comment;
1540     var review_form = $('#reviewform').contents();
1541     review_form.find('#comment').val(comment);
1542     review_form.find('#flags select').each(function() {
1543       var control = findControlForFlag(this);
1544       if (!control.size())
1545         return;
1546       $(this).attr('selectedIndex', control.attr('selectedIndex'));
1547     });
1548   }
1549
1550   $('#preview_comments').live('click', function() {
1551     fillInReviewForm();
1552     $('#comment_form').removeClass('inactive');
1553   });
1554
1555   $('#post_comments').live('click', function() {
1556     fillInReviewForm();
1557     eraseDraftComments();
1558     $('#reviewform').contents().find('form').submit();
1559   });
1560 })();