Automate lazy generation of REPL completion lists

All invokable methods, slots, signals, and properties visible from
JavaScript, but which do not start with an underscore, are lazily
(only when necessary) added to the completion list through dynamic
reflection.

This leverages `QMetaObject` for reflection of `QObject`s.  As such,
there is now no need to inherit `REPLCompletable` and it has been
removed.

http://code.google.com/p/phantomjs/issues/detail?id=943
1.9
execjosh 2012-12-30 01:23:10 +09:00 committed by Ariya Hidayat
parent d0fe6864a9
commit d906bc3819
16 changed files with 119 additions and 360 deletions

View File

@ -38,7 +38,7 @@
// File
// public:
File::File(QFile *openfile, QTextCodec *codec, QObject *parent) :
REPLCompletable(parent),
QObject(parent),
m_file(openfile),
m_fileStream(0)
{
@ -213,23 +213,11 @@ void File::close()
deleteLater();
}
void File::initCompletions()
{
// Add completion for the Dynamic Properties of the 'file' object
// functions
addCompletion("read");
addCompletion("write");
addCompletion("readLine");
addCompletion("writeLine");
addCompletion("flush");
addCompletion("close");
}
// FileSystem
// public:
FileSystem::FileSystem(QObject *parent)
: REPLCompletable(parent)
: QObject(parent)
{ }
// public slots:
@ -487,39 +475,3 @@ bool FileSystem::_remove(const QString &path) const
bool FileSystem::_copy(const QString &source, const QString &destination) const {
return QFile(source).copy(destination);
}
void FileSystem::initCompletions()
{
// Add completion for the Dynamic Properties of the 'fs' object
// properties
addCompletion("separator");
addCompletion("workingDirectory");
// functions
addCompletion("list");
addCompletion("absolute");
addCompletion("readLink");
addCompletion("exists");
addCompletion("isDirectory");
addCompletion("isFile");
addCompletion("isAbsolute");
addCompletion("isExecutable");
addCompletion("isReadable");
addCompletion("isWritable");
addCompletion("isLink");
addCompletion("changeWorkingDirectory");
addCompletion("makeDirectory");
addCompletion("makeTree");
addCompletion("removeDirectory");
addCompletion("removeTree");
addCompletion("copyTree");
addCompletion("open");
addCompletion("read");
addCompletion("write");
addCompletion("size");
addCompletion("remove");
addCompletion("copy");
addCompletion("move");
addCompletion("touch");
addCompletion("join");
addCompletion("split");
}

View File

@ -36,9 +36,7 @@
#include <QTextStream>
#include <QVariant>
#include "replcompletable.h"
class File : public REPLCompletable
class File : public QObject
{
Q_OBJECT
@ -66,16 +64,13 @@ public slots:
void flush();
void close();
private:
virtual void initCompletions();
private:
QFile *m_file;
QTextStream *m_fileStream;
};
class FileSystem : public REPLCompletable
class FileSystem : public QObject
{
Q_OBJECT
Q_PROPERTY(QString workingDirectory READ workingDirectory)
@ -139,9 +134,6 @@ public slots:
bool isReadable(const QString &path) const;
bool isWritable(const QString &path) const;
bool isLink(const QString &path) const;
private:
virtual void initCompletions();
};
#endif // FILESYSTEM_H

View File

@ -54,7 +54,7 @@ static Phantom *phantomInstance = NULL;
// private:
Phantom::Phantom(QObject *parent)
: REPLCompletable(parent)
: QObject(parent)
, m_terminated(false)
, m_returnValue(0)
, m_filesystem(0)
@ -471,24 +471,3 @@ void Phantom::doExit(int code)
m_page = 0;
QApplication::instance()->exit(code);
}
void Phantom::initCompletions()
{
// Add completion for the Dynamic Properties of the 'phantom' object
// properties
addCompletion("args");
addCompletion("defaultPageSettings");
addCompletion("libraryPath");
addCompletion("outputEncoding");
addCompletion("scriptName");
addCompletion("version");
addCompletion("cookiesEnabled");
addCompletion("cookies");
// functions
addCompletion("exit");
addCompletion("debugExit");
addCompletion("injectJs");
addCompletion("addCookie");
addCompletion("deleteCookie");
addCompletion("clearCookies");
}

View File

