| /* |
| * 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; |