Reviewed by Adam Roben.
[WebKit-https.git] / WebCore / page / inspector / ConsolePanel.js
1 /*
2  * Copyright (C) 2007, 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  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 WebInspector.ConsolePanel = function()
30 {
31     WebInspector.Panel.call(this);
32
33     this.messages = [];
34
35     this.commandHistory = [];
36     this.commandOffset = 0;
37
38     this.messagesElement = document.createElement("div");
39     this.messagesElement.id = "console-messages";
40     this.messagesElement.addEventListener("selectstart", this.messagesSelectStart.bind(this), true);
41     this.messagesElement.addEventListener("click", this.messagesClicked.bind(this), true);
42     this.element.appendChild(this.messagesElement);
43
44     this.promptElement = document.createElement("div");
45     this.promptElement.id = "console-prompt";
46     this.promptElement.addEventListener("keydown", this.promptKeyDown.bind(this), false);
47     this.promptElement.appendChild(document.createElement("br"));
48     this.messagesElement.appendChild(this.promptElement);
49
50     this.clearButton = document.createElement("button");
51     this.clearButton.title = WebInspector.UIString("Clear");
52     this.clearButton.textContent = WebInspector.UIString("Clear");
53     this.clearButton.addEventListener("click", this.clearButtonClicked.bind(this), false);
54 }
55
56 WebInspector.ConsolePanel.prototype = {
57     get promptText()
58     {
59         return this.promptElement.textContent;
60     },
61
62     set promptText(x)
63     {
64         if (!x) {
65             // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
66             this.promptElement.removeChildren();
67             this.promptElement.appendChild(document.createElement("br"));
68         } else
69             this.promptElement.textContent = x;
70
71         this._moveCaretToEndOfPrompt();
72     },
73
74     show: function()
75     {
76         WebInspector.Panel.prototype.show.call(this);
77         WebInspector.consoleListItem.select();
78
79         this.clearButton.removeStyleClass("hidden");
80         if (!this.clearButton.parentNode)
81             document.getElementById("toolbarButtons").appendChild(this.clearButton);
82
83         WebInspector.currentFocusElement = document.getElementById("main");
84
85         function focusPrompt()
86         {
87             if (!this._caretInsidePrompt())
88                 this._moveCaretToEndOfPrompt();
89         }
90
91         setTimeout(focusPrompt.bind(this), 0);
92     },
93
94     hide: function()
95     {
96         WebInspector.Panel.prototype.hide.call(this);
97         WebInspector.consoleListItem.deselect();
98         this.clearButton.addStyleClass("hidden");
99     },
100
101     addMessage: function(msg)
102     {
103         if (msg.url in WebInspector.resourceURLMap) {
104             msg.resource = WebInspector.resourceURLMap[msg.url];
105             switch (msg.level) {
106                 case WebInspector.ConsoleMessage.MessageLevel.Warning:
107                     ++msg.resource.warnings;
108                     msg.resource.panel.addMessageToSource(msg);
109                     break;
110                 case WebInspector.ConsoleMessage.MessageLevel.Error:
111                     ++msg.resource.errors;
112                     msg.resource.panel.addMessageToSource(msg);
113                     break;
114             }
115         }
116
117         this.messages.push(msg);
118
119         var element = msg.toMessageElement();
120         this.messagesElement.insertBefore(element, this.promptElement);
121         this.promptElement.scrollIntoView(false);
122     },
123
124     clearMessages: function()
125     {
126         for (var i = 0; i < this.messages.length; ++i) {
127             var resource = this.messages[i].resource;
128             if (!resource)
129                 continue;
130             resource.errors = 0;
131             resource.warnings = 0;
132         }
133
134         this.messages = [];
135
136         while (this.messagesElement.firstChild != this.promptElement)
137             this.messagesElement.removeChild(this.messagesElement.firstChild);
138     },
139
140     acceptAutoComplete: function()
141     {
142         if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
143             return false;
144
145         var text = this.autoCompleteElement.textContent;
146         var textNode = document.createTextNode(text);
147         this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
148         delete this.autoCompleteElement;
149
150         var finalSelectionRange = document.createRange();
151         finalSelectionRange.setStart(textNode, text.length);
152         finalSelectionRange.setEnd(textNode, text.length);
153
154         var selection = window.getSelection();
155         selection.removeAllRanges();
156         selection.addRange(finalSelectionRange);
157
158         return true;
159     },
160
161     clearAutoComplete: function(includeTimeout)
162     {
163         if (includeTimeout && "completeTimeout" in this) {
164             clearTimeout(this.completeTimeout);
165             delete this.completeTimeout;
166         }
167
168         if (!this.autoCompleteElement)
169             return;
170
171         if (this.autoCompleteElement.parentNode)
172             this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
173         delete this.autoCompleteElement;
174     },
175
176     autoCompleteSoon: function()
177     {
178         if (!("completeTimeout" in this))
179             this.completeTimeout = setTimeout(this.complete.bind(this, true), 250);
180     },
181
182     complete: function(auto)
183     {
184         this.clearAutoComplete(true);
185
186         var selection = window.getSelection();
187         if (!selection.rangeCount)
188             return;
189
190         var selectionRange = selection.getRangeAt(0);
191         if (!selectionRange.commonAncestorContainer.isDescendant(this.promptElement))
192             return;
193
194         if (auto) {
195             if (!selection.isCollapsed)
196                 return;
197
198             var node = selectionRange.startContainer;
199             if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
200                 return;
201
202             var foundNextText = false;
203             while (node) {
204                 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
205                     if (foundNextText)
206                         return;
207                     foundNextText = true;
208                 }
209
210                 node = node.traverseNextNode(false, this.promptElement);
211             }
212         }
213
214         var wordPrefixRange = this._backwardsRange(" .=:[({;", selectionRange.startContainer, selectionRange.startOffset, this.promptElement);
215
216         var completions = this.completions(wordPrefixRange, auto);
217
218         if (!completions || !completions.length)
219             return;
220
221         var fullWordRange = document.createRange();
222         fullWordRange.setStart(wordPrefixRange.startContainer, wordPrefixRange.startOffset);
223         fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
224
225         if (completions.length === 1 || selection.isCollapsed || auto) {
226             var completionText = completions[0];
227         } else {
228             var currentText = fullWordRange.toString().trimTrailingWhitespace();
229
230             var foundIndex = null;
231             for (var i = 0; i < completions.length; ++i) {
232                 if (completions[i] === currentText)
233                     foundIndex = i;
234             }
235
236             if (foundIndex === null || (foundIndex + 1) >= completions.length)
237                 var completionText = completions[0];
238             else
239                 var completionText = completions[foundIndex + 1];
240         }
241
242         var wordPrefixLength = wordPrefixRange.toString().length;
243
244         fullWordRange.deleteContents();
245
246         var finalSelectionRange = document.createRange();
247
248         if (auto) {
249             var prefixText = completionText.substring(0, wordPrefixLength);
250             var suffixText = completionText.substring(wordPrefixLength);
251
252             var prefixTextNode = document.createTextNode(prefixText);
253             fullWordRange.insertNode(prefixTextNode);           
254
255             this.autoCompleteElement = document.createElement("span");
256             this.autoCompleteElement.className = "auto-complete-text";
257             this.autoCompleteElement.textContent = suffixText;
258
259             prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
260
261             finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
262             finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
263         } else {
264             var completionTextNode = document.createTextNode(completionText);
265             fullWordRange.insertNode(completionTextNode);           
266
267             if (completions.length > 1)
268                 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
269             else
270                 finalSelectionRange.setStart(completionTextNode, completionText.length);
271
272             finalSelectionRange.setEnd(completionTextNode, completionText.length);
273         }
274
275         selection.removeAllRanges();
276         selection.addRange(finalSelectionRange);
277     },
278
279     completions: function(wordRange, bestMatchOnly)
280     {
281         var prefix = wordRange.toString();
282         var expression = this._backwardsRange(" =:({;", wordRange.startContainer, wordRange.startOffset, this.promptElement);
283         var expressionString = expression.toString().replace(/\.+$/, "");
284
285         if (!expressionString && !prefix)
286             return;
287
288         var result = window;
289         if (expressionString) {
290             try {
291                 result = this._evalInInspectedWindow(expressionString);
292             } catch(e) {
293                 return;
294             }
295         }
296
297         var results = [];
298         var properties = Object.sortedProperties(result);
299         for (var i = 0; i < properties.length; ++i) {
300             var property = properties[i];
301             if (property.length < prefix.length)
302                 continue;
303             if (property.indexOf(prefix) !== 0)
304                 continue;
305             results.push(property);
306             if (bestMatchOnly)
307                 break;
308         }
309
310         return results;
311     },
312
313     clearButtonClicked: function()
314     {
315         this.clearMessages();
316     },
317
318     messagesSelectStart: function(event)
319     {
320         if (this._selectionTimeout)
321             clearTimeout(this._selectionTimeout);
322
323         function moveBackIfOutside()
324         {
325             delete this._selectionTimeout;
326             if (this._caretInsidePrompt() || !window.getSelection().isCollapsed)
327                 return;
328             this._moveCaretToEndOfPrompt();
329         }
330
331         this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
332     },
333
334     messagesClicked: function(event)
335     {
336         var link = event.target.firstParentOrSelfWithNodeName("a");
337         if (link && link.representedNode) {
338             WebInspector.updateFocusedNode(link.representedNode);
339             return;
340         }
341
342         var messageElement = event.target.firstParentOrSelfWithClass("console-message");
343         if (!messageElement)
344             return;
345
346         if (!messageElement.message)
347             return;
348
349         var resource = messageElement.message.resource;
350         if (!resource)
351             return;
352
353         if (link && link.hasStyleClass("console-message-url")) {
354             WebInspector.navigateToResource(resource);
355             resource.panel.showSourceLine(item.message.line);
356         }
357
358         event.stopPropagation();
359         event.preventDefault();
360     },
361
362     promptKeyDown: function(event)
363     {
364         switch (event.keyIdentifier) {
365             case "Enter":
366                 this._onEnterPressed(event);
367                 break;
368             case "Up":
369                 this._onUpPressed(event);
370                 break;
371             case "Down":
372                 this._onDownPressed(event);
373                 break;
374             case "U+0009": // Tab
375                 this._onTabPressed(event);
376                 break;
377             case "Right":
378                 if (!this.acceptAutoComplete())
379                     this.autoCompleteSoon();
380                 break;
381             default:
382                 this.clearAutoComplete();
383                 this.autoCompleteSoon();
384                 break;
385         }
386     },
387
388     _backwardsRange: function(stopCharacters, endNode, endOffset, stayWithinElement)
389     {
390         var startNode;
391         var startOffset = 0;
392         var node = endNode;
393
394         while (node) {
395             if (node === stayWithinElement) {
396                 if (!startNode)
397                     startNode = stayWithinElement;
398                 break;
399             }
400
401             if (node.nodeType === Node.TEXT_NODE) {
402                 var start = (node === endNode ? endOffset : node.nodeValue.length);
403                 for (var i = (start - 1); i >= 0; --i) {
404                     var character = node.nodeValue[i];
405                     if (stopCharacters.indexOf(character) !== -1) {
406                         startNode = node;
407                         startOffset = i + 1;
408                         break;
409                     }
410                 }
411             }
412
413             if (startNode)
414                 break;
415
416             node = node.traversePreviousNode();
417         }
418
419         var result = document.createRange();
420         result.setStart(startNode, startOffset);
421         result.setEnd(endNode, endOffset);
422
423         return result;
424     },
425
426     _evalInInspectedWindow: function(expression)
427     {
428         // This with block is needed to work around http://bugs.webkit.org/show_bug.cgi?id=11399
429         with (InspectorController.inspectedWindow()) {
430             return eval(expression);
431         }
432     },
433
434     _caretInsidePrompt: function()
435     {
436         var selection = window.getSelection();
437         if (!selection.rangeCount || !selection.isCollapsed)
438             return false;
439         var selectionRange = selection.getRangeAt(0);
440         return selectionRange.startContainer === this.promptElement && selectionRange.startContainer.isDescendant(this.promptElement);
441     },
442
443     _moveCaretToEndOfPrompt: function()
444     {
445         var selection = window.getSelection();
446         var selectionRange = document.createRange();
447
448         var offset = this.promptElement.firstChild ? 1 : 0;
449         selectionRange.setStart(this.promptElement, offset);
450         selectionRange.setEnd(this.promptElement, offset);
451
452         selection.removeAllRanges();
453         selection.addRange(selectionRange);
454     },
455
456     _onTabPressed: function(event)
457     {
458         event.preventDefault();
459         event.stopPropagation();
460         this.complete();
461     },
462
463     _onEnterPressed: function(event)
464     {
465         event.preventDefault();
466         event.stopPropagation();
467
468         this.clearAutoComplete(true);
469
470         var str = this.promptText;
471         if (!str.length)
472             return;
473
474         this.commandHistory.push(str);
475         this.commandOffset = 0;
476
477         this.promptText = "";
478
479         var result;
480         var exception = false;
481         try {
482             result = this._evalInInspectedWindow(str);
483         } catch(e) {
484             result = e;
485             exception = true;
486         }
487
488         var level = exception ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log;
489         this.addMessage(new WebInspector.ConsoleCommand(str, result, this._format(result), level));
490     },
491
492     _onUpPressed: function(event)
493     {
494         event.preventDefault();
495         event.stopPropagation();
496
497         if (this.commandOffset == this.commandHistory.length)
498             return;
499
500         if (this.commandOffset == 0)
501             this.tempSavedCommand = this.promptText;
502
503         ++this.commandOffset;
504         this.promptText = this.commandHistory[this.commandHistory.length - this.commandOffset];
505     },
506
507     _onDownPressed: function(event)
508     {
509         event.preventDefault();
510         event.stopPropagation();
511
512         if (this.commandOffset == 0)
513             return;
514
515         --this.commandOffset;
516
517         if (this.commandOffset == 0) {
518             this.promptText = this.tempSavedCommand;
519             delete this.tempSavedCommand;
520             return;
521         }
522
523         this.promptText = this.commandHistory[this.commandHistory.length - this.commandOffset];
524     },
525
526     _format: function(output)
527     {
528         var type = Object.type(output);
529         if (type === "object") {
530             if (output instanceof Node)
531                 type = "node";
532         }
533
534         // We don't perform any special formatting on these types, so we just
535         // pass them through the simple _formatvalue function.
536         var undecoratedTypes = {
537             "undefined": 1,
538             "null": 1,
539             "boolean": 1,
540             "number": 1,
541             "date": 1,
542             "function": 1,
543         };
544
545         var formatter;
546         if (type in undecoratedTypes)
547             formatter = "_formatvalue";
548         else {
549             formatter = "_format" + type;
550             if (!(formatter in this)) {
551                 formatter = "_formatobject";
552                 type = "object";
553             }
554         }
555
556         var span = document.createElement("span");
557         span.addStyleClass("console-formatted-" + type);
558         this[formatter](output, span);
559         return span;
560     },
561
562     _formatvalue: function(val, elem)
563     {
564         elem.appendChild(document.createTextNode(val));
565     },
566
567     _formatstring: function(str, elem)
568     {
569         elem.appendChild(document.createTextNode("\"" + str + "\""));
570     },
571
572     _formatregexp: function(re, elem)
573     {
574         var formatted = String(re).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1);
575         elem.appendChild(document.createTextNode(formatted));
576     },
577
578     _formatarray: function(arr, elem)
579     {
580         elem.appendChild(document.createTextNode("["));
581         for (var i = 0; i < arr.length; ++i) {
582             elem.appendChild(this._format(arr[i]));
583             if (i < arr.length - 1)
584                 elem.appendChild(document.createTextNode(", "));
585         }
586         elem.appendChild(document.createTextNode("]"));
587     },
588
589     _formatnode: function(node, elem)
590     {
591         var anchor = document.createElement("a");
592         anchor.innerHTML = node.titleInfo().title;
593         anchor.representedNode = node;
594         elem.appendChild(anchor);
595     },
596
597     _formatobject: function(obj, elem)
598     {
599         elem.appendChild(document.createTextNode(Object.describe(obj)));
600     },
601 }
602
603 WebInspector.ConsolePanel.prototype.__proto__ = WebInspector.Panel.prototype;
604
605 WebInspector.ConsoleMessage = function(source, level, message, line, url)
606 {
607     this.source = source;
608     this.level = level;
609     this.message = message;
610     this.line = line;
611     this.url = url;
612 }
613
614 WebInspector.ConsoleMessage.prototype = {
615     get shortURL()
616     {
617         if (this.resource)
618             return this.resource.displayName;
619         return this.url;
620     },
621
622     toMessageElement: function()
623     {
624         var element = document.createElement("div");
625         element.message = this;
626         element.className = "console-message";
627
628         switch (this.source) {
629             case WebInspector.ConsoleMessage.MessageSource.HTML:
630                 element.addStyleClass("console-html-source");
631                 break;
632             case WebInspector.ConsoleMessage.MessageSource.XML:
633                 element.addStyleClass("console-xml-source");
634                 break;
635             case WebInspector.ConsoleMessage.MessageSource.JS:
636                 element.addStyleClass("console-js-source");
637                 break;
638             case WebInspector.ConsoleMessage.MessageSource.CSS:
639                 element.addStyleClass("console-css-source");
640                 break;
641             case WebInspector.ConsoleMessage.MessageSource.Other:
642                 element.addStyleClass("console-other-source");
643                 break;
644         }
645
646         switch (this.level) {
647             case WebInspector.ConsoleMessage.MessageLevel.Tip:
648                 element.addStyleClass("console-tip-level");
649                 break;
650             case WebInspector.ConsoleMessage.MessageLevel.Log:
651                 element.addStyleClass("console-log-level");
652                 break;
653             case WebInspector.ConsoleMessage.MessageLevel.Warning:
654                 element.addStyleClass("console-warning-level");
655                 break;
656             case WebInspector.ConsoleMessage.MessageLevel.Error:
657                 element.addStyleClass("console-error-level");
658         }
659
660         var messageTextElement = document.createElement("span");
661         messageTextElement.className = "console-message-text";
662         messageTextElement.textContent = this.message;
663         element.appendChild(messageTextElement);
664
665         element.appendChild(document.createTextNode(" "));
666
667         if (this.url && this.url !== "undefined") {
668             var urlElement = document.createElement("a");
669             urlElement.className = "console-message-url";
670
671             if (this.line > 0)
672                 urlElement.textContent = WebInspector.UIString("%s (line %d)", this.url, this.line);
673             else
674                 urlElement.textContent = this.url;
675
676             element.appendChild(urlElement);
677         }
678
679         return element;
680     },
681
682     toString: function()
683     {
684         var sourceString;
685         switch (this.source) {
686             case WebInspector.ConsoleMessage.MessageSource.HTML:
687                 sourceString = "HTML";
688                 break;
689             case WebInspector.ConsoleMessage.MessageSource.XML:
690                 sourceString = "XML";
691                 break;
692             case WebInspector.ConsoleMessage.MessageSource.JS:
693                 sourceString = "JS";
694                 break;
695             case WebInspector.ConsoleMessage.MessageSource.CSS:
696                 sourceString = "CSS";
697                 break;
698             case WebInspector.ConsoleMessage.MessageSource.Other:
699                 sourceString = "Other";
700                 break;
701         }
702
703         var levelString;
704         switch (this.level) {
705             case WebInspector.ConsoleMessage.MessageLevel.Tip:
706                 levelString = "Tip";
707                 break;
708             case WebInspector.ConsoleMessage.MessageLevel.Log:
709                 levelString = "Log";
710                 break;
711             case WebInspector.ConsoleMessage.MessageLevel.Warning:
712                 levelString = "Warning";
713                 break;
714             case WebInspector.ConsoleMessage.MessageLevel.Error:
715                 levelString = "Error";
716                 break;
717         }
718
719         return sourceString + " " + levelString + ": " + this.message + "\n" + this.url + " line " + this.line;
720     }
721 }
722
723 // Note: Keep these constants in sync with the ones in Chrome.h
724 WebInspector.ConsoleMessage.MessageSource = {
725     HTML: 0,
726     XML: 1,
727     JS: 2,
728     CSS: 3,
729     Other: 4,
730 }
731
732 WebInspector.ConsoleMessage.MessageLevel = {
733     Tip: 0,
734     Log: 1,
735     Warning: 2,
736     Error: 3
737 }
738
739 WebInspector.ConsoleCommand = function(command, result, formattedResultElement, level)
740 {
741     this.command = command;
742     this.formattedResultElement = formattedResultElement;
743     this.level = level;
744 }
745
746 WebInspector.ConsoleCommand.prototype = {
747     toMessageElement: function()
748     {
749         var element = document.createElement("div");
750         element.command = this;
751         element.className = "console-user-command";
752
753         var commandTextElement = document.createElement("span");
754         commandTextElement.className = "console-message-text";
755         commandTextElement.textContent = this.command;
756         element.appendChild(commandTextElement);
757
758         var resultElement = document.createElement("div");
759         resultElement.className = "console-message";
760         element.appendChild(resultElement);
761
762         switch (this.level) {
763             case WebInspector.ConsoleMessage.MessageLevel.Log:
764                 resultElement.addStyleClass("console-log-level");
765                 break;
766             case WebInspector.ConsoleMessage.MessageLevel.Warning:
767                 resultElement.addStyleClass("console-warning-level");
768                 break;
769             case WebInspector.ConsoleMessage.MessageLevel.Error:
770                 resultElement.addStyleClass("console-error-level");
771         }
772
773         var resultTextElement = document.createElement("span");
774         resultTextElement.className = "console-message-text";
775         resultTextElement.appendChild(this.formattedResultElement);
776         resultElement.appendChild(resultTextElement);
777
778         return element;
779     }
780 }