blob: edc4cf2dca05609fa3a23112416126bd0c0b3857 [file] [log] [blame] [edit]
/*
* Copyright (C) 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WebInspector.JavaScriptLogViewController = function(element, scrollElement, textPrompt, delegate, historySettingIdentifier)
{
WebInspector.Object.call(this);
console.assert(textPrompt instanceof WebInspector.ConsolePrompt);
console.assert(historySettingIdentifier);
this._element = element;
this._scrollElement = scrollElement;
this._promptHistorySetting = new WebInspector.Setting(historySettingIdentifier, null);
this._prompt = textPrompt;
this._prompt.delegate = this;
this._prompt.history = this._promptHistorySetting.value;
this.delegate = delegate;
this._cleared = true;
this._previousMessage = null;
this._repeatCountWasInterrupted = false;
this._topConsoleGroups = [];
this.messagesClearKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command, "K", this._handleClearShortcut.bind(this));
this.messagesAlternateClearKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Control, "L", this._handleClearShortcut.bind(this), this._element);
this._messagesFindKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command, "F", this._handleFindShortcut.bind(this), this._element);
this._messagesFindNextKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command, "G", this._handleFindNextShortcut.bind(this), this._element);
this._messagesFindPreviousKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command | WebInspector.KeyboardShortcut.Modifier.Shift, "G", this._handleFindPreviousShortcut.bind(this), this._element);
this._promptAlternateClearKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Control, "L", this._handleClearShortcut.bind(this), this._prompt.element);
this._promptFindKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command, "F", this._handleFindShortcut.bind(this), this._prompt.element);
this._promptFindNextKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command, "G", this._handleFindNextShortcut.bind(this), this._prompt.element);
this._promptFindPreviousKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command | WebInspector.KeyboardShortcut.Modifier.Shift, "G", this._handleFindPreviousShortcut.bind(this), this._prompt.element);
WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._clearLastProperties, this);
this.startNewSession();
};
WebInspector.JavaScriptLogViewController.CachedPropertiesDuration = 30000;
WebInspector.JavaScriptLogViewController.prototype = {
constructor: WebInspector.JavaScriptLogViewController,
// Public
get prompt()
{
return this._prompt;
},
get topConsoleGroup()
{
return this._topConsoleGroup;
},
get currentConsoleGroup()
{
return this._currentConsoleGroup;
},
clear: function()
{
this._cleared = true;
this.startNewSession(true);
this.prompt.focus();
if (this.delegate && typeof this.delegate.didClearMessages === "function")
this.delegate.didClearMessages();
},
startNewSession: function(clearPreviousSessions)
{
if (this._topConsoleGroups.length && clearPreviousSessions) {
for (var i = 0; i < this._topConsoleGroups.length; ++i)
this._element.removeChild(this._topConsoleGroups[i].element);
this._topConsoleGroups = [];
this._topConsoleGroup = null;
this._currentConsoleGroup = null;
}
// Reuse the top group if it has no messages.
if (this._topConsoleGroup && !this._topConsoleGroup.hasMessages()) {
// Make sure the session is visible.
this._topConsoleGroup.element.scrollIntoView();
return;
}
var hasPreviousSession = !!this._topConsoleGroup;
var consoleGroup = new WebInspector.ConsoleGroup(null, hasPreviousSession);
this._previousMessage = null;
this._repeatCountWasInterrupted = false;
this._topConsoleGroups.push(consoleGroup);
this._topConsoleGroup = consoleGroup;
this._currentConsoleGroup = consoleGroup;
this._element.appendChild(consoleGroup.element);
// Make sure the new session is visible.
consoleGroup.element.scrollIntoView();
},
appendConsoleMessage: function(consoleMessage)
{
// Clone the message since there might be multiple clients using the message,
// and since the message has a DOM element it can't be two places at once.
var messageClone = consoleMessage.clone();
this._appendConsoleMessage(messageClone);
return messageClone;
},
updatePreviousMessageRepeatCount: function(count)
{
console.assert(this._previousMessage);
if (!this._previousMessage)
return;
if (!this._repeatCountWasInterrupted) {
this._previousMessage.repeatCount = count - (this._previousMessage.ignoredCount || 0);
this._previousMessage.updateRepeatCount();
} else {
var newMessage = this._previousMessage.clone();
// If this message is repeated after being cloned, messageRepeatCountUpdated will be called with a
// count that includes the count of this message before cloning. We should ignore instances of this
// log that occurred before we cloned, so set a property on the message with the number of previous
// instances of this log that we should ignore.
newMessage.ignoredCount = newMessage.repeatCount + (this._previousMessage.ignoredCount || 0);
newMessage.repeatCount = 1;
this._appendConsoleMessage(newMessage);
}
},
isScrolledToBottom: function()
{
// Lie about being scrolled to the bottom if we have a pending request to scroll to the bottom soon.
return this._scrollToBottomTimeout || this._scrollElement.isScrolledToBottom();
},
scrollToBottom: function()
{
if (this._scrollToBottomTimeout)
return;
function delayedWork()
{
this._scrollToBottomTimeout = null;
this._scrollElement.scrollTop = this._scrollElement.scrollHeight;
}
// Don't scroll immediately so we are not causing excessive layouts when there
// are many messages being added at once.
this._scrollToBottomTimeout = setTimeout(delayedWork.bind(this), 0);
},
// Protected
consolePromptHistoryDidChange: function(prompt)
{
this._promptHistorySetting.value = this.prompt.history;
},
consolePromptShouldCommitText: function(prompt, text, cursorIsAtLastPosition, handler)
{
// Always commit the text if we are not at the last position.
if (!cursorIsAtLastPosition) {
handler(true);
return;
}
// COMPATIBILITY (iOS 6): RuntimeAgent.parse did not exist in iOS 6. Always commit.
if (!RuntimeAgent.parse) {
handler(true);
return;
}
function parseFinished(error, result, message, range)
{
handler(result !== RuntimeAgent.SyntaxErrorType.Recoverable);
}
RuntimeAgent.parse(text, parseFinished.bind(this));
},
consolePromptTextCommitted: function(prompt, text)
{
console.assert(text);
var commandMessage = new WebInspector.ConsoleCommand(text);
this._appendConsoleMessage(commandMessage, true);
function printResult(result, wasThrown)
{
if (!result || this._cleared)
return;
this._appendConsoleMessage(new WebInspector.ConsoleCommandResult(result, wasThrown, commandMessage), true);
}
this._evaluateInInspectedWindow(text, "console", true, true, false, printResult.bind(this));
},
consolePromptCompletionsNeeded: function(prompt, defaultCompletions, base, prefix, suffix, forced)
{
// Don't allow non-forced empty prefix completions unless the base is that start of property access.
if (!forced && !prefix && !/[.[]$/.test(base)) {
prompt.updateCompletions(null);
return;
}
// If the base ends with an open parentheses or open curly bracket then treat it like there is
// no base so we get global object completions.
if (/[({]$/.test(base))
base = "";
var lastBaseIndex = base.length - 1;
var dotNotation = base[lastBaseIndex] === ".";
var bracketNotation = base[lastBaseIndex] === "[";
if (dotNotation || bracketNotation) {
base = base.substring(0, lastBaseIndex);
// Don't suggest anything for an empty base that is using dot notation.
// Bracket notation with an empty base will be treated as an array.
if (!base && dotNotation) {
prompt.updateCompletions(defaultCompletions);
return;
}
// Don't allow non-forced empty prefix completions if the user is entering a number, since it might be a float.
// But allow number completions if the base already has a decimal, so "10.0." will suggest Number properties.
if (!forced && !prefix && dotNotation && base.indexOf(".") === -1 && parseInt(base, 10) == base) {
prompt.updateCompletions(null);
return;
}
// An empty base with bracket notation is not property access, it is an array.
// Clear the bracketNotation flag so completions are not quoted.
if (!base && bracketNotation)
bracketNotation = false;
}
// If the base is the same as the last time, we can reuse the property names we have already gathered.
// Doing this eliminates delay caused by the async nature of the code below and it only calls getters
// and functions once instead of repetitively. Sure, there can be difference each time the base is evaluated,
// but this optimization gives us more of a win. We clear the cache after 30 seconds or when stepping in the
// debugger to make sure we don't use stale properties in most cases.
if (this._lastBase === base && this._lastPropertyNames) {
receivedPropertyNames.call(this, this._lastPropertyNames);
return;
}
this._lastBase = base;
this._lastPropertyNames = null;
var activeCallFrame = WebInspector.debuggerManager.activeCallFrame;
if (!base && activeCallFrame)
activeCallFrame.collectScopeChainVariableNames(receivedPropertyNames.bind(this));
else
this._evaluateInInspectedWindow(base, "completion", true, true, false, evaluated.bind(this));
function updateLastPropertyNames(propertyNames)
{
if (this._clearLastPropertiesTimeout)
clearTimeout(this._clearLastPropertiesTimeout);
this._clearLastPropertiesTimeout = setTimeout(this._clearLastProperties.bind(this), WebInspector.JavaScriptLogViewController.CachedPropertiesDuration);
this._lastPropertyNames = propertyNames || {};
}
function evaluated(result, wasThrown)
{
if (wasThrown || !result || result.type === "undefined" || (result.type === "object" && result.subtype === "null")) {
RuntimeAgent.releaseObjectGroup("completion");
updateLastPropertyNames.call(this, {});
prompt.updateCompletions(defaultCompletions);
return;
}
function getCompletions(primitiveType)
{
var object;
if (primitiveType === "string")
object = new String("");
else if (primitiveType === "number")
object = new Number(0);
else if (primitiveType === "boolean")
object = new Boolean(false);
else
object = this;
var resultSet = {};
for (var o = object; o; o = o.__proto__) {
try {
var names = Object.getOwnPropertyNames(o);
for (var i = 0; i < names.length; ++i)
resultSet[names[i]] = true;
} catch (e) {
// Ignore
}
}
return resultSet;
}
if (result.type === "object" || result.type === "function")
result.callFunctionJSON(getCompletions, undefined, receivedPropertyNames.bind(this));
else if (result.type === "string" || result.type === "number" || result.type === "boolean")
this._evaluateInInspectedWindow("(" + getCompletions + ")(\"" + result.type + "\")", "completion", false, true, true, receivedPropertyNamesFromEvaluate.bind(this));
else
console.error("Unknown result type: " + result.type);
}
function receivedPropertyNamesFromEvaluate(object, wasThrown, result)
{
receivedPropertyNames.call(this, result && !wasThrown ? result.value : null);
}
function receivedPropertyNames(propertyNames)
{
propertyNames = propertyNames || {};
updateLastPropertyNames.call(this, propertyNames);
RuntimeAgent.releaseObjectGroup("completion");
if (!base) {
const commandLineAPI = ["$", "$$", "$x", "dir", "dirxml", "keys", "values", "profile", "profileEnd", "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear", "getEventListeners", "$0", "$1", "$2", "$3", "$4", "$_"];
for (var i = 0; i < commandLineAPI.length; ++i)
propertyNames[commandLineAPI[i]] = true;
}
propertyNames = Object.keys(propertyNames);
var implicitSuffix = "";
if (bracketNotation) {
var quoteUsed = prefix[0] === "'" ? "'" : "\"";
if (suffix !== "]" && suffix !== quoteUsed)
implicitSuffix = "]";
}
var completions = defaultCompletions;
var knownCompletions = completions.keySet();
for (var i = 0; i < propertyNames.length; ++i) {
var property = propertyNames[i];
if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property))
continue;
if (bracketNotation) {
if (parseInt(property) != property)
property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + (suffix !== quoteUsed ? quoteUsed : "");
}
if (!property.startsWith(prefix) || property in knownCompletions)
continue;
completions.push(property);
knownCompletions[property] = true;
}
function compare(a, b)
{
// Try to sort in numerical order first.
var numericCompareResult = a - b;
if (!isNaN(numericCompareResult))
return numericCompareResult;
// Not numbers, sort as strings.
return a.localeCompare(b);
}
completions.sort(compare);
prompt.updateCompletions(completions, implicitSuffix);
}
},
// Private
_clearLastProperties: function()
{
if (this._clearLastPropertiesTimeout) {
clearTimeout(this._clearLastPropertiesTimeout);
delete this._clearLastPropertiesTimeout;
}
// Clear the cache of property names so any changes while stepping or sitting idle get picked up if the same
// expression is evaluated again.
this._lastPropertyNames = null;
},
_evaluateInInspectedWindow: function(expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, callback)
{
if (!expression) {
// There is no expression, so the completion should happen against global properties.
expression = "this";
}
function evalCallback(error, result, wasThrown)
{
if (error) {
console.error(error);
callback(null, false);
return;
}
if (returnByValue)
callback(null, wasThrown, wasThrown ? null : result);
else
callback(WebInspector.RemoteObject.fromPayload(result), wasThrown);
}
if (WebInspector.debuggerManager.activeCallFrame) {
DebuggerAgent.evaluateOnCallFrame(WebInspector.debuggerManager.activeCallFrame.id, expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, evalCallback);
return;
}
// COMPATIBILITY (iOS 6): Execution context identifiers (contextId) did not exist
// in iOS 6. Fallback to including the frame identifier (frameId).
var contextId = WebInspector.quickConsole.executionContextIdentifier;
RuntimeAgent.evaluate.invoke({expression: expression, objectGroup: objectGroup, includeCommandLineAPI: includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole: doNotPauseOnExceptionsAndMuteConsole, contextId: contextId, frameId: contextId, returnByValue: returnByValue}, evalCallback);
},
_handleClearShortcut: function()
{
this.clear();
},
_handleFindShortcut: function()
{
this.delegate.focusSearchBar();
},
_handleFindNextShortcut: function()
{
this.delegate.highlightNextSearchMatch();
},
_handleFindPreviousShortcut: function()
{
this.delegate.highlightPreviousSearchMatch();
},
_appendConsoleMessage: function(msg, repeatCountWasInterrupted)
{
var wasScrolledToBottom = this.isScrolledToBottom();
this._cleared = false;
this._repeatCountWasInterrupted = repeatCountWasInterrupted || false;
if (!repeatCountWasInterrupted)
this._previousMessage = msg;
if (msg.type === WebInspector.ConsoleMessage.MessageType.EndGroup) {
var parentGroup = this._currentConsoleGroup.parentGroup;
if (parentGroup)
this._currentConsoleGroup = parentGroup;
} else {
if (msg.type === WebInspector.ConsoleMessage.MessageType.StartGroup || msg.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed) {
var group = new WebInspector.ConsoleGroup(this._currentConsoleGroup);
this._currentConsoleGroup.messagesElement.appendChild(group.element);
this._currentConsoleGroup = group;
}
this._currentConsoleGroup.addMessage(msg);
}
if (msg.type === WebInspector.ConsoleMessage.MessageType.Result || wasScrolledToBottom)
this.scrollToBottom();
if (this.delegate && typeof this.delegate.didAppendConsoleMessage === "function")
this.delegate.didAppendConsoleMessage(msg);
}
};
WebInspector.JavaScriptLogViewController.prototype.__proto__ = WebInspector.Object.prototype;