59fa74ba798ca9b435b42c3857d2badb76bfcc95
[WebKit-https.git] / WebCore / page / inspector / Console.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.messagesElement = document.createElement("div");
36     this.messagesElement.id = "console-messages";
37     this.messagesElement.addEventListener("selectstart", this.messagesSelectStart.bind(this), true);
38     this.messagesElement.addEventListener("click", this.messagesClicked.bind(this), true);
39     this.element.appendChild(this.messagesElement);
40
41     this.promptElement = document.createElement("div");
42     this.promptElement.id = "console-prompt";
43     this.promptElement.addEventListener("keydown", this.promptKeyDown.bind(this), false);
44     this.promptElement.appendChild(document.createElement("br"));
45     this.messagesElement.appendChild(this.promptElement);
46
47     this.prompt = new WebInspector.TextPrompt(this.promptElement, this.completions.bind(this), " .=:[({;");
48
49     this.clearButton = document.createElement("button");
50     this.clearButton.title = WebInspector.UIString("Clear");
51     this.clearButton.textContent = WebInspector.UIString("Clear");
52     this.clearButton.addEventListener("click", this.clearButtonClicked.bind(this), false);
53 }
54
55 WebInspector.ConsolePanel.prototype = {
56     show: function()
57     {
58         WebInspector.Panel.prototype.show.call(this);
59         WebInspector.consoleListItem.select();
60
61         this.clearButton.removeStyleClass("hidden");
62         if (!this.clearButton.parentNode)
63             document.getElementById("toolbarButtons").appendChild(this.clearButton);
64
65         WebInspector.currentFocusElement = document.getElementById("main");
66
67         function focusPrompt()
68         {
69             if (!this.prompt.isCaretInsidePrompt())
70                 this.prompt.moveCaretToEndOfPrompt();
71         }
72
73         setTimeout(focusPrompt.bind(this), 0);
74     },
75
76     hide: function()
77     {
78         WebInspector.Panel.prototype.hide.call(this);
79         WebInspector.consoleListItem.deselect();
80         this.clearButton.addStyleClass("hidden");
81     },
82
83     addMessage: function(msg)
84     {
85         if (msg.url in WebInspector.resourceURLMap) {
86             msg.resource = WebInspector.resourceURLMap[msg.url];
87             switch (msg.level) {
88                 case WebInspector.ConsoleMessage.MessageLevel.Warning:
89                     ++msg.resource.warnings;
90                     if (msg.resource.panel.addMessageToSource)
91                         msg.resource.panel.addMessageToSource(msg);
92                     break;
93                 case WebInspector.ConsoleMessage.MessageLevel.Error:
94                     ++msg.resource.errors;
95                     if (msg.resource.panel.addMessageToSource)
96                         msg.resource.panel.addMessageToSource(msg);
97                     break;
98             }
99         }
100
101         this.messages.push(msg);
102
103         var element = msg.toMessageElement();
104         this.messagesElement.insertBefore(element, this.promptElement);
105         this.promptElement.scrollIntoView(false);
106     },
107
108     clearMessages: function()
109     {
110         for (var i = 0; i < this.messages.length; ++i) {
111             var resource = this.messages[i].resource;
112             if (!resource)
113                 continue;
114             resource.errors = 0;
115             resource.warnings = 0;
116         }
117
118         this.messages = [];
119
120         while (this.messagesElement.firstChild != this.promptElement)
121             this.messagesElement.removeChild(this.messagesElement.firstChild);
122     },
123
124     completions: function(wordRange, bestMatchOnly)
125     {
126         // Pass less characters to scanBackwards so the range will be a more complete expression.
127         var expression = this.prompt.scanBackwards(" =:{;", wordRange.startContainer, wordRange.startOffset);
128         var expressionString = expression.toString();
129         var lastIndex = expressionString.length - 1;
130
131         var dotNotation = (expressionString[lastIndex] === ".");
132         var bracketNotation = (expressionString[lastIndex] === "[");
133
134         if (dotNotation || bracketNotation)
135             expressionString = expressionString.substr(0, lastIndex);
136
137         var prefix = wordRange.toString();
138         if (!expressionString && !prefix)
139             return;
140
141         var result = InspectorController.inspectedWindow();
142         if (expressionString) {
143             try {
144                 result = this._evalInInspectedWindow(expressionString);
145             } catch(e) {
146                 // Do nothing, the prefix will be considered a window property.
147             }
148         }
149
150         if (bracketNotation) {
151             if (prefix.length && prefix[0] === "'")
152                 var quoteUsed = "'";
153             else
154                 var quoteUsed = "\"";
155         }
156
157         var results = [];
158         var properties = Object.sortedProperties(result);
159         for (var i = 0; i < properties.length; ++i) {
160             var property = properties[i];
161             if (bracketNotation)
162                 property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + quoteUsed + "]";
163             if (property.length < prefix.length)
164                 continue;
165             if (property.indexOf(prefix) !== 0)
166                 continue;
167             results.push(property);
168             if (bestMatchOnly)
169                 break;
170         }
171
172         return results;
173     },
174
175     clearButtonClicked: function()
176     {
177         this.clearMessages();
178     },
179
180     messagesSelectStart: function(event)
181     {
182         if (this._selectionTimeout)
183             clearTimeout(this._selectionTimeout);
184
185         this.prompt.clearAutoComplete();
186
187         function moveBackIfOutside()
188         {
189             delete this._selectionTimeout;
190             if (!this.prompt.isCaretInsidePrompt() && window.getSelection().isCollapsed)
191                 this.prompt.moveCaretToEndOfPrompt();
192             this.prompt.autoCompleteSoon();
193         }
194
195         this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
196     },
197
198     messagesClicked: function(event)
199     {
200         var link = event.target.enclosingNodeOrSelfWithNodeName("a");
201         if (link && link.representedNode) {
202             WebInspector.updateFocusedNode(link.representedNode);
203             return;
204         }
205
206         var messageElement = event.target.enclosingNodeOrSelfWithClass("console-message");
207         if (!messageElement)
208             return;
209
210         if (!messageElement.message)
211             return;
212
213         var resource = messageElement.message.resource;
214         if (!resource)
215             return;
216
217         if (link && link.hasStyleClass("console-message-url")) {
218             WebInspector.navigateToResource(resource);
219             resource.panel.showSourceLine(item.message.line);
220         }
221
222         event.stopPropagation();
223         event.preventDefault();
224     },
225
226     promptKeyDown: function(event)
227     {
228         switch (event.keyIdentifier) {
229             case "Enter":
230                 this._enterKeyPressed(event);
231                 return;
232         }
233
234         this.prompt.handleKeyEvent(event);
235     },
236
237     _evalInInspectedWindow: function(expression)
238     {
239         // This with block is needed to work around http://bugs.webkit.org/show_bug.cgi?id=11399
240         with (InspectorController.inspectedWindow()) {
241             return eval(expression);
242         }
243     },
244
245     _enterKeyPressed: function(event)
246     {
247         event.preventDefault();
248         event.stopPropagation();
249
250         this.prompt.clearAutoComplete(true);
251
252         var str = this.prompt.text;
253         if (!str.length)
254             return;
255
256         this.commandHistory.push(str);
257         this.commandOffset = 0;
258
259         this.promptText = "";
260
261         var result;
262         var exception = false;
263         try {
264             result = this._evalInInspectedWindow(str);
265         } catch(e) {
266             result = e;
267             exception = true;
268         }
269
270         this.prompt.history.push(str);
271         this.prompt.historyOffset = 0;
272         this.prompt.text = "";
273
274         var level = exception ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log;
275         this.addMessage(new WebInspector.ConsoleCommand(str, result, this._format(result), level));
276     },
277
278     _format: function(output)
279     {
280         var type = Object.type(output);
281         if (type === "object") {
282             if (output instanceof Node)
283                 type = "node";
284         }
285
286         // We don't perform any special formatting on these types, so we just
287         // pass them through the simple _formatvalue function.
288         var undecoratedTypes = {
289             "undefined": 1,
290             "null": 1,
291             "boolean": 1,
292             "number": 1,
293             "date": 1,
294             "function": 1,
295         };
296
297         var formatter;
298         if (type in undecoratedTypes)
299             formatter = "_formatvalue";
300         else {
301             formatter = "_format" + type;
302             if (!(formatter in this)) {
303                 formatter = "_formatobject";
304                 type = "object";
305             }
306         }
307
308         var span = document.createElement("span");
309         span.addStyleClass("console-formatted-" + type);
310         this[formatter](output, span);
311         return span;
312     },
313
314     _formatvalue: function(val, elem)
315     {
316         elem.appendChild(document.createTextNode(val));
317     },
318
319     _formatstring: function(str, elem)
320     {
321         elem.appendChild(document.createTextNode("\"" + str + "\""));
322     },
323
324     _formatregexp: function(re, elem)
325     {
326         var formatted = String(re).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1);
327         elem.appendChild(document.createTextNode(formatted));
328     },
329
330     _formatarray: function(arr, elem)
331     {
332         elem.appendChild(document.createTextNode("["));
333         for (var i = 0; i < arr.length; ++i) {
334             elem.appendChild(this._format(arr[i]));
335             if (i < arr.length - 1)
336                 elem.appendChild(document.createTextNode(", "));
337         }
338         elem.appendChild(document.createTextNode("]"));
339     },
340
341     _formatnode: function(node, elem)
342     {
343         var anchor = document.createElement("a");
344         anchor.innerHTML = nodeTitleInfo.call(node).title;
345         anchor.representedNode = node;
346         anchor.addEventListener("mouseover", function() { InspectorController.highlightDOMNode(node) }, false);
347         anchor.addEventListener("mouseout", function() { InspectorController.hideDOMNodeHighlight() }, false);
348         elem.appendChild(anchor);
349     },
350
351     _formatobject: function(obj, elem)
352     {
353         elem.appendChild(document.createTextNode(Object.describe(obj)));
354     },
355 }
356
357 WebInspector.ConsolePanel.prototype.__proto__ = WebInspector.Panel.prototype;
358
359 WebInspector.ConsoleMessage = function(source, level, message, line, url)
360 {
361     this.source = source;
362     this.level = level;
363     this.message = message;
364     this.line = line;
365     this.url = url;
366 }
367
368 WebInspector.ConsoleMessage.prototype = {
369     get shortURL()
370     {
371         if (this.resource)
372             return this.resource.displayName;
373         return this.url;
374     },
375
376     toMessageElement: function()
377     {
378         var element = document.createElement("div");
379         element.message = this;
380         element.className = "console-message";
381
382         switch (this.source) {
383             case WebInspector.ConsoleMessage.MessageSource.HTML:
384                 element.addStyleClass("console-html-source");
385                 break;
386             case WebInspector.ConsoleMessage.MessageSource.XML:
387                 element.addStyleClass("console-xml-source");
388                 break;
389             case WebInspector.ConsoleMessage.MessageSource.JS:
390                 element.addStyleClass("console-js-source");
391                 break;
392             case WebInspector.ConsoleMessage.MessageSource.CSS:
393                 element.addStyleClass("console-css-source");
394                 break;
395             case WebInspector.ConsoleMessage.MessageSource.Other:
396                 element.addStyleClass("console-other-source");
397                 break;
398         }
399
400         switch (this.level) {
401             case WebInspector.ConsoleMessage.MessageLevel.Tip:
402                 element.addStyleClass("console-tip-level");
403                 break;
404             case WebInspector.ConsoleMessage.MessageLevel.Log:
405                 element.addStyleClass("console-log-level");
406                 break;
407             case WebInspector.ConsoleMessage.MessageLevel.Warning:
408                 element.addStyleClass("console-warning-level");
409                 break;
410             case WebInspector.ConsoleMessage.MessageLevel.Error:
411                 element.addStyleClass("console-error-level");
412         }
413
414         var messageTextElement = document.createElement("span");
415         messageTextElement.className = "console-message-text";
416         messageTextElement.textContent = this.message;
417         element.appendChild(messageTextElement);
418
419         element.appendChild(document.createTextNode(" "));
420
421         if (this.url && this.url !== "undefined") {
422             var urlElement = document.createElement("a");
423             urlElement.className = "console-message-url";
424
425             if (this.line > 0)
426                 urlElement.textContent = WebInspector.UIString("%s (line %d)", this.url, this.line);
427             else
428                 urlElement.textContent = this.url;
429
430             element.appendChild(urlElement);
431         }
432
433         return element;
434     },
435
436     toString: function()
437     {
438         var sourceString;
439         switch (this.source) {
440             case WebInspector.ConsoleMessage.MessageSource.HTML:
441                 sourceString = "HTML";
442                 break;
443             case WebInspector.ConsoleMessage.MessageSource.XML:
444                 sourceString = "XML";
445                 break;
446             case WebInspector.ConsoleMessage.MessageSource.JS:
447                 sourceString = "JS";
448                 break;
449             case WebInspector.ConsoleMessage.MessageSource.CSS:
450                 sourceString = "CSS";
451                 break;
452             case WebInspector.ConsoleMessage.MessageSource.Other:
453                 sourceString = "Other";
454                 break;
455         }
456
457         var levelString;
458         switch (this.level) {
459             case WebInspector.ConsoleMessage.MessageLevel.Tip:
460                 levelString = "Tip";
461                 break;
462             case WebInspector.ConsoleMessage.MessageLevel.Log:
463                 levelString = "Log";
464                 break;
465             case WebInspector.ConsoleMessage.MessageLevel.Warning:
466                 levelString = "Warning";
467                 break;
468             case WebInspector.ConsoleMessage.MessageLevel.Error:
469                 levelString = "Error";
470                 break;
471         }
472
473         return sourceString + " " + levelString + ": " + this.message + "\n" + this.url + " line " + this.line;
474     }
475 }
476
477 // Note: Keep these constants in sync with the ones in Chrome.h
478 WebInspector.ConsoleMessage.MessageSource = {
479     HTML: 0,
480     XML: 1,
481     JS: 2,
482     CSS: 3,
483     Other: 4,
484 }
485
486 WebInspector.ConsoleMessage.MessageLevel = {
487     Tip: 0,
488     Log: 1,
489     Warning: 2,
490     Error: 3
491 }
492
493 WebInspector.ConsoleCommand = function(command, result, formattedResultElement, level)
494 {
495     this.command = command;
496     this.formattedResultElement = formattedResultElement;
497     this.level = level;
498 }
499
500 WebInspector.ConsoleCommand.prototype = {
501     toMessageElement: function()
502     {
503         var element = document.createElement("div");
504         element.command = this;
505         element.className = "console-user-command";
506
507         var commandTextElement = document.createElement("span");
508         commandTextElement.className = "console-message-text";
509         commandTextElement.textContent = this.command;
510         element.appendChild(commandTextElement);
511
512         var resultElement = document.createElement("div");
513         resultElement.className = "console-message";
514         element.appendChild(resultElement);
515
516         switch (this.level) {
517             case WebInspector.ConsoleMessage.MessageLevel.Log:
518                 resultElement.addStyleClass("console-log-level");
519                 break;
520             case WebInspector.ConsoleMessage.MessageLevel.Warning:
521                 resultElement.addStyleClass("console-warning-level");
522                 break;
523             case WebInspector.ConsoleMessage.MessageLevel.Error:
524                 resultElement.addStyleClass("console-error-level");
525         }
526
527         var resultTextElement = document.createElement("span");
528         resultTextElement.className = "console-message-text";
529         resultTextElement.appendChild(this.formattedResultElement);
530         resultElement.appendChild(resultTextElement);
531
532         return element;
533     }
534 }