@ -37,7 +37,6 @@
#include "filesystem.h"
#include "encoding.h"
#include "config.h"
#include "replcompletable.h"
#include "system.h"
#include "childprocess.h"
@ -45,7 +44,7 @@ class WebPage;
class CustomPage;
class WebServer;
class Phantom: public REPLCompletable
class Phantom : public QObject
{
Q_OBJECT
Q_PROPERTY(QStringList args READ args)
@ -181,7 +180,6 @@ private slots:
private:
void doExit(int code);
virtual void initCompletions();
Encoding m_scriptFileEnc;
WebPage *m_page;

View File

@ -26,8 +26,7 @@ HEADERS += csconverter.h \
encoding.h \
config.h \
childprocess.h \
repl.h \
replcompletable.h
repl.h
SOURCES += phantom.cpp \
callback.cpp \
@ -45,8 +44,7 @@ SOURCES += phantom.cpp \
encoding.cpp \
config.cpp \
childprocess.cpp \
repl.cpp \
replcompletable.cpp
repl.cpp
OTHER_FILES += \
bootstrap.js \

View File

@ -33,6 +33,8 @@
#include <QTimer>
#include <QDir>
#include <QRegExp>
#include <QMetaMethod>
#include <QMetaProperty>
#include "consts.h"
#include "terminal.h"
@ -78,6 +80,52 @@ REPL *REPL::getInstance(QWebFrame *webframe, Phantom *parent)
return singleton;
}
QString REPL::_getClassName(QObject *obj) const
{
const QMetaObject *meta = obj->metaObject();
return QString::fromLatin1(meta->className());
}
QStringList REPL::_enumerateCompletions(QObject *obj) const
{
const QMetaObject *meta = obj->metaObject();
QMap<QString, bool> completions;
// List up slots, signals, and invokable methods
const int methodOffset = meta->methodOffset();
const int methodCount = meta->methodCount();
for (int i = methodOffset; i < methodCount; i++) {
const QString name = QString::fromLatin1(meta->method(i).signature());
// Ignore methods starting with underscores
if (name.startsWith('_')) {
continue;
}
// Keep only up to, but not including, first paren
const int cutoff = name.indexOf('(');
completions.insert((0 < cutoff ? name.left(cutoff) : name), true);
}
// List up properties
const int propertyOffset = meta->propertyOffset();
const int propertyCount = meta->propertyCount();
for (int i = propertyOffset; i < propertyCount; i++) {
const QMetaProperty prop = meta->property(i);
// Ignore non-scriptable properties
if (!prop.isScriptable()) {
continue;
}
const QString name = QString::fromLatin1(prop.name());
// Ignore properties starting with underscores
if (name.startsWith('_')) {
continue;
}
completions.insert(name, true);
}
return completions.uniqueKeys();
}
// private:
REPL::REPL(QWebFrame *webframe, Phantom *parent)
: QObject(parent),
@ -101,6 +149,9 @@ REPL::REPL(QWebFrame *webframe, Phantom *parent)
// Inject REPL utility functions
m_webframe->evaluateJavaScript(Utils::readResourceFileUtf8(":/repl.js"), QString("phantomjs://repl.js"));
// Add self to JavaScript world
m_webframe->addToJavaScriptWindowObject("_repl", this);
// Start the REPL's loop
QTimer::singleShot(0, this, SLOT(startLoop()));
}

View File

@ -58,6 +58,9 @@ public:
static bool instanceExists();
static REPL *getInstance(QWebFrame *webframe = NULL, Phantom *parent = NULL);
Q_INVOKABLE QString _getClassName(QObject *obj) const;
Q_INVOKABLE QStringList _enumerateCompletions(QObject *obj) const;
private:
REPL(QWebFrame *webframe, Phantom *parent);
static void offerCompletion(const char *buf, linenoiseCompletions *lc);

View File

@ -31,6 +31,13 @@
var REPL = REPL || {};
(function () {
/**
* Cache to hold completions
*/
var _cache = {};
/**
* Return the Completions of the Object, applying the prefix
*
@ -40,16 +47,45 @@ var REPL = REPL || {};
REPL._getCompletions = function (obj, prefix) {
var completions = [];
// If the given object is "(REPL)Completable", just return it's completions
if (obj._isCompletable && obj._isCompletable() === true) {
completions = obj._getCompletions(prefix || "");
if (typeof prefix !== "string") {
prefix = "";
} else {
// It's a JS Native Object: build the list of completions manually
for (k in obj) {
if (obj.hasOwnProperty(k) && k.indexOf(prefix || "") === 0) {
completions.push(k);
prefix = prefix.trim();
}
try {
// Try to get `QObject` inherited class's name (throws exception if not
// inherited from `QObject`)
var className = _repl._getClassName(obj);
// Initialize completions for this class as needed
if (null == _cache[className]) {
_cache[className] = _repl._enumerateCompletions(obj);
}
var key = className;
if ("" !== prefix) {
key = "-" + prefix;
if (null == _cache[key]) {
// Filter out completions
var regexp = new RegExp("^" + prefix);
_cache[key] = _cache[className].filter(function (elm) {
return regexp.test(elm);
});
}
}
completions = _cache[key];
} catch (e) {
try {
Object.keys(obj).forEach(function (k) {
if (obj.hasOwnProperty(k) && k.indexOf(prefix) === 0) {
completions.push(k);
}
});
completions.sort();
} catch (e) {
// Ignore...
}
}
return completions;
@ -67,14 +103,12 @@ REPL._expResStringifyReplacer = function (k, v) {
mock = {},
funcToStr = "[Function]";
// If the result of the last evaluated expression is a REPLCompletable object
// If the result of the last evaluated expression is an object
if (k === "" //< only first level of recursive calls
&& REPL._lastEval
&& REPL._lastEval._isCompletable
&& REPL._lastEval._isCompletable() === true) {
&& REPL._lastEval) {
// Get all the completions for the REPLCompletable object we are going to pretty-print
iarr = REPL._lastEval._getCompletions("");
// Get all the completions for the object we are going to pretty-print
iarr = REPL._getCompletions(REPL._lastEval);
for (i in iarr) {
if (typeof(v[iarr[i]]) !== "undefined") {
// add a reference to this "real" property into the mock object
@ -96,3 +130,5 @@ REPL._expResStringifyReplacer = function (k, v) {
return v;
};
})();

View File

@ -1,86 +0,0 @@
#include "replcompletable.h"
// public:
REPLCompletable::REPLCompletable(QObject *parent)
: QObject(parent),
mCompletionsInitialised(false)
{ }
REPLCompletable::~REPLCompletable()
{ }
bool REPLCompletable::_isCompletable()
{
return true;
}
QStringList REPLCompletable::_getCompletions(const QString &prefix)
{
// First time this method is invoked, initialise the completions (lazy init)
if (!mCompletionsInitialised) {
initCompletions();
mCompletionsInitialised = true;
}
// If no prefix provided?
if (prefix.isEmpty()) {
// Return all the possible completions
return REPLCompletable::getCompletionsIndex()->values(
this->metaObject()->className());
}
// make a key to store the (new) completions list
QString cacheKey = QString("%1-%2").arg(this->metaObject()->className()).arg(prefix);
// If a list of completion withi this key is not already in the cache
if (!getCompletionsCache()->contains(cacheKey)) {
// Loop over the completions and pick the one that match the given prefix
QStringList allCompletions = REPLCompletable::getCompletionsIndex()->values(
this->metaObject()->className());
QStringList *matchingPrefixCompletions = new QStringList();
QStringList::iterator i;
for (i = allCompletions.begin(); i != allCompletions.end(); ++i) {
if (((QString) *i).startsWith(prefix)) {
matchingPrefixCompletions->append((QString) *i);
}
}
// Store the result in the cache
getCompletionsCache()->insert(cacheKey, matchingPrefixCompletions);
}
return *(getCompletionsCache()->object(cacheKey));
}
// protected:
void REPLCompletable::addCompletion(const char *completion)
{
addCompletion(QString(completion));
}
void REPLCompletable::addCompletion(QString completion)
{
// Accept a completion only if it's unique per MetaObject ClassName
if (!REPLCompletable::getCompletionsIndex()->contains(this->metaObject()->className(), completion)) {
REPLCompletable::getCompletionsIndex()->insert(this->metaObject()->className(), completion);
}
}
// private:
QMultiHash<const char *, QString> *REPLCompletable::getCompletionsIndex()
{
static QMultiHash<const char *, QString> *compIndex = NULL;
if (!compIndex) {
compIndex = new QMultiHash<const char *, QString>();
}
return compIndex;
}
QCache<QString, QStringList> *REPLCompletable::getCompletionsCache()
{
static QCache<QString, QStringList> *compCache = NULL;
if (!compCache) {
compCache = new QCache<QString, QStringList>();
}
return compCache;
}

View File

@ -1,60 +0,0 @@
#ifndef REPLCOMPLETABLE_H
#define REPLCOMPLETABLE_H
#include <QMultiHash>
#include <QStringList>
#include <QCache>
/**
* This subclass of QObject is used by the REPL to better control
* what is "shown" of a QObject exposed in the Javascript Runtime.
*
* By default the JS environment will see all the slots and the Q_INVOKABLE
* of a "exposed" QObject. But also some extra QObject specific methods
* that, in our case, we prefer not to list during REPL autocompletion
* listing or expression result prettyfication.
*/
class REPLCompletable : public QObject
{
Q_OBJECT
public:
REPLCompletable(QObject *parent = 0);
virtual ~REPLCompletable();
Q_INVOKABLE bool _isCompletable();
Q_INVOKABLE QStringList _getCompletions(const QString &prefixToComplete);
protected:
/**
* Used by sublcasses to register a possible "completion".
*
* @param completion Array of characters representing a function/property
* that will be listed as possible completion
*/
void addCompletion(const char *completion);
/**
* Used by sublcasses to register a possible "completion".
*
* @param completion String representing a function/property
* that will be listed as possible completion
*/
void addCompletion(QString completion);
private:
/**
* This is where subclasses should use REPLCompletable#addCompletion(...)
* to declare/register their completions for the REPL.
* This ensures that ONLY if a REPL is actually requested by the user,
* we bother registering the completion strings.
*/
virtual void initCompletions() = 0;
static QMultiHash<const char *, QString> *getCompletionsIndex();
static QCache<QString, QStringList> *getCompletionsCache();
private:
bool mCompletionsInitialised;
};
#endif // REPLCOMPLETABLE_H

View File

@ -38,7 +38,7 @@
#include "../env.h"
System::System(QObject *parent) :
REPLCompletable(parent)
QObject(parent)
, m_stdout((File *)NULL)
, m_stderr((File *)NULL)
, m_stdin((File *)NULL)
@ -201,16 +201,3 @@ QObject *System::_stdin() {
return m_stdin;
}
void System::initCompletions()
{
addCompletion("pid");
addCompletion("args");
addCompletion("env");
addCompletion("platform");
addCompletion("os");
addCompletion("isSSLSupported");
addCompletion("stdin");
addCompletion("stdout");
addCompletion("stderr");
}

View File

@ -37,11 +37,10 @@
#include <QVariant>
#include "filesystem.h"
#include "replcompletable.h"
// This class implements the CommonJS System/1.0 spec.
// See: http://wiki.commonjs.org/wiki/System/1.0
class System : public REPLCompletable
class System : public QObject
{
Q_OBJECT
Q_PROPERTY(qint64 pid READ pid)
@ -81,7 +80,6 @@ private:
QStringList m_args;
QVariant m_env;
QMap<QString, QVariant> m_os;
virtual void initCompletions();
File *m_stdout;
File *m_stderr;
File *m_stdin;

View File

@ -295,7 +295,7 @@ private:
WebPage::WebPage(QObject *parent, const QUrl &baseUrl)
: REPLCompletable(parent)
: QObject(parent)
, m_navigationLocked(false)
, m_mousePos(QPoint(0, 0))
, m_ownsPages(true)
@ -1451,59 +1451,4 @@ void WebPage::setupFrame(QWebFrame *frame)
injectCallbacksObjIntoFrame(frame == NULL ? m_mainFrame : frame, m_callbacks);
}
void WebPage::initCompletions()
{
// Add completion for the Dynamic Properties of the 'webpage' object
// properties
addCompletion("clipRect");
addCompletion("content");
addCompletion("libraryPath");
addCompletion("settings");
addCompletion("viewportSize");
addCompletion("ownsPages");
addCompletion("windowName");
addCompletion("pages");
addCompletion("pagesWindowName");
addCompletion("frameName");
addCompletion("framesName");
addCompletion("framesCount");
addCompletion("cookies");
// functions
addCompletion("evaluate");
addCompletion("includeJs");
addCompletion("injectJs");
addCompletion("open");
addCompletion("release");
addCompletion("render");
addCompletion("renderBase64");
addCompletion("sendEvent");
addCompletion("uploadFile");
addCompletion("getPage");
addCompletion("switchToFrame");
addCompletion("switchToMainFrame");
addCompletion("switchToParentFrame");
addCompletion("switchToFocusedFrame");
addCompletion("addCookie");
addCompletion("deleteCookie");
addCompletion("clearCookies");
addCompletion("setContent");
// callbacks
addCompletion("onAlert");
addCompletion("onCallback");
addCompletion("onPrompt");
addCompletion("onConfirm");
addCompletion("onFilePicker");
addCompletion("onConsoleMessage");
addCompletion("onInitialized");
addCompletion("onLoadStarted");
addCompletion("onLoadFinished");
addCompletion("onResourceRequested");
addCompletion("onResourceReceived");
addCompletion("onUrlChanged");
addCompletion("onNavigationRequested");
addCompletion("onError");
addCompletion("onPageCreated");
addCompletion("onClosing");
}
#include "webpage.moc"

View File

@ -36,8 +36,6 @@
#include <QWebPage>
#include <QWebFrame>
#include "replcompletable.h"
class Config;
class CustomPage;
class WebpageCallbacks;
@ -45,7 +43,7 @@ class NetworkAccessManager;
class QWebInspector;
class Phantom;
class WebPage: public REPLCompletable, public QWebFrame::PrintCallback
class WebPage : public QObject, public QWebFrame::PrintCallback
{
Q_OBJECT
Q_PROPERTY(QString title READ title)
@ -492,8 +490,6 @@ private:
bool javaScriptConfirm(const QString &msg);
bool javaScriptPrompt(const QString &msg, const QString &defaultValue, QString *result);
virtual void initCompletions();
private:
CustomPage *m_customWebPage;
NetworkAccessManager *m_networkAccessManager;

View File

@ -93,7 +93,7 @@ static void *callback(mg_event event,
}
WebServer::WebServer(QObject *parent)
: REPLCompletable(parent)
: QObject(parent)
, m_ctx(0)
{
setObjectName("WebServer");
@ -268,23 +268,11 @@ bool WebServer::handleRequest(mg_event event, mg_connection *conn, const mg_requ
return true;
}
void WebServer::initCompletions()
{
// Add completion for the Dynamic Properties of the 'webpage' object
// properties
addCompletion("clipRect");
// functions
addCompletion("listen");
addCompletion("close");
// callbacks
addCompletion("onNewRequest");
}
//BEGIN WebServerResponse
WebServerResponse::WebServerResponse(mg_connection* conn, QSemaphore* close)
: REPLCompletable()
: QObject()
, m_conn(conn)
, m_statusCode(200)
, m_headersSent(false)
@ -472,15 +460,4 @@ void WebServerResponse::setHeaders(const QVariantMap &headers)
m_headers = headers;
}
void WebServerResponse::initCompletions()
{
// Add completion for the Dynamic Properties of the 'webpage' object
// properties
addCompletion("statusCode");
addCompletion("headers");
// functions
addCompletion("writeHead");
addCompletion("write");
}
//END WebServerResponse

View File

@ -36,7 +36,6 @@
#include <QSemaphore>
#include "mongoose.h"
#include "replcompletable.h"
class Config;
@ -47,7 +46,7 @@ class WebServerResponse;
*
* see also: modules/webserver.js
*/
class WebServer : public REPLCompletable
class WebServer : public QObject
{
Q_OBJECT
Q_PROPERTY(QString port READ port)
@ -84,9 +83,6 @@ signals:
public:
bool handleRequest(mg_event event, mg_connection *conn, const mg_request_info *request);
private:
virtual void initCompletions();
private:
mg_context *m_ctx;
QString m_port;
@ -99,7 +95,7 @@ private:
/**
* Outgoing HTTP response to client.
*/
class WebServerResponse : public REPLCompletable
class WebServerResponse : public QObject
{
Q_OBJECT
@ -146,9 +142,6 @@ public slots:
/// set all headers
void setHeaders(const QVariantMap &headers);
private:
virtual void initCompletions();
private:
mg_connection *m_conn;
int m_statusCode;