code-review.js 69 KB

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