Commit 572a17e2 authored by joepeck@webkit.org's avatar joepeck@webkit.org

Web Inspector: Breakpoint Actions

https://bugs.webkit.org/show_bug.cgi?id=120576

Reviewed by Timothy Hatcher.

Give the CodeMirror editor for JavaScript Breakpoint Actions JS Runtime
completions (like window, document, etc.).

The logic was inside of JavaScriptLogViewController but was already
entirely independent. Factor it out into its own class and plug it into
CodeMirrorCompletionController as a "CompletionsProvider".

Because the class hooks into a global event to reset some state, make
it a singleton, so new instances are not leaked.

* UserInterface/Main.html:
* UserInterface/Main.js:
(WebInspector.loaded):
* UserInterface/RuntimeManager.js: Added.
(WebInspector.RuntimeManager):
(WebInspector.RuntimeManager.prototype.evalCallback):
(WebInspector.RuntimeManager.prototype.evaluateInInspectedWindow):
Move eval in window to new RuntimeManager. A slightly cleaner place to this
since it is used both by the console and JS completions provider.

* UserInterface/CodeMirrorCompletionController.js:
(WebInspector.CodeMirrorCompletionController):
(WebInspector.CodeMirrorCompletionController.prototype.setExtendedCompletionProvider):
(WebInspector.CodeMirrorCompletionController.prototype._completeAtCurrentPosition):
Allow a completions provider plugin to completions controller. If one
is available, this supercedes the delegate extend completions path.

* UserInterface/BreakpointActionView.js:
* UserInterface/ConsolePrompt.js:
(WebInspector.ConsolePrompt):
Use the new JS completions provider to get runtime JS completions.
NOTE: ConsolePrompt still needs the delegate path for SQL completions.

* UserInterface/JavaScriptLogViewController.js:
(WebInspector.JavaScriptLogViewController):
(WebInspector.JavaScriptLogViewController.prototype.consolePromptTextCommitted):
* UserInterface/JavaScriptRuntimeCompletionProvider.js: Added.
(WebInspector.JavaScriptRuntimeCompletionProvider):
(get WebInspector.JavaScriptRuntimeCompletionProvider.prototype.):
(get WebInspector.JavaScriptRuntimeCompletionProvider.prototype.completionControllerCompletionsNeeded):
Move the JS completions code to a new class. No changes needed.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@155134 268f45cc-cd09-0410-ab3c-d52691b4dbfc
parent 8dbc7b8e
2013-09-05 Joseph Pecoraro <pecoraro@apple.com>
Web Inspector: Breakpoint Actions
https://bugs.webkit.org/show_bug.cgi?id=120576
Reviewed by Timothy Hatcher.
Give the CodeMirror editor for JavaScript Breakpoint Actions JS Runtime
completions (like window, document, etc.).
The logic was inside of JavaScriptLogViewController but was already
entirely independent. Factor it out into its own class and plug it into
CodeMirrorCompletionController as a "CompletionsProvider".
Because the class hooks into a global event to reset some state, make
it a singleton, so new instances are not leaked.
* UserInterface/Main.html:
* UserInterface/Main.js:
(WebInspector.loaded):
* UserInterface/RuntimeManager.js: Added.
(WebInspector.RuntimeManager):
(WebInspector.RuntimeManager.prototype.evalCallback):
(WebInspector.RuntimeManager.prototype.evaluateInInspectedWindow):
Move eval in window to new RuntimeManager. A slightly cleaner place to this
since it is used both by the console and JS completions provider.
* UserInterface/CodeMirrorCompletionController.js:
(WebInspector.CodeMirrorCompletionController):
(WebInspector.CodeMirrorCompletionController.prototype.setExtendedCompletionProvider):
(WebInspector.CodeMirrorCompletionController.prototype._completeAtCurrentPosition):
Allow a completions provider plugin to completions controller. If one
is available, this supercedes the delegate extend completions path.
* UserInterface/BreakpointActionView.js:
* UserInterface/ConsolePrompt.js:
(WebInspector.ConsolePrompt):
Use the new JS completions provider to get runtime JS completions.
NOTE: ConsolePrompt still needs the delegate path for SQL completions.
* UserInterface/JavaScriptLogViewController.js:
(WebInspector.JavaScriptLogViewController):
(WebInspector.JavaScriptLogViewController.prototype.consolePromptTextCommitted):
* UserInterface/JavaScriptRuntimeCompletionProvider.js: Added.
(WebInspector.JavaScriptRuntimeCompletionProvider):
(get WebInspector.JavaScriptRuntimeCompletionProvider.prototype.):
(get WebInspector.JavaScriptRuntimeCompletionProvider.prototype.completionControllerCompletionsNeeded):
Move the JS completions code to a new class. No changes needed.
2013-09-05 Joseph Pecoraro <pecoraro@apple.com>
Web Inspector: Breakpoint Actions
......
......@@ -157,6 +157,7 @@ WebInspector.BreakpointActionView.prototype = {
this._codeMirror.on("blur", this._codeMirrorBlurred.bind(this));
var completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror);
completionController.addExtendedCompletionProvider("javascript", WebInspector.javaScriptRuntimeCompletionProvider);
// CodeMirror needs a refresh after the popover displays, to layout, otherwise it doesn't appear.
setTimeout(function() {
......
......@@ -38,6 +38,7 @@ WebInspector.CodeMirrorCompletionController = function(codeMirror, delegate, sto
this._lineNumber = NaN;
this._prefix = "";
this._completions = [];
this._extendedCompletionProviders = {};
this._suggestionsView = new WebInspector.CompletionSuggestionsView(this);
......@@ -88,6 +89,11 @@ WebInspector.CodeMirrorCompletionController.prototype = {
return this._delegate;
},
addExtendedCompletionProvider: function(modeName, provider)
{
this._extendedCompletionProviders[modeName] = provider;
},
updateCompletions: function(completions, implicitSuffix)
{
if (isNaN(this._startOffset) || isNaN(this._endOffset) || isNaN(this._lineNumber))
......@@ -498,6 +504,12 @@ WebInspector.CodeMirrorCompletionController.prototype = {
break;
}
var extendedCompletionsProvider = this._extendedCompletionProviders[modeName];
if (extendedCompletionsProvider) {
extendedCompletionsProvider.completionControllerCompletionsNeeded(this, defaultCompletions, baseScanResult ? baseScanResult.string : null, this._prefix, suffix, force);
return;
}
if (this._delegate && typeof this._delegate.completionControllerCompletionsNeeded === "function")
this._delegate.completionControllerCompletionsNeeded(this, this._prefix, defaultCompletions, baseScanResult ? baseScanResult.string : null, suffix, force);
else
......
......@@ -53,6 +53,7 @@ WebInspector.ConsolePrompt = function(delegate, mimeType, element)
this._codeMirror.addKeyMap(keyMap);
this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this);
this._completionController.addExtendedCompletionProvider("javascript", WebInspector.javaScriptRuntimeCompletionProvider);
this._history = [{}];
this._historyIndex = 0;
......
......@@ -59,8 +59,6 @@ WebInspector.JavaScriptLogViewController = function(element, scrollElement, text
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();
};
......@@ -234,234 +232,11 @@ WebInspector.JavaScriptLogViewController.prototype = {
text += "\n//# sourceURL=__WebInspectorConsole__\n";
this._evaluateInInspectedWindow(text, "console", true, false, 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);
}
WebInspector.runtimeManager.evaluateInInspectedWindow(text, "console", true, false, false, printResult.bind(this));
},
// 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();
......
/*
* 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.JavaScriptRuntimeCompletionProvider = function()
{
WebInspector.Object.call(this);
console.assert(!WebInspector.JavaScriptRuntimeCompletionProvider._instance);
WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._clearLastProperties, this);
};
Object.defineProperty(WebInspector, "javaScriptRuntimeCompletionProvider",
{
get: function()
{
if (!WebInspector.JavaScriptRuntimeCompletionProvider._instance)
WebInspector.JavaScriptRuntimeCompletionProvider._instance = new WebInspector.JavaScriptRuntimeCompletionProvider;
return WebInspector.JavaScriptRuntimeCompletionProvider._instance;
}
});
WebInspector.JavaScriptRuntimeCompletionProvider.prototype = {
constructor: WebInspector.JavaScriptRuntimeCompletionProvider,
// Protected
completionControllerCompletionsNeeded: function(completionController, 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)) {
completionController.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) {
completionController.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) {
completionController.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 && !this._alwaysEvaluateInWindowContext)
activeCallFrame.collectScopeChainVariableNames(receivedPropertyNames.bind(this));
else
WebInspector.runtimeManager.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, {});
completionController.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")
WebInspector.runtimeManager.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);
completionController.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;
}