7929806ae9b49cf55d96ff10ed843f22f8ac79eb
[WebKit-https.git] / WebCore / inspector / front-end / SourceFrame.js
1 /*
2  * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.SourceFrame = function(element, addBreakpointDelegate)
27 {
28     this.messages = [];
29     this.breakpoints = [];
30     this._shortcuts = {};
31
32     this.addBreakpointDelegate = addBreakpointDelegate;
33
34     this.element = element || document.createElement("iframe");
35     this.element.addStyleClass("source-view-frame");
36     this.element.setAttribute("viewsource", "true");
37
38     this.element.addEventListener("load", this._loaded.bind(this), false);
39 }
40
41 WebInspector.SourceFrame.prototype = {
42     get executionLine()
43     {
44         return this._executionLine;
45     },
46
47     set executionLine(x)
48     {
49         if (this._executionLine === x)
50             return;
51
52         var previousLine = this._executionLine;
53         this._executionLine = x;
54
55         this._updateExecutionLine(previousLine);
56     },
57
58     get autoSizesToFitContentHeight()
59     {
60         return this._autoSizesToFitContentHeight;
61     },
62
63     set autoSizesToFitContentHeight(x)
64     {
65         if (this._autoSizesToFitContentHeight === x)
66             return;
67
68         this._autoSizesToFitContentHeight = x;
69
70         if (this._autoSizesToFitContentHeight) {
71             this._windowResizeListener = this._windowResized.bind(this);
72             window.addEventListener("resize", this._windowResizeListener, false);
73             this.sizeToFitContentHeight();
74         } else {
75             this.element.style.removeProperty("height");
76             if (this.element.contentDocument)
77                 this.element.contentDocument.body.removeStyleClass("webkit-height-sized-to-fit");
78             window.removeEventListener("resize", this._windowResizeListener, false);
79             delete this._windowResizeListener;
80         }
81     },
82
83     sourceRow: function(lineNumber)
84     {
85         if (!lineNumber || !this.element.contentDocument)
86             return;
87
88         var table = this.element.contentDocument.getElementsByTagName("table")[0];
89         if (!table)
90             return;
91
92         var rows = table.rows;
93
94         // Line numbers are a 1-based index, but the rows collection is 0-based.
95         --lineNumber;
96
97         return rows[lineNumber];
98     },
99
100     lineNumberForSourceRow: function(sourceRow)
101     {
102         // Line numbers are a 1-based index, but the rows collection is 0-based.
103         var lineNumber = 0;
104         while (sourceRow) {
105             ++lineNumber;
106             sourceRow = sourceRow.previousSibling;
107         }
108
109         return lineNumber;
110     },
111
112     revealLine: function(lineNumber)
113     {
114         if (!this._isContentLoaded()) {
115             this._lineNumberToReveal = lineNumber;
116             return;
117         }
118
119         var row = this.sourceRow(lineNumber);
120         if (row)
121             row.scrollIntoViewIfNeeded(true);
122     },
123
124     addBreakpoint: function(breakpoint)
125     {
126         this.breakpoints.push(breakpoint);
127         breakpoint.addEventListener("enabled", this._breakpointEnableChanged, this);
128         breakpoint.addEventListener("disabled", this._breakpointEnableChanged, this);
129         this._addBreakpointToSource(breakpoint);
130     },
131
132     removeBreakpoint: function(breakpoint)
133     {
134         this.breakpoints.remove(breakpoint);
135         breakpoint.removeEventListener("enabled", null, this);
136         breakpoint.removeEventListener("disabled", null, this);
137         this._removeBreakpointFromSource(breakpoint);
138     },
139
140     addMessage: function(msg)
141     {
142         // Don't add the message if there is no message or valid line or if the msg isn't an error or warning.
143         if (!msg.message || msg.line <= 0 || !msg.isErrorOrWarning())
144             return;
145         this.messages.push(msg);
146         this._addMessageToSource(msg);
147     },
148
149     clearMessages: function()
150     {
151         this.messages = [];
152
153         if (!this.element.contentDocument)
154             return;
155
156         var bubbles = this.element.contentDocument.querySelectorAll(".webkit-html-message-bubble");
157         if (!bubbles)
158             return;
159
160         for (var i = 0; i < bubbles.length; ++i) {
161             var bubble = bubbles[i];
162             bubble.parentNode.removeChild(bubble);
163         }
164     },
165
166     sizeToFitContentHeight: function()
167     {
168         if (this.element.contentDocument) {
169             this.element.style.setProperty("height", this.element.contentDocument.body.offsetHeight + "px");
170             this.element.contentDocument.body.addStyleClass("webkit-height-sized-to-fit");
171         }
172     },
173
174     _highlightLineEnds: function(event)
175     {
176         event.target.parentNode.removeStyleClass("webkit-highlighted-line");
177     },
178
179     highlightLine: function(lineNumber)
180     {
181         if (!this._isContentLoaded()) {
182             this._lineNumberToHighlight = lineNumber;
183             return;
184         }
185
186         var sourceRow = this.sourceRow(lineNumber);
187         if (!sourceRow)
188             return;
189         var line = sourceRow.getElementsByClassName('webkit-line-content')[0];
190         // Trick to reset the animation if the user clicks on the same link
191         // Using a timeout to avoid coalesced style updates
192         line.style.setProperty("-webkit-animation-name", "none");
193         setTimeout(function () {
194             line.style.removeProperty("-webkit-animation-name");
195             sourceRow.addStyleClass("webkit-highlighted-line");
196         }, 0);
197     },
198
199     _loaded: function()
200     {
201         WebInspector.addMainEventListeners(this.element.contentDocument);
202         this.element.contentDocument.addEventListener("contextmenu", this._documentContextMenu.bind(this), true);
203         this.element.contentDocument.addEventListener("mousedown", this._documentMouseDown.bind(this), true);
204         this.element.contentDocument.addEventListener("keydown", this._documentKeyDown.bind(this), true);
205         this.element.contentDocument.addEventListener("keyup", WebInspector.documentKeyUp.bind(WebInspector), true);
206         this.element.contentDocument.addEventListener("webkitAnimationEnd", this._highlightLineEnds.bind(this), false);
207
208         // Register 'eval' shortcut.
209         var isMac = InspectorController.platform().indexOf("mac-") === 0;
210         var platformSpecificModifier = isMac ? WebInspector.KeyboardShortcut.Modifiers.Meta : WebInspector.KeyboardShortcut.Modifiers.Ctrl;
211         var shortcut = WebInspector.KeyboardShortcut.makeKey(69 /* 'E' */, platformSpecificModifier | WebInspector.KeyboardShortcut.Modifiers.Shift);
212         this._shortcuts[shortcut] = this._evalSelectionInCallFrame.bind(this);
213
214         var headElement = this.element.contentDocument.getElementsByTagName("head")[0];
215         if (!headElement) {
216             headElement = this.element.contentDocument.createElement("head");
217             this.element.contentDocument.documentElement.insertBefore(headElement, this.element.contentDocument.documentElement.firstChild);
218         }
219
220         var styleElement = this.element.contentDocument.createElement("style");
221         headElement.appendChild(styleElement);
222
223         // Add these style rules here since they are specific to the Inspector. They also behave oddly and not
224         // all properties apply if added to view-source.css (becuase it is a user agent sheet.)
225         var styleText = ".webkit-line-number { background-repeat: no-repeat; background-position: right 1px; }\n";
226         styleText += ".webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(program-counter); }\n";
227
228         styleText += ".webkit-breakpoint .webkit-line-number { color: white; background-image: -webkit-canvas(breakpoint); }\n";
229         styleText += ".webkit-breakpoint-disabled .webkit-line-number { color: white; background-image: -webkit-canvas(breakpoint-disabled); }\n";
230         styleText += ".webkit-breakpoint.webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(breakpoint-program-counter); }\n";
231         styleText += ".webkit-breakpoint-disabled.webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(breakpoint-disabled-program-counter); }\n";
232
233         styleText += ".webkit-breakpoint.webkit-breakpoint-conditional .webkit-line-number { color: white; background-image: -webkit-canvas(breakpoint-conditional); }\n";
234         styleText += ".webkit-breakpoint-disabled.webkit-breakpoint-conditional .webkit-line-number { color: white; background-image: -webkit-canvas(breakpoint-disabled-conditional); }\n";
235         styleText += ".webkit-breakpoint.webkit-breakpoint-conditional.webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(breakpoint-conditional-program-counter); }\n";
236         styleText += ".webkit-breakpoint-disabled.webkit-breakpoint-conditional.webkit-execution-line .webkit-line-number { color: transparent; background-image: -webkit-canvas(breakpoint-disabled-conditional-program-counter); }\n";
237
238         styleText += ".webkit-execution-line .webkit-line-content { background-color: rgb(171, 191, 254); outline: 1px solid rgb(64, 115, 244); }\n";
239         styleText += ".webkit-height-sized-to-fit { overflow-y: hidden }\n";
240         styleText += ".webkit-line-content { background-color: white; }\n";
241         styleText += "@-webkit-keyframes fadeout {from {background-color: rgb(255, 255, 120);} to { background-color: white;}}\n";
242         styleText += ".webkit-highlighted-line .webkit-line-content { background-color: rgb(255, 255, 120); -webkit-animation: 'fadeout' 2s 500ms}\n";
243         styleText += ".webkit-javascript-comment { color: rgb(0, 116, 0); }\n";
244         styleText += ".webkit-javascript-keyword { color: rgb(170, 13, 145); }\n";
245         styleText += ".webkit-javascript-number { color: rgb(28, 0, 207); }\n";
246         styleText += ".webkit-javascript-string, .webkit-javascript-regexp { color: rgb(196, 26, 22); }\n";
247
248         styleText += ".webkit-css-comment { color: rgb(0, 116, 0); }\n";
249         styleText += ".webkit-css-string, .webkit-css-keyword, .webkit-css-unit { color: rgb(7, 144, 154); }\n";
250         styleText += ".webkit-css-number { color: rgb(50, 0, 255); }\n";
251         styleText += ".webkit-css-property, .webkit-css-at-rule { color: rgb(200, 0, 0); }\n";
252         styleText += ".webkit-css-url { color: rgb(0, 0, 0); }\n";
253         styleText += ".webkit-css-selector { color: rgb(0, 0, 0); }\n";
254         styleText += ".webkit-css-pseudo-class { color: rgb(128, 128, 128); }\n";
255
256         // TODO: Move these styles into inspector.css once https://bugs.webkit.org/show_bug.cgi?id=28913 is fixed and popup moved into the top frame.
257         styleText += ".popup-content { position: absolute; z-index: 10000; padding: 4px; background-color: rgb(203, 226, 255); -webkit-border-radius: 7px; border: 2px solid rgb(169, 172, 203); }";
258         styleText += ".popup-glasspane { position: absolute; top: 0; left: 0; height: 100%; width: 100%; opacity: 0; z-index: 9900; }";
259         styleText += ".popup-message { background-color: transparent; font-family: Lucida Grande, sans-serif; font-weight: normal; font-size: 11px; text-align: left; text-shadow: none; color: rgb(85, 85, 85); cursor: default; margin: 0 0 2px 0; }";
260         styleText += ".popup-content.breakpoint-condition { width: 90%; }";
261         styleText += ".popup-content input#bp-condition { font-family: monospace; margin: 0; border: 1px inset rgb(190, 190, 190) !important; width: 100%; box-shadow: none !important; outline: none !important; -webkit-user-modify: read-write; }";
262         // This class is already in inspector.css
263         styleText += ".hidden { display: none !important; }";
264
265         styleElement.textContent = styleText;
266
267         this._needsProgramCounterImage = true;
268         this._needsBreakpointImages = true;
269
270         this.element.contentWindow.Element.prototype.addStyleClass = Element.prototype.addStyleClass;
271         this.element.contentWindow.Element.prototype.removeStyleClass = Element.prototype.removeStyleClass;
272         this.element.contentWindow.Element.prototype.positionAt = Element.prototype.positionAt;
273         this.element.contentWindow.Element.prototype.removeMatchingStyleClasses = Element.prototype.removeMatchingStyleClasses;
274         this.element.contentWindow.Element.prototype.hasStyleClass = Element.prototype.hasStyleClass;
275         this.element.contentWindow.Element.prototype.pageOffsetRelativeToWindow = Element.prototype.pageOffsetRelativeToWindow;
276         this.element.contentWindow.Element.prototype.__defineGetter__("totalOffsetLeft", Element.prototype.__lookupGetter__("totalOffsetLeft"));
277         this.element.contentWindow.Element.prototype.__defineGetter__("totalOffsetTop", Element.prototype.__lookupGetter__("totalOffsetTop"));
278         this.element.contentWindow.Node.prototype.enclosingNodeOrSelfWithNodeName = Node.prototype.enclosingNodeOrSelfWithNodeName;
279         this.element.contentWindow.Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = Node.prototype.enclosingNodeOrSelfWithNodeNameInArray;
280
281         this._addExistingMessagesToSource();
282         this._addExistingBreakpointsToSource();
283         this._updateExecutionLine();
284         if (this._executionLine)
285             this.revealLine(this._executionLine);
286
287         if (this.autoSizesToFitContentHeight)
288             this.sizeToFitContentHeight();
289
290         if (this._lineNumberToReveal) {
291             this.revealLine(this._lineNumberToReveal);
292             delete this._lineNumberToReveal;
293         }
294
295         if (this._lineNumberToHighlight) {
296             this.highlightLine(this._lineNumberToHighlight);
297             delete this._lineNumberToHighlight;
298         }
299
300         this.dispatchEventToListeners("content loaded");
301     },
302
303     _isContentLoaded: function() {
304         var doc = this.element.contentDocument;
305         return doc && doc.getElementsByTagName("table")[0];
306     },
307
308     _windowResized: function(event)
309     {
310         if (!this._autoSizesToFitContentHeight)
311             return;
312         this.sizeToFitContentHeight();
313     },
314
315     _documentContextMenu: function(event)
316     {
317         if (!event.target.hasStyleClass("webkit-line-number"))
318             return;
319         var sourceRow = event.target.enclosingNodeOrSelfWithNodeName("tr");
320         if (!sourceRow._breakpointObject && this.addBreakpointDelegate)
321             this.addBreakpointDelegate(this.lineNumberForSourceRow(sourceRow));
322
323         var breakpoint = sourceRow._breakpointObject;
324         if (!breakpoint)
325             return;
326
327         this._editBreakpointCondition(event.target, sourceRow, breakpoint);
328         event.preventDefault();
329     },
330
331     _documentMouseDown: function(event)
332     {
333         if (!event.target.hasStyleClass("webkit-line-number"))
334             return;
335         if (event.button != 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
336             return;
337         var sourceRow = event.target.enclosingNodeOrSelfWithNodeName("tr");
338         if (sourceRow._breakpointObject && sourceRow._breakpointObject.enabled)
339             sourceRow._breakpointObject.enabled = false;
340         else if (sourceRow._breakpointObject)
341             WebInspector.panels.scripts.removeBreakpoint(sourceRow._breakpointObject);
342         else if (this.addBreakpointDelegate)
343             this.addBreakpointDelegate(this.lineNumberForSourceRow(sourceRow));
344     },
345
346     _editBreakpointCondition: function(eventTarget, sourceRow, breakpoint)
347     {
348         // TODO: Migrate the popup to the top-level document and remove the blur listener from conditionElement once https://bugs.webkit.org/show_bug.cgi?id=28913 is fixed.
349         var popupDocument = this.element.contentDocument;
350         this._showBreakpointConditionPopup(eventTarget, breakpoint.line, popupDocument);
351
352         function committed(element, newText)
353         {
354             breakpoint.condition = newText;
355             if (breakpoint.condition)
356                 sourceRow.addStyleClass("webkit-breakpoint-conditional");
357             else
358                 sourceRow.removeStyleClass("webkit-breakpoint-conditional");
359             dismissed.call(this);
360         }
361
362         function dismissed()
363         {
364             this._popup.hide();
365             delete this._conditionEditorElement;
366         }
367
368         var dismissedHandler = dismissed.bind(this);
369         this._conditionEditorElement.addEventListener("blur", dismissedHandler, false);
370
371         WebInspector.startEditing(this._conditionEditorElement, committed.bind(this), dismissedHandler);
372         this._conditionEditorElement.value = breakpoint.condition;
373         this._conditionEditorElement.select();
374     },
375
376     _showBreakpointConditionPopup: function(clickedElement, lineNumber, popupDocument)
377     {
378         var popupContentElement = this._createPopupElement(lineNumber, popupDocument);
379         var lineElement = clickedElement.enclosingNodeOrSelfWithNodeName("td").nextSibling;
380         if (this._popup) {
381             this._popup.hide();
382             this._popup.element = popupContentElement;
383         } else {
384             this._popup = new WebInspector.Popup(popupContentElement);
385             this._popup.autoHide = true;
386         }
387         this._popup.anchor = lineElement;
388         this._popup.show();
389     },
390
391     _createPopupElement: function(lineNumber, popupDocument)
392     {
393         var popupContentElement = popupDocument.createElement("div");
394         popupContentElement.className = "popup-content breakpoint-condition";
395
396         var labelElement = document.createElement("label");
397         labelElement.className = "popup-message";
398         labelElement.htmlFor = "bp-condition";
399         labelElement.appendChild(document.createTextNode(WebInspector.UIString("The breakpoint on line %d will stop only if this expression is true:", lineNumber)));
400         popupContentElement.appendChild(labelElement);
401
402         var editorElement = document.createElement("input");
403         editorElement.id = "bp-condition";
404         editorElement.type = "text"
405         popupContentElement.appendChild(editorElement);
406         this._conditionEditorElement = editorElement;
407
408         return popupContentElement;
409     },
410
411     _documentKeyDown: function(event)
412     {
413         var shortcut = WebInspector.KeyboardShortcut.makeKeyFromEvent(event);
414         var handler = this._shortcuts[shortcut];
415         if (handler) {
416             handler(event);
417             event.preventDefault();
418         } else {
419             WebInspector.documentKeyDown(event);
420         }
421     },
422
423     _evalSelectionInCallFrame: function(event)
424     {
425         if (!WebInspector.panels.scripts || !WebInspector.panels.scripts.paused)
426             return;
427
428         var selection = this.element.contentWindow.getSelection();
429         if (!selection.rangeCount)
430             return;
431
432         var expression = selection.getRangeAt(0).toString().trimWhitespace();
433         WebInspector.panels.scripts.evaluateInSelectedCallFrame(expression, false, function(result, exception) {
434             WebInspector.showConsole();
435             var commandMessage = new WebInspector.ConsoleCommand(expression);
436             WebInspector.console.addMessage(commandMessage);
437             WebInspector.console.addMessage(new WebInspector.ConsoleCommandResult(result, exception, commandMessage));
438         });
439     },
440
441     _breakpointEnableChanged: function(event)
442     {
443         var breakpoint = event.target;
444         var sourceRow = this.sourceRow(breakpoint.line);
445         if (!sourceRow)
446             return;
447
448         sourceRow.addStyleClass("webkit-breakpoint");
449
450         if (breakpoint.enabled)
451             sourceRow.removeStyleClass("webkit-breakpoint-disabled");
452         else
453             sourceRow.addStyleClass("webkit-breakpoint-disabled");
454     },
455
456     _updateExecutionLine: function(previousLine)
457     {
458         if (previousLine) {
459             var sourceRow = this.sourceRow(previousLine);
460             if (sourceRow)
461                 sourceRow.removeStyleClass("webkit-execution-line");
462         }
463
464         if (!this._executionLine)
465             return;
466
467         this._drawProgramCounterImageIfNeeded();
468
469         var sourceRow = this.sourceRow(this._executionLine);
470         if (sourceRow)
471             sourceRow.addStyleClass("webkit-execution-line");
472     },
473
474     _addExistingBreakpointsToSource: function()
475     {
476         var length = this.breakpoints.length;
477         for (var i = 0; i < length; ++i)
478             this._addBreakpointToSource(this.breakpoints[i]);
479     },
480
481     _addBreakpointToSource: function(breakpoint)
482     {
483         var sourceRow = this.sourceRow(breakpoint.line);
484         if (!sourceRow)
485             return;
486
487         breakpoint.sourceText = sourceRow.getElementsByClassName('webkit-line-content')[0].textContent;
488
489         this._drawBreakpointImagesIfNeeded();
490
491         sourceRow._breakpointObject = breakpoint;
492
493         sourceRow.addStyleClass("webkit-breakpoint");
494         if (!breakpoint.enabled)
495             sourceRow.addStyleClass("webkit-breakpoint-disabled");
496         if (breakpoint.condition)
497             sourceRow.addStyleClass("webkit-breakpoint-conditional");
498     },
499
500     _removeBreakpointFromSource: function(breakpoint)
501     {
502         var sourceRow = this.sourceRow(breakpoint.line);
503         if (!sourceRow)
504             return;
505
506         delete sourceRow._breakpointObject;
507
508         sourceRow.removeStyleClass("webkit-breakpoint");
509         sourceRow.removeStyleClass("webkit-breakpoint-disabled");
510         sourceRow.removeStyleClass("webkit-breakpoint-conditional");
511     },
512
513     _incrementMessageRepeatCount: function(msg, repeatDelta)
514     {
515         if (!msg._resourceMessageLineElement)
516             return;
517
518         if (!msg._resourceMessageRepeatCountElement) {
519             var repeatedElement = document.createElement("span");
520             msg._resourceMessageLineElement.appendChild(repeatedElement);
521             msg._resourceMessageRepeatCountElement = repeatedElement;
522         }
523
524         msg.repeatCount += repeatDelta;
525         msg._resourceMessageRepeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", msg.repeatCount);
526     },
527
528     _addExistingMessagesToSource: function()
529     {
530         var length = this.messages.length;
531         for (var i = 0; i < length; ++i)
532             this._addMessageToSource(this.messages[i]);
533     },
534
535     _addMessageToSource: function(msg)
536     {
537         var row = this.sourceRow(msg.line);
538         if (!row)
539             return;
540
541         var cell = row.cells[1];
542         if (!cell)
543             return;
544
545         var messageBubbleElement = cell.lastChild;
546         if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
547             messageBubbleElement = this.element.contentDocument.createElement("div");
548             messageBubbleElement.className = "webkit-html-message-bubble";
549             cell.appendChild(messageBubbleElement);
550         }
551
552         if (!row.messages)
553             row.messages = [];
554
555         for (var i = 0; i < row.messages.length; ++i) {
556             if (row.messages[i].isEqual(msg, true)) {
557                 this._incrementMessageRepeatCount(row.messages[i], msg.repeatDelta);
558                 return;
559             }
560         }
561
562         row.messages.push(msg);
563
564         var imageURL;
565         switch (msg.level) {
566             case WebInspector.ConsoleMessage.MessageLevel.Error:
567                 messageBubbleElement.addStyleClass("webkit-html-error-message");
568                 imageURL = "Images/errorIcon.png";
569                 break;
570             case WebInspector.ConsoleMessage.MessageLevel.Warning:
571                 messageBubbleElement.addStyleClass("webkit-html-warning-message");
572                 imageURL = "Images/warningIcon.png";
573                 break;
574         }
575
576         var messageLineElement = this.element.contentDocument.createElement("div");
577         messageLineElement.className = "webkit-html-message-line";
578         messageBubbleElement.appendChild(messageLineElement);
579
580         // Create the image element in the Inspector's document so we can use relative image URLs.
581         var image = document.createElement("img");
582         image.src = imageURL;
583         image.className = "webkit-html-message-icon";
584
585         // Adopt the image element since it wasn't created in element's contentDocument.
586         image = this.element.contentDocument.adoptNode(image);
587         messageLineElement.appendChild(image);
588         messageLineElement.appendChild(this.element.contentDocument.createTextNode(msg.message));
589
590         msg._resourceMessageLineElement = messageLineElement;
591     },
592
593     _drawProgramCounterInContext: function(ctx, glow)
594     {
595         if (glow)
596             ctx.save();
597
598         ctx.beginPath();
599         ctx.moveTo(17, 2);
600         ctx.lineTo(19, 2);
601         ctx.lineTo(19, 0);
602         ctx.lineTo(21, 0);
603         ctx.lineTo(26, 5.5);
604         ctx.lineTo(21, 11);
605         ctx.lineTo(19, 11);
606         ctx.lineTo(19, 9);
607         ctx.lineTo(17, 9);
608         ctx.closePath();
609         ctx.fillStyle = "rgb(142, 5, 4)";
610
611         if (glow) {
612             ctx.shadowBlur = 4;
613             ctx.shadowColor = "rgb(255, 255, 255)";
614             ctx.shadowOffsetX = -1;
615             ctx.shadowOffsetY = 0;
616         }
617
618         ctx.fill();
619         ctx.fill(); // Fill twice to get a good shadow and darker anti-aliased pixels.
620
621         if (glow)
622             ctx.restore();
623     },
624
625     _drawProgramCounterImageIfNeeded: function()
626     {
627         if (!this._needsProgramCounterImage || !this.element.contentDocument)
628             return;
629
630         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "program-counter", 26, 11);
631         ctx.clearRect(0, 0, 26, 11);
632         this._drawProgramCounterInContext(ctx, true);
633
634         delete this._needsProgramCounterImage;
635     },
636
637     _drawBreakpointImagesIfNeeded: function(conditional)
638     {
639         if (!this._needsBreakpointImages || !this.element.contentDocument)
640             return;
641
642         function drawBreakpoint(ctx, disabled, conditional)
643         {
644             ctx.beginPath();
645             ctx.moveTo(0, 2);
646             ctx.lineTo(2, 0);
647             ctx.lineTo(21, 0);
648             ctx.lineTo(26, 5.5);
649             ctx.lineTo(21, 11);
650             ctx.lineTo(2, 11);
651             ctx.lineTo(0, 9);
652             ctx.closePath();
653             ctx.fillStyle = conditional ? "rgb(217, 142, 1)" : "rgb(1, 142, 217)";
654             ctx.strokeStyle = conditional ? "rgb(205, 103, 0)" : "rgb(0, 103, 205)";
655             ctx.lineWidth = 3;
656             ctx.fill();
657             ctx.save();
658             ctx.clip();
659             ctx.stroke();
660             ctx.restore();
661
662             if (!disabled)
663                 return;
664
665             ctx.save();
666             ctx.globalCompositeOperation = "destination-out";
667             ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
668             ctx.fillRect(0, 0, 26, 11);
669             ctx.restore();
670         }
671
672
673         // Unconditional breakpoints.
674
675         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint", 26, 11);
676         ctx.clearRect(0, 0, 26, 11);
677         drawBreakpoint(ctx);
678
679         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-program-counter", 26, 11);
680         ctx.clearRect(0, 0, 26, 11);
681         drawBreakpoint(ctx);
682         ctx.clearRect(20, 0, 6, 11);
683         this._drawProgramCounterInContext(ctx, true);
684
685         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-disabled", 26, 11);
686         ctx.clearRect(0, 0, 26, 11);
687         drawBreakpoint(ctx, true);
688
689         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-disabled-program-counter", 26, 11);
690         ctx.clearRect(0, 0, 26, 11);
691         drawBreakpoint(ctx, true);
692         ctx.clearRect(20, 0, 6, 11);
693         this._drawProgramCounterInContext(ctx, true);
694
695
696         // Conditional breakpoints.
697
698         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-conditional", 26, 11);
699         ctx.clearRect(0, 0, 26, 11);
700         drawBreakpoint(ctx, false, true);
701
702         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-conditional-program-counter", 26, 11);
703         ctx.clearRect(0, 0, 26, 11);
704         drawBreakpoint(ctx, false, true);
705         ctx.clearRect(20, 0, 6, 11);
706         this._drawProgramCounterInContext(ctx, true);
707
708         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-disabled-conditional", 26, 11);
709         ctx.clearRect(0, 0, 26, 11);
710         drawBreakpoint(ctx, true, true);
711
712         var ctx = this.element.contentDocument.getCSSCanvasContext("2d", "breakpoint-disabled-conditional-program-counter", 26, 11);
713         ctx.clearRect(0, 0, 26, 11);
714         drawBreakpoint(ctx, true, true);
715         ctx.clearRect(20, 0, 6, 11);
716         this._drawProgramCounterInContext(ctx, true);
717
718         delete this._needsBreakpointImages;
719     },
720
721     syntaxHighlightJavascript: function()
722     {
723         var table = this.element.contentDocument.getElementsByTagName("table")[0];
724         if (!table)
725             return;
726
727         var jsSyntaxHighlighter = new WebInspector.JavaScriptSourceSyntaxHighlighter(table, this);
728         jsSyntaxHighlighter.process();
729     },
730
731     syntaxHighlightCSS: function()
732     {
733         var table = this.element.contentDocument.getElementsByTagName("table")[0];
734         if (!table)
735             return;
736
737         var cssSyntaxHighlighter = new WebInspector.CSSSourceSyntaxHighligher(table, this);
738         cssSyntaxHighlighter.process();
739     }
740 }
741
742 WebInspector.SourceFrame.prototype.__proto__ = WebInspector.Object.prototype;
743
744 WebInspector.SourceSyntaxHighligher = function(table, sourceFrame)
745 {
746     this.table = table;
747     this.sourceFrame = sourceFrame;
748 }
749
750 WebInspector.SourceSyntaxHighligher.prototype = {
751     createSpan: function(content, className)
752     {
753         var span = document.createElement("span");
754         span.className = className;
755         span.appendChild(document.createTextNode(content));
756         return span;
757     },
758
759     generateFinder: function(regex, matchNumber, className)
760     {
761         return function(str) {
762             var match = regex.exec(str);
763             if (!match)
764                 return null;
765             this.previousMatchLength = match[matchNumber].length;
766             return this.createSpan(match[matchNumber], className);
767         };
768     },
769
770     process: function()
771     {
772         // Split up the work into chunks so we don't block the
773         // UI thread while processing.
774
775         var i = 0;
776         var rows = this.table.rows;
777         var rowsLength = rows.length;
778         var previousCell = null;
779         const linesPerChunk = 10;
780
781         function processChunk()
782         {
783             for (var end = Math.min(i + linesPerChunk, rowsLength); i < end; ++i) {
784                 var row = rows[i];
785                 if (!row)
786                     continue;
787                 var cell = row.cells[1];
788                 if (!cell)
789                     continue;
790                 this.syntaxHighlightLine(cell, previousCell);
791                 if (i < (end - 1))
792                     this.deleteContinueFlags(previousCell);
793                 previousCell = cell;
794             }
795
796             if (i >= rowsLength && processChunkInterval) {
797                 this.deleteContinueFlags(previousCell);
798                 delete this.previousMatchLength;
799                 clearInterval(processChunkInterval);
800
801                 this.sourceFrame.dispatchEventToListeners("syntax highlighting complete");
802             }
803         }
804
805         var boundProcessChunk = processChunk.bind(this);
806         var processChunkInterval = setInterval(boundProcessChunk, 25);
807         boundProcessChunk();
808     }
809 }
810
811 WebInspector.CSSSourceSyntaxHighligher = function(table, sourceFrame) {
812     WebInspector.SourceSyntaxHighligher.call(this, table, sourceFrame);
813
814     this.findNumber = this.generateFinder(/^((-?(\d+|\d*\.\d+))|^(#[a-fA-F0-9]{3,6}))(?:\D|$)/, 1, "webkit-css-number");
815     this.findUnits = this.generateFinder(/^(px|em|pt|in|cm|mm|pc|ex)(?:\W|$)/, 1, "webkit-css-unit");
816     this.findKeyword = this.generateFinder(/^(rgba?|hsla?)(?:\W|$)/, 1, "webkit-css-keyword");
817     this.findSingleLineString = this.generateFinder(/^"(?:[^"\\]|\\.)*"|^'([^'\\]|\\.)*'/, 0, "webkit-css-string"); // " this quote keeps Xcode happy
818     this.findSingleLineComment = this.generateFinder(/^\/\*.*?\*\//, 0, "webkit-css-comment");
819     this.findMultilineCommentStart = this.generateFinder(/^\/\*.*$/, 0, "webkit-css-comment");
820     this.findMultilineCommentEnd = this.generateFinder(/^.*?\*\//, 0, "webkit-css-comment");
821     this.findSelector = this.generateFinder(/^([#\.]?[_a-zA-Z].*?)(?:\W|$)/, 1, "webkit-css-selector");
822     this.findProperty = this.generateFinder(/^(-?[_a-z0-9][_a-z0-9-]*\s*)(?:\:)/, 1, "webkit-css-property");
823     this.findGenericIdent = this.generateFinder(/^([@-]?[_a-z0-9][_a-z0-9-]*)(?:\W|$)/, 1, "webkit-css-string");
824 }
825
826 WebInspector.CSSSourceSyntaxHighligher.prototype = {
827     deleteContinueFlags: function(cell)
828     {
829         if (!cell)
830             return;
831         delete cell._commentContinues;
832         delete cell._inSelector;
833     },
834
835     findPseudoClass: function(str)
836     {
837         var match = /^(::?)([_a-z0-9][_a-z0-9-]*)/.exec(str);
838         if (!match)
839             return null;
840         this.previousMatchLength = match[0].length;
841         var span = document.createElement("span");
842         span.appendChild(document.createTextNode(match[1]));
843         span.appendChild(this.createSpan(match[2], "webkit-css-pseudo-class"));
844         return span;
845     },
846
847     findURL: function(str)
848     {
849         var match = /^(?:local|url)\(([^\)]*?)\)/.exec(str);
850         if (!match)
851             return null;
852         this.previousMatchLength = match[0].length;
853         var innerUrlSpan = this.createSpan(match[1], "webkit-css-url");
854         var outerSpan = document.createElement("span");
855         outerSpan.appendChild(this.createSpan("url", "webkit-css-keyword"));
856         outerSpan.appendChild(document.createTextNode("("));
857         outerSpan.appendChild(innerUrlSpan);
858         outerSpan.appendChild(document.createTextNode(")"));
859         return outerSpan;
860     },
861
862     findAtRule: function(str)
863     {
864         var match = /^@[_a-z0-9][_a-z0-9-]*(?:\W|$)/.exec(str);
865         if (!match)
866             return null;
867         this.previousMatchLength = match[0].length;
868         return this.createSpan(match[0], "webkit-css-at-rule");
869     },
870
871     syntaxHighlightLine: function(line, prevLine)
872     {
873         var code = line.textContent;
874         while (line.firstChild)
875             line.removeChild(line.firstChild);
876
877         var token;
878         var tmp = 0;
879         var i = 0;
880         this.previousMatchLength = 0;
881
882         if (prevLine) {
883             if (prevLine._commentContinues) {
884                 if (!(token = this.findMultilineCommentEnd(code))) {
885                     token = this.createSpan(code, "webkit-javascript-comment");
886                     line._commentContinues = true;
887                 }
888             }
889             if (token) {
890                 i += this.previousMatchLength ? this.previousMatchLength : code.length;
891                 tmp = i;
892                 line.appendChild(token);
893             }
894         }
895
896         var inSelector = (prevLine && prevLine._inSelector); // inside a selector, we can now parse properties and values
897         var inAtRuleBlock = (prevLine && prevLine._inAtRuleBlock); // inside an @rule block, but not necessarily inside a selector yet
898         var atRuleStarted = (prevLine && prevLine._atRuleStarted); // we received an @rule, we may stop the @rule at a semicolon or open a block and become inAtRuleBlock
899         var atRuleIsSelector = (prevLine && prevLine._atRuleIsSelector); // when this @rule opens a block it immediately goes into parsing properties and values instead of selectors
900
901         for ( ; i < code.length; ++i) {
902             var codeFragment = code.substr(i);
903             var prevChar = code[i - 1];
904             var currChar = codeFragment[0];
905             token = this.findSingleLineComment(codeFragment);
906             if (!token) {
907                 if ((token = this.findMultilineCommentStart(codeFragment)))
908                     line._commentContinues = true;
909                 else if (currChar === ";" && !inAtRuleBlock)
910                     atRuleStarted = false;
911                 else if (currChar === "}") {
912                     if (inSelector && inAtRuleBlock && atRuleIsSelector) {
913                         inSelector = false;
914                         inAtRuleBlock = false;
915                         atRuleStarted = false;
916                     } else if (inSelector) {
917                         inSelector = false;
918                     } else if (inAtRuleBlock) {
919                         inAtRuleBlock = false;
920                         atRuleStarted = false;
921                     }
922                 } else if (currChar === "{") {
923                     if (!atRuleStarted || inAtRuleBlock) {
924                         inSelector = true;
925                     } else if (!inAtRuleBlock && atRuleIsSelector) {
926                         inAtRuleBlock = true;
927                         inSelector = true;
928                     } else if (!inAtRuleBlock) {
929                         inAtRuleBlock = true;
930                         inSelector = false;
931                     }
932                 } else if (inSelector) {
933                     if (!prevChar || /^\d/.test(prevChar)) {
934                         token = this.findUnits(codeFragment);
935                     } else if (!prevChar || /^\W/.test(prevChar)) {
936                         token = this.findNumber(codeFragment) ||
937                                 this.findKeyword(codeFragment) ||
938                                 this.findURL(codeFragment) ||
939                                 this.findProperty(codeFragment) ||
940                                 this.findAtRule(codeFragment) ||
941                                 this.findGenericIdent(codeFragment) ||
942                                 this.findSingleLineString(codeFragment);
943                     }
944                 } else if (!inSelector) {
945                     if (atRuleStarted && !inAtRuleBlock)
946                         token = this.findURL(codeFragment); // for @import
947                     if (!token) {
948                         token = this.findSelector(codeFragment) ||
949                                 this.findPseudoClass(codeFragment) ||
950                                 this.findAtRule(codeFragment);
951                     }
952                 }
953             }
954
955             if (token) {
956                 if (currChar === '@') {
957                     atRuleStarted = true;
958
959                     // The @font-face at-rule does not contain selectors like other at-rules
960                     // instead it acts as a selector and contains properties and values.
961                     atRuleIsSelector = /font-face/.test(token.textContent);
962                 }
963
964                 if (tmp !== i)
965                     line.appendChild(document.createTextNode(code.substring(tmp, i)));
966                 line.appendChild(token);
967                 i += this.previousMatchLength - 1;
968                 tmp = i + 1;
969             }
970         }
971
972         line._inSelector = inSelector;
973         line._inAtRuleBlock = inAtRuleBlock;
974         line._atRuleStarted = atRuleStarted;
975         line._atRuleIsSelector = atRuleIsSelector;
976
977         if (tmp < code.length)
978             line.appendChild(document.createTextNode(code.substring(tmp, i)));
979     }
980 }
981
982 WebInspector.CSSSourceSyntaxHighligher.prototype.__proto__ = WebInspector.SourceSyntaxHighligher.prototype;
983
984 WebInspector.JavaScriptSourceSyntaxHighlighter = function(table, sourceFrame) {
985     WebInspector.SourceSyntaxHighligher.call(this, table, sourceFrame);
986
987     this.findNumber = this.generateFinder(/^(-?(\d+\.?\d*([eE][+-]\d+)?|0[xX]\h+|Infinity)|NaN)(?:\W|$)/, 1, "webkit-javascript-number");
988     this.findKeyword = this.generateFinder(/^(null|true|false|break|case|catch|const|default|finally|for|instanceof|new|var|continue|function|return|void|delete|if|this|do|while|else|in|switch|throw|try|typeof|with|debugger|class|enum|export|extends|import|super|get|set)(?:\W|$)/, 1, "webkit-javascript-keyword");
989     this.findSingleLineString = this.generateFinder(/^"(?:[^"\\]|\\.)*"|^'([^'\\]|\\.)*'/, 0, "webkit-javascript-string"); // " this quote keeps Xcode happy
990     this.findMultilineCommentStart = this.generateFinder(/^\/\*.*$/, 0, "webkit-javascript-comment");
991     this.findMultilineCommentEnd = this.generateFinder(/^.*?\*\//, 0, "webkit-javascript-comment");
992     this.findMultilineSingleQuoteStringStart = this.generateFinder(/^'(?:[^'\\]|\\.)*\\$/, 0, "webkit-javascript-string");
993     this.findMultilineSingleQuoteStringEnd = this.generateFinder(/^(?:[^'\\]|\\.)*?'/, 0, "webkit-javascript-string");
994     this.findMultilineDoubleQuoteStringStart = this.generateFinder(/^"(?:[^"\\]|\\.)*\\$/, 0, "webkit-javascript-string");
995     this.findMultilineDoubleQuoteStringEnd = this.generateFinder(/^(?:[^"\\]|\\.)*?"/, 0, "webkit-javascript-string");
996     this.findMultilineRegExpEnd = this.generateFinder(/^(?:[^\/\\]|\\.)*?\/([gim]{0,3})/, 0, "webkit-javascript-regexp");
997     this.findSingleLineComment = this.generateFinder(/^\/\/.*|^\/\*.*?\*\//, 0, "webkit-javascript-comment");
998 }
999
1000 WebInspector.JavaScriptSourceSyntaxHighlighter.prototype = {
1001     deleteContinueFlags: function(cell)
1002     {
1003         if (!cell)
1004             return;
1005         delete cell._commentContinues;
1006         delete cell._singleQuoteStringContinues;
1007         delete cell._doubleQuoteStringContinues;
1008         delete cell._regexpContinues;
1009     },
1010
1011     findMultilineRegExpStart: function(str)
1012     {
1013         var match = /^\/(?:[^\/\\]|\\.)*\\$/.exec(str);
1014         if (!match || !/\\|\$|\.[\?\*\+]|[^\|]\|[^\|]/.test(match[0]))
1015             return null;
1016         this.previousMatchLength = match[0].length;
1017         return this.createSpan(match[0], "webkit-javascript-regexp");
1018     },
1019
1020     findSingleLineRegExp: function(str)
1021     {
1022         var match = /^(\/(?:[^\/\\]|\\.)*\/([gim]{0,3}))(.?)/.exec(str);
1023         if (!match || !(match[2].length > 0 || /\\|\$|\.[\?\*\+]|[^\|]\|[^\|]/.test(match[1]) || /\.|;|,/.test(match[3])))
1024             return null;
1025         this.previousMatchLength = match[1].length;
1026         return this.createSpan(match[1], "webkit-javascript-regexp");
1027     },
1028
1029     syntaxHighlightLine: function(line, prevLine)
1030     {
1031         var messageBubble = line.lastChild;
1032         if (messageBubble && messageBubble.nodeType === Node.ELEMENT_NODE && messageBubble.hasStyleClass("webkit-html-message-bubble"))
1033             line.removeChild(messageBubble);
1034         else
1035             messageBubble = null;
1036
1037         var code = line.textContent;
1038
1039         while (line.firstChild)
1040             line.removeChild(line.firstChild);
1041
1042         var token;
1043         var tmp = 0;
1044         var i = 0;
1045         this.previousMatchLength = 0;
1046
1047         if (prevLine) {
1048             if (prevLine._commentContinues) {
1049                 if (!(token = this.findMultilineCommentEnd(code))) {
1050                     token = this.createSpan(code, "webkit-javascript-comment");
1051                     line._commentContinues = true;
1052                 }
1053             } else if (prevLine._singleQuoteStringContinues) {
1054                 if (!(token = this.findMultilineSingleQuoteStringEnd(code))) {
1055                     token = this.createSpan(code, "webkit-javascript-string");
1056                     line._singleQuoteStringContinues = true;
1057                 }
1058             } else if (prevLine._doubleQuoteStringContinues) {
1059                 if (!(token = this.findMultilineDoubleQuoteStringEnd(code))) {
1060                     token = this.createSpan(code, "webkit-javascript-string");
1061                     line._doubleQuoteStringContinues = true;
1062                 }
1063             } else if (prevLine._regexpContinues) {
1064                 if (!(token = this.findMultilineRegExpEnd(code))) {
1065                     token = this.createSpan(code, "webkit-javascript-regexp");
1066                     line._regexpContinues = true;
1067                 }
1068             }
1069             if (token) {
1070                 i += this.previousMatchLength ? this.previousMatchLength : code.length;
1071                 tmp = i;
1072                 line.appendChild(token);
1073             }
1074         }
1075
1076         for ( ; i < code.length; ++i) {
1077             var codeFragment = code.substr(i);
1078             var prevChar = code[i - 1];
1079             token = this.findSingleLineComment(codeFragment);
1080             if (!token) {
1081                 if ((token = this.findMultilineCommentStart(codeFragment)))
1082                     line._commentContinues = true;
1083                 else if (!prevChar || /^\W/.test(prevChar)) {
1084                     token = this.findNumber(codeFragment) ||
1085                             this.findKeyword(codeFragment) ||
1086                             this.findSingleLineString(codeFragment) ||
1087                             this.findSingleLineRegExp(codeFragment);
1088                     if (!token) {
1089                         if (token = this.findMultilineSingleQuoteStringStart(codeFragment))
1090                             line._singleQuoteStringContinues = true;
1091                         else if (token = this.findMultilineDoubleQuoteStringStart(codeFragment))
1092                             line._doubleQuoteStringContinues = true;
1093                         else if (token = this.findMultilineRegExpStart(codeFragment))
1094                             line._regexpContinues = true;
1095                     }
1096                 }
1097             }
1098
1099             if (token) {
1100                 if (tmp !== i)
1101                     line.appendChild(document.createTextNode(code.substring(tmp, i)));
1102                 line.appendChild(token);
1103                 i += this.previousMatchLength - 1;
1104                 tmp = i + 1;
1105             }
1106         }
1107
1108         if (tmp < code.length)
1109             line.appendChild(document.createTextNode(code.substring(tmp, i)));
1110
1111         if (messageBubble)
1112             line.appendChild(messageBubble);
1113     }
1114 }
1115
1116 WebInspector.JavaScriptSourceSyntaxHighlighter.prototype.__proto__ = WebInspector.SourceSyntaxHighligher.prototype;