From f9ff17fddea2a3622b4b7a909e51161f8308316f Mon Sep 17 00:00:00 2001 From: bruneton Date: Thu, 4 Apr 2013 10:46:11 +0000 Subject: [PATCH] PyConsole: new class PyConsole_EnhConsole offering autocompletion. A new interpreter PyConsole_EnhInterp has also been created for this purpose and the LightApp_PyInterp and SalomeApp_PyInterp have been tweaked accordingly. --- src/LightApp/LightApp_Application.cxx | 2 +- src/LightApp/LightApp_PyInterp.cxx | 2 +- src/LightApp/LightApp_PyInterp.h | 4 +- src/PyConsole/CMakeLists.txt | 5 + src/PyConsole/PyConsole_Console.cxx | 44 ++- src/PyConsole/PyConsole_Console.h | 21 +- src/PyConsole/PyConsole_EnhEditor.cxx | 349 ++++++++++++++++++++++++ src/PyConsole/PyConsole_EnhEditor.h | 90 ++++++ src/PyConsole/PyConsole_EnhInterp.cxx | 154 +++++++++++ src/PyConsole/PyConsole_EnhInterp.h | 67 +++++ src/PyConsole/PyConsole_Event.h | 7 + src/PyConsole/PyConsole_Request.cxx | 39 ++- src/PyConsole/PyConsole_Request.h | 38 ++- src/PyInterp/PyInterp_Dispatcher.cxx | 4 +- src/PyInterp/PyInterp_Event.h | 3 +- src/PyInterp/PyInterp_Request.cxx | 39 --- src/PyInterp/PyInterp_Request.h | 2 +- src/SalomeApp/SalomeApp_Application.cxx | 2 +- src/SalomeApp/SalomeApp_PyInterp.cxx | 2 +- src/SalomeApp/SalomeApp_PyInterp.h | 4 +- 20 files changed, 817 insertions(+), 61 deletions(-) create mode 100644 src/PyConsole/PyConsole_EnhEditor.cxx create mode 100644 src/PyConsole/PyConsole_EnhEditor.h create mode 100644 src/PyConsole/PyConsole_EnhInterp.cxx create mode 100644 src/PyConsole/PyConsole_EnhInterp.h diff --git a/src/LightApp/LightApp_Application.cxx b/src/LightApp/LightApp_Application.cxx index 9142c8da6..8dd648f27 100644 --- a/src/LightApp/LightApp_Application.cxx +++ b/src/LightApp/LightApp_Application.cxx @@ -1839,7 +1839,7 @@ QWidget* LightApp_Application::createWindow( const int flag ) #ifndef DISABLE_PYCONSOLE else if ( flag == WT_PyConsole ) { - PyConsole_Console* pyCons = new PyConsole_Console( desktop(),new LightApp_PyInterp()); + PyConsole_Console* pyCons = new PyConsole_EnhConsole( desktop(),new LightApp_PyInterp()); pyCons->setWindowTitle( tr( "PYTHON_CONSOLE" ) ); pyCons->setFont(resourceMgr()->fontValue( "PyConsole", "font" )); pyCons->setIsShowBanner(resourceMgr()->booleanValue( "PyConsole", "show_banner", true )); diff --git a/src/LightApp/LightApp_PyInterp.cxx b/src/LightApp/LightApp_PyInterp.cxx index d6179eca3..5bc90b951 100644 --- a/src/LightApp/LightApp_PyInterp.cxx +++ b/src/LightApp/LightApp_PyInterp.cxx @@ -32,7 +32,7 @@ * calls initialize method defined in base class, which calls virtual methods * initstate & initcontext redefined here. */ -LightApp_PyInterp::LightApp_PyInterp(): PyConsole_Interp() +LightApp_PyInterp::LightApp_PyInterp(): PyConsole_EnhInterp() { } diff --git a/src/LightApp/LightApp_PyInterp.h b/src/LightApp/LightApp_PyInterp.h index 22fdb87f0..d13d6bd85 100644 --- a/src/LightApp/LightApp_PyInterp.h +++ b/src/LightApp/LightApp_PyInterp.h @@ -23,9 +23,9 @@ #ifndef _LIGHTAPP_PYINTERP_H_ #define _LIGHTAPP_PYINTERP_H_ -#include // this include must be first (see PyInterp_base.h)! +#include // this include must be first (see PyInterp_base.h)! -class LightApp_PyInterp : public PyConsole_Interp +class LightApp_PyInterp : public PyConsole_EnhInterp { public: LightApp_PyInterp(); diff --git a/src/PyConsole/CMakeLists.txt b/src/PyConsole/CMakeLists.txt index 48a250f63..00d0f49ca 100755 --- a/src/PyConsole/CMakeLists.txt +++ b/src/PyConsole/CMakeLists.txt @@ -34,6 +34,7 @@ SET(COMMON_LIBS ${PYTHON_LIBRARIES} ${QT_LIBRARIES} ${SALOMELocalTrace} qtx suit SET(GUI_HEADERS PyConsole_Editor.h + PyConsole_EnhEditor.h PyConsole_Console.h ) QT4_WRAP_CPP(GUI_HEADERS_MOC ${GUI_HEADERS}) @@ -41,7 +42,9 @@ QT4_WRAP_CPP(GUI_HEADERS_MOC ${GUI_HEADERS}) SET(PyConsole_SOURCES PyConsole_Console.cxx PyConsole_Editor.cxx + PyConsole_EnhEditor.cxx PyConsole_Interp.cxx + PyConsole_EnhInterp.cxx PyConsole_Event.cxx PyConsole_Request.cxx ) @@ -61,7 +64,9 @@ SET(COMMON_HEADERS_H PyConsole.h PyConsole_Console.h PyConsole_Editor.h + PyConsole_EnhEditor.h PyConsole_Interp.h + PyConsole_EnhInterp.h PyConsole_Event.h PyConsole_Request.h ) diff --git a/src/PyConsole/PyConsole_Console.cxx b/src/PyConsole/PyConsole_Console.cxx index 8fbd17296..58944c956 100644 --- a/src/PyConsole/PyConsole_Console.cxx +++ b/src/PyConsole/PyConsole_Console.cxx @@ -30,7 +30,7 @@ #include "PyConsole_Interp.h" /// !!! WARNING !!! THIS INCLUDE MUST BE VERY FIRST !!! #include "PyConsole_Console.h" -#include "PyConsole_Editor.h" +#include "PyConsole_EnhEditor.h" #include @@ -49,8 +49,7 @@ \param interp python interpreter */ PyConsole_Console::PyConsole_Console( QWidget* parent, PyConsole_Interp* interp ) -: QWidget( parent ), - myEditor( 0 ) +: QWidget( parent ) { // create python interpreter myInterp = interp; @@ -75,6 +74,13 @@ PyConsole_Console::PyConsole_Console( QWidget* parent, PyConsole_Interp* interp createActions(); } +/** + * Protected constructor. + */ +PyConsole_Console::PyConsole_Console( QWidget* parent, PyConsole_Interp* i, PyConsole_Editor* e) + : QWidget (parent), myEditor(e), myInterp(i) +{} + /*! \brief Destructor. @@ -324,3 +330,35 @@ void PyConsole_Console::updateActions() myActions[PasteId]->setEnabled( !myEditor->isReadOnly() && !QApplication::clipboard()->text().isEmpty() ); myActions[SelectAllId]->setEnabled( !myEditor->document()->isEmpty() ); } + +/** + * Similar to constructor of the base class but using enhanced objects. + * TODO: this should really be done in a factory to avoid code duplication. + * @param parent + * @param interp + */ +PyConsole_EnhConsole::PyConsole_EnhConsole( QWidget* parent, PyConsole_EnhInterp* interp) + : PyConsole_Console(parent, interp, 0) +{ + // create python interpreter + myInterp = interp; + if ( !myInterp ) + myInterp = new PyConsole_EnhInterp(); + + // initialize Python interpretator + myInterp->initialize(); + + // create editor console + QVBoxLayout* lay = new QVBoxLayout( this ); + lay->setMargin( 0 ); + myEditor = new PyConsole_EnhEditor( static_cast(myInterp), this ); + char* synchronous = getenv("PYTHON_CONSOLE_SYNC"); + if (synchronous && atoi(synchronous)) + { + myEditor->setIsSync(true); + } + myEditor->viewport()->installEventFilter( this ); + lay->addWidget( myEditor ); + + createActions(); +} diff --git a/src/PyConsole/PyConsole_Console.h b/src/PyConsole/PyConsole_Console.h index 6258ee97f..9da256727 100644 --- a/src/PyConsole/PyConsole_Console.h +++ b/src/PyConsole/PyConsole_Console.h @@ -28,6 +28,7 @@ #include "PyConsole.h" +#include "PyConsole_EnhInterp.h" #include #include #include @@ -81,14 +82,30 @@ public: void setMenuActions( const int ); int menuActions() const; -private: +protected: void createActions(); void updateActions(); -private: + PyConsole_Console( QWidget* parent, PyConsole_Interp*, PyConsole_Editor*); + + PyConsole_Interp* myInterp; //!< python interpreter PyConsole_Editor* myEditor; //!< python console editor widget QMap myActions; //!< menu actions list }; +/** + * Enhance console object providing auto-completion. + * Similar to PyConsole_Console except that an enhanced interpreter and enhanced editor + * are encapsulated. + */ +class PYCONSOLE_EXPORT PyConsole_EnhConsole: public PyConsole_Console +{ + Q_OBJECT + +public: + PyConsole_EnhConsole( QWidget* parent, PyConsole_EnhInterp* interp = 0); + virtual ~PyConsole_EnhConsole() {} +}; + #endif // PYCONSOLE_CONSOLE_H diff --git a/src/PyConsole/PyConsole_EnhEditor.cxx b/src/PyConsole/PyConsole_EnhEditor.cxx new file mode 100644 index 000000000..aa73d152f --- /dev/null +++ b/src/PyConsole/PyConsole_EnhEditor.cxx @@ -0,0 +1,349 @@ +// Copyright (C) 2007-2013 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// +// Author : Adrien Bruneton (CEA/DEN) +// Created on: 4 avr. 2013 + +#include "PyConsole.h" +#include + +#include +#include +#include +#include +#include + +#include "PyConsole_EnhEditor.h" +#include "PyConsole_EnhInterp.h" +#include "PyConsole_Request.h" +#include "PyInterp_Dispatcher.h" + +// Initialize list of valid separators +static const char * tmp_a[] = {" ", "(", "[","+", "-", "*", "/", ";", "^", "="}; +const std::vector PyConsole_EnhEditor::SEPARATORS = \ + std::vector(tmp_a, tmp_a + sizeof(tmp_a)/sizeof(tmp_a[0])); + +PyConsole_EnhEditor::PyConsole_EnhEditor(PyConsole_EnhInterp * interp, QWidget * parent) : + PyConsole_Editor(interp, parent), + _tab_mode(false), + _cursor_pos(-1) +{ + document()->setUndoRedoEnabled(true); +} + +void PyConsole_EnhEditor::keyPressEvent ( QKeyEvent* event) +{ + // check if is pressed + bool ctrlPressed = event->modifiers() & Qt::ControlModifier; + // check if is pressed + bool shftPressed = event->modifiers() & Qt::ShiftModifier; + + if (event->key() == Qt::Key_Tab && !shftPressed) + { + if (!ctrlPressed) + handleTab(); + else + { + clearCompletion(); + handleBackTab(); + } + PyConsole_Editor::keyPressEvent(event); + } + else + { + if (!ctrlPressed) + { + clearCompletion(); + _cursor_pos = -1; + } + PyConsole_Editor::keyPressEvent(event); + } +} + +void PyConsole_EnhEditor::mousePressEvent(QMouseEvent* e) +{ + clearCompletion(); + _cursor_pos = -1; + PyConsole_Editor::mousePressEvent(e); +} + +void PyConsole_EnhEditor::clearCompletion() +{ + // Delete completion text if present + if (_tab_mode) + { + // Remove completion display + document()->undo(); + // Remove trailing line return: + QTextCursor tc(textCursor()); + tc.setPosition(document()->characterCount()-1); + setTextCursor(tc); + textCursor().deletePreviousChar(); + // TODO: before wait for any TAB event to be completed + static_cast(myInterp)->clearCompletion(); + } + _tab_mode = false; +} + +void PyConsole_EnhEditor::handleTab() +{ + if (_tab_mode) + { + // Already tab mode - nothing to do ! + return; + } + + QTextCursor cursor(textCursor()); + + // Cursor at end of input + cursor.movePosition(QTextCursor::End); + setTextCursor(cursor); + + // Save cursor position if needed + if (_cursor_pos == -1) + _cursor_pos = textCursor().position(); + + // get last line + QTextBlock par = document()->end().previous(); + if ( !par.isValid() ) return; + + // Switch to completion mode + _tab_mode = true; + + QString cmd = par.text().mid(promptSize()); + + // Post completion request + // Editor will be informed via a custom event that completion has been run + PyInterp_Request* req = createTabRequest(cmd); + PyInterp_Dispatcher::Get()->Exec(req); +} + +void PyConsole_EnhEditor::handleBackTab() +{ + QTextCursor cursor(textCursor()); + + if (_cursor_pos == -1) + { + // Invalid cursor position - we can't do anything: + return; + } + // Ensure cursor is at the end of command line + cursor.setPosition(_cursor_pos); + cursor.movePosition(QTextCursor::EndOfLine); + //setCursor(cursor); + + // Delete last completed text + int i = cursor.position() - _cursor_pos; + cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, i); + cursor.removeSelectedText(); + _cursor_pos = -1; +} + +PyInterp_Request* PyConsole_EnhEditor::createTabRequest( const QString& input ) +{ + // Parse input to extract on what part the dir() has to be executed + QString input2(input); + + // Split up to the last syntaxical separator + int lastSp = -1; + for (std::vector::const_iterator i = SEPARATORS.begin(); i != SEPARATORS.end(); i++) + { + int j = input2.lastIndexOf(*i); + if (j > lastSp) + lastSp = j; + } + if (lastSp >= 0) + input2 = input.mid(lastSp+1); + + // Detect a qualified name (with a point) + int lastPt = input2.lastIndexOf(QString(".")); + + // Split the 2 surrounding parts of the qualified name + if (lastPt != -1) + { + _compl_before_point = input2.left(lastPt); + _compl_after_point = input2.mid(lastPt+1); + } + else + { + // No point found - do a global matching - + // (the following will call dir() with an empty string) + _compl_after_point = input2; + _compl_before_point = QString(""); + } + + return new CompletionCommand( static_cast(myInterp), _compl_before_point, + _compl_after_point, this, isSync() ); +} + +/** + * Format completion results - this is where we should create 3 columns etc ... + * @param matches list of possible completions + * @param result return value + */ +void PyConsole_EnhEditor::formatCompletion(const std::vector & matches, QString & result) const +{ + int sz = matches.size(); + + if (sz > MAX_COMPLETIONS) + { + sz = MAX_COMPLETIONS; + result.append("[Too many matches! Displaying first ones only ...]\n"); + } + + for (int i = 0; i < sz; ++i) + { + result.append(matches[i]); + result.append("\n"); + } +} + +void PyConsole_EnhEditor::customEvent( QEvent* event ) +{ + std::vector matches; + QString first_match, comple_text, doc, base; + QTextCursor cursor(textCursor()); + QTextBlockFormat bf; + QTextCharFormat cf; + PyConsole_EnhInterp * interp = static_cast(myInterp); + + switch( event->type() ) + { + case PyInterp_Event::ES_TAB_COMPLETE_OK: + // Extract corresponding matches from the interpreter + matches = interp->getLastMatches(); + + if (matches.size() == 0) + { + // Completion successful but nothing returned. + _tab_mode = false; + _cursor_pos = -1; + return; + } + + // Only one match - complete directly and update doc string window + doc = interp->getDocStr(); + if (matches.size() == 1) + { + first_match = matches[0].mid(_compl_after_point.size()); + cursor.insertText(first_match); + _tab_mode = false; + if (doc == QString("")) + emit updateDoc(formatDocHTML("(no documentation available)\n")); + else + emit updateDoc(formatDocHTML(doc)); + } + else + { + // Detect if there is a common base to all available completion + // In this case append this base to the text already + extractCommon(matches, base); + first_match = base.mid(_compl_after_point.size()); + cursor.insertText(first_match); + + // If this happens to match exaclty the first completion + // also provide doc + if (base == matches[0]) + { + doc = formatDocHTML(doc); + emit updateDoc(doc); + } + + // Print all matching completion in a "undo-able" block + cursor.insertBlock(); + cursor.beginEditBlock(); + + // Insert all matches + QTextCharFormat cf; + cf.setForeground(QBrush(Qt::darkGreen)); + cursor.setCharFormat(cf); + formatCompletion(matches, comple_text); + cursor.insertText(comple_text); + cursor.endEditBlock(); + } + break; + case PyInterp_Event::ES_TAB_COMPLETE_ERR: + // Tab completion was unsuccessful, switch off mode: + _tab_mode = false; + _cursor_pos = -1; + break; +// case PyEnhInterp_Event::ES_MULTI_PASTE: +// multilinePaste(); +// break; + default: + PyConsole_Editor::customEvent( event ); + break; + } +} + +/** + * Extract the common leading part of all strings in matches. + * @param matches + * @param result + */ +void PyConsole_EnhEditor::extractCommon(const std::vector & matches, QString & result) const +{ + result = ""; + int charIdx = 0; + + if (matches.size() < 2) + return; + + while (true) + { + if (charIdx >= matches[0].size()) + return; + QChar ch = matches[0][charIdx]; + for (int j = 1; j < matches.size(); j++) + if (charIdx >= matches[j].size() || matches[j][charIdx] != ch) + return; + result += ch; + charIdx++; + } +} + +QString PyConsole_EnhEditor::formatDocHTML(const QString & doc) const +{ + QString templ = QString("\n ") + + QString(" ") + + QString(" ") + + QString("\n") + + QString("

") + + QString("%1

") + + QString("

%2

") + + QString(""); + + QString fst, rest(""); + + // Extract first line of doc + int idx = doc.indexOf("\n"); + if (idx > 0) + { + fst = doc.left(idx); + rest = doc.mid(idx+1); + } + else + { + fst = doc; + } + + fst = fst.replace("\n", " "); + rest = rest.replace("\n", " "); + return templ.arg(fst).arg(rest); +} diff --git a/src/PyConsole/PyConsole_EnhEditor.h b/src/PyConsole/PyConsole_EnhEditor.h new file mode 100644 index 000000000..8a9bc2804 --- /dev/null +++ b/src/PyConsole/PyConsole_EnhEditor.h @@ -0,0 +1,90 @@ +// Copyright (C) 2007-2013 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// +// Author : Adrien Bruneton (CEA/DEN) +// Created on: 4 avr. 2013 + +#ifndef PYCONSOLE_ENHEDITOR_H_ +#define PYCONSOLE_ENHEDITOR_H_ + +#include "PyConsole.h" + +#include "PyConsole_Editor.h" +#include + +class PyConsole_EnhInterp; + +/** + * Enhanced Python editor handling tab completion. + */ +class PyConsole_EnhEditor: public PyConsole_Editor +{ + Q_OBJECT; + +public: + PyConsole_EnhEditor(PyConsole_EnhInterp * interp, QWidget * parent=0); + virtual ~PyConsole_EnhEditor() {} + +signals: + /** + * Signal emitted by the editor widget when the doc string should be updated. + * @param doc a HTML block with the formatted doc string. + * TODO: for now this signal is left uncaught. + */ + void updateDoc(QString doc); + +protected: + /** List of separators identifying the last parsable token for completion */ + static const std::vector SEPARATORS; + + /** Maximum number of completions shown at once */ + static const int MAX_COMPLETIONS = 70; + + /** Are we in completion mode */ + bool _tab_mode; + + /** String on which the dir() comamnd is executed */ + QString _compl_before_point; + /** String on which the results of the dir() are matched */ + QString _compl_after_point; + + /** Cursor position when is hit */ + int _cursor_pos; + +// std::stack _multi_line_content; + + // Overrides: + virtual void keyPressEvent ( QKeyEvent* event); + virtual void customEvent( QEvent* event); + virtual void mousePressEvent( QMouseEvent* event ); +// virtual void insertFromMimeData(const QMimeData * source); + + virtual PyInterp_Request* createTabRequest( const QString& input ); + virtual void handleTab(); + virtual void handleBackTab(); + virtual void clearCompletion(); + virtual void formatCompletion(const std::vector & matches, QString & result) const; + virtual QString formatDocHTML(const QString & doc) const; +// virtual void multilinePaste(); + +private: + void extractCommon(const std::vector & matches, QString & result) const; + +}; + +#endif /* PYCONSOLE_ENHEDITOR_H_ */ diff --git a/src/PyConsole/PyConsole_EnhInterp.cxx b/src/PyConsole/PyConsole_EnhInterp.cxx new file mode 100644 index 000000000..c64808622 --- /dev/null +++ b/src/PyConsole/PyConsole_EnhInterp.cxx @@ -0,0 +1,154 @@ +// Copyright (C) 2007-2013 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// +// Author : Adrien Bruneton (CEA/DEN) +// Created on: 4 avr. 2013 + + +#include "PyConsole.h" + +#include "PyConsole_EnhInterp.h" + +#include +#include +#include + +static const char * tmp_k[] = {"and", "as", "assert", "break", "class", + "continue", "def", "del", + "elif", "else", "except", "exec", "finally", "for", "from", "global", "if", + "import", "in", "is", "lambda", "not", "or", "pass", "print", "raise", + "return", "try", "while", "with", "yield"}; + +const std::vector PyConsole_EnhInterp::PYTHON_KEYWORDS = \ + std::vector(tmp_k, tmp_k+sizeof(tmp_k)/sizeof(tmp_k[0])); + +/*! + \brief Run Python dir() command and saves the result internally in _lastPy + \param dirArgument Python expression to pass to the dir command. The parsing of what the + user actually started typing is dedicated to the caller + \param startMatch string representing the begining of the patter to be completed. For example when + the user types "a_string_variable.rsp ", this is "rsp". + \return command exit status - 0 = success +*/ +int PyConsole_EnhInterp::runDirCommand(const QString & dirArgument, const QString & startMatch) +{ + int ret; + std::vector v; + + clearCompletion(); + if ( (ret = runDirAndExtract(dirArgument, startMatch, _last_matches)) ) + return ret; + + // If dirArgument is empty, we append the __builtins__ + if (dirArgument.isEmpty()) + { + if ( (ret = runDirAndExtract(QString("__builtins__"), startMatch, _last_matches, false)) ) + return ret; + + // ... and we match on Python's keywords as well: + for (std::vector::const_iterator it = PYTHON_KEYWORDS.begin(); it != PYTHON_KEYWORDS.end(); it++) + if ((*it).startsWith(startMatch)) + _last_matches.push_back(*it); + } + + // Try to get doc string of the first match + if (_last_matches.size() > 0) + { + QString cmd(""); + if (dirArgument.trimmed() != "") + cmd = dirArgument + "."; + cmd += _last_matches[0] + ".__doc__"; + PyObject * str = PyRun_String(cmd.toStdString().c_str(), Py_eval_input, _g, _g); + if (!str || str == Py_None || !PyString_Check(str)) + { + if (!str) + PyErr_Clear(); + _doc_str = ""; + } + else + _doc_str = QString(PyString_AsString(str)); + Py_XDECREF(str); + } + + // The command has been successfully executed + return 0; +} + +/** + * See runDirCommand(). + * @param dirArgument see runDirCommand() + * @param startMatch see runDirCommand() + * @param[out] result the list of matches + * @param discardSwig if true a regular expression is used to discard all static method generated + * by SWIG. typically: MEDCouplingUMesh_Blabla + * @return -1 if the call to dir() or the parsing of the result failed, 0 otherwise. + */ +int PyConsole_EnhInterp::runDirAndExtract(const QString& dirArgument, + const QString & startMatch, std::vector & result, + bool discardSwig) const +{ + QRegExp re("^[A-Z].+_[A-Z]+[a-z]+.+$"); // discard SWIG static method, e.g. MEDCouplingUMesh_Blabla + QString command("dir(" + dirArgument + ")"); + PyObject * plst = PyRun_String(command.toStdString().c_str(), Py_eval_input, _g, _g); + if(!plst || plst == Py_None) { + if(!plst) + PyErr_Clear(); + + Py_XDECREF(plst); + return -1; + } + + // Extract the returned list and convert it to a vector<> + if (!PySequence_Check(plst)) + { + // Should never happen ... + //std::cerr << "not a list!" << std::endl; + Py_XDECREF(plst); + return -1; + } + + // Convert plst to a vector of QString + int n = PySequence_Length(plst); + for (int i = 0; i < n; i++) + { + PyObject * it; + it = PySequence_GetItem(plst, i); + QString s(PyString_AsString(it)); + // if the method is not from swig, not static (guessed from the reg exp) and matches + // what is already there + if (s.startsWith(startMatch)) + if(!discardSwig || (!re.exactMatch(s) && !s.contains("swig"))) + result.push_back(s); + Py_DECREF(it); + } + Py_DECREF(plst); + + return 0; +} + +/** + * Clear internal members containing the last completion results. + */ +void PyConsole_EnhInterp::clearCompletion() +{ + _last_matches.resize(0); + _doc_str = QString(""); +} + + + diff --git a/src/PyConsole/PyConsole_EnhInterp.h b/src/PyConsole/PyConsole_EnhInterp.h new file mode 100644 index 000000000..5e19ca53c --- /dev/null +++ b/src/PyConsole/PyConsole_EnhInterp.h @@ -0,0 +1,67 @@ +// Copyright (C) 2007-2013 CEA/DEN, EDF R&D +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// +// Author : Adrien Bruneton (CEA/DEN) +// Created on: 4 avr. 2013 + + +#ifndef PYCONSOLE_ENHINTERP_H_ +#define PYCONSOLE_ENHINTERP_H_ + +#include "PyConsole.h" + +#include +#include "PyConsole_Interp.h" + +#include +#include + +/** + * Enhanced Python interpreter used for auto-completion. + * This extends PyConsole_Interp with an API wrapping the Python dir() command nicely. + */ +class PyConsole_EnhInterp: public PyConsole_Interp +{ +public: + PyConsole_EnhInterp() + : PyConsole_Interp(), _last_matches(0), _doc_str("") + {} + + virtual ~PyConsole_EnhInterp() {} + + const std::vector& getLastMatches() const { return _last_matches; } + const QString & getDocStr() const { return _doc_str; } + + virtual int runDirCommand(const QString& dirArgument, const QString& startMatch); + virtual void clearCompletion(); + +protected: + /** Hard coded list of Python keywords */ + static const std::vector PYTHON_KEYWORDS; + + /** Last computed matches */ + std::vector _last_matches; + /** Doc string of the first match - when only one match it will be displayed by the Editor*/ + QString _doc_str; + + virtual int runDirAndExtract(const QString& dirArgument, const QString & startMatch, + std::vector & result, bool discardSwig=true) const; + +}; + +#endif /* PYCONSOLE_ENHINTERP_H_ */ diff --git a/src/PyConsole/PyConsole_Event.h b/src/PyConsole/PyConsole_Event.h index b99a58d58..ac6a75fa2 100644 --- a/src/PyConsole/PyConsole_Event.h +++ b/src/PyConsole/PyConsole_Event.h @@ -24,6 +24,8 @@ #ifndef PYCONSOLE_EVENT_H #define PYCONSOLE_EVENT_H +#include "PyConsole.h" + #include #include @@ -40,6 +42,7 @@ public: /*! \brief Constructor \param c message text (python trace) + \param isError default to false - if true indicates that an error is being printed. */ PrintEvent( const char* c, bool isError = false) : QEvent( (QEvent::Type)EVENT_ID ), myText( c ), errorFlag(isError) @@ -50,6 +53,10 @@ public: \return message text (python trace) */ QString text() const { return myText; } + + /** + * @return true if this is an error message + */ bool isError() const { return errorFlag; } protected: diff --git a/src/PyConsole/PyConsole_Request.cxx b/src/PyConsole/PyConsole_Request.cxx index 7b0095862..4e3c134a2 100644 --- a/src/PyConsole/PyConsole_Request.cxx +++ b/src/PyConsole/PyConsole_Request.cxx @@ -23,8 +23,8 @@ #include "PyInterp_Event.h" #include "PyConsole_Event.h" -#include "PyInterp_Interp.h" -#include "PyConsole_Editor.h" +#include "PyConsole_EnhInterp.h" +#include "PyConsole_EnhEditor.h" #include @@ -48,9 +48,40 @@ void ExecCommand::execute() } } -QEvent* ExecCommand::createEvent() const +QEvent* ExecCommand::createEvent() { if ( IsSync() ) QCoreApplication::sendPostedEvents( listener(), PrintEvent::EVENT_ID ); - return new PyInterp_Event( myState, (PyInterp_Request*)this ); + return new PyInterp_Event( myState, this ); } + + + +CompletionCommand::CompletionCommand( PyConsole_EnhInterp* theInterp, + const QString& input, + const QString& startMatch, + PyConsole_EnhEditor* theListener, + bool sync) + : PyInterp_LockRequest( theInterp, theListener, sync ), + _tabSuccess(false), _dirArg(input), _startMatch(startMatch) +{} + +void CompletionCommand::execute() +{ + PyConsole_EnhInterp * interp = static_cast(getInterp()); + int ret = interp->runDirCommand( _dirArg, _startMatch); + if (ret == 0) + _tabSuccess = true; + else + _tabSuccess = false; +} + +QEvent* CompletionCommand::createEvent() +{ + int typ = _tabSuccess ? PyInterp_Event::ES_TAB_COMPLETE_OK : PyInterp_Event::ES_TAB_COMPLETE_ERR; + + return new PyInterp_Event( typ, this); +} + + + diff --git a/src/PyConsole/PyConsole_Request.h b/src/PyConsole/PyConsole_Request.h index e4f20801f..d20bcd84a 100644 --- a/src/PyConsole/PyConsole_Request.h +++ b/src/PyConsole/PyConsole_Request.h @@ -23,8 +23,10 @@ #ifndef PYCONSOLE_REQUEST_H_ #define PYCONSOLE_REQUEST_H_ +#include "PyConsole.h" #include "PyInterp_Request.h" +#include #include #include @@ -64,12 +66,46 @@ protected: \brief Create and return a notification event. \return new notification event */ - virtual QEvent* createEvent() const; + virtual QEvent* createEvent(); private: QString myCommand; //!< Python command int myState; //!< Python command execution status }; +class PyConsole_EnhInterp; +class PyConsole_EnhEditor; + +class CompletionCommand : public PyInterp_LockRequest +{ +public: + /*! + Constructor. + Creates a new python completion request. + \param theInterp python interpreter + \param input string containing the dir() command to be executed + \param startMatch part to be matched with the results of the dir() command + \param theListener widget to get the notification messages + \param sync if True the request is processed synchronously + */ + CompletionCommand( PyConsole_EnhInterp* theInterp, + const QString& input, + const QString& startMatch, + PyConsole_EnhEditor* theListener, + bool sync = false ); + + +protected: + /** List of separators identifying the last parsable token for completion */ + static const std::vector SEPARATORS; + + QString _dirArg; + QString _startMatch; + bool _tabSuccess; + + virtual void execute(); + virtual QEvent* createEvent(); + +}; #endif /* PYCONSOLE_REQUEST_H_ */ diff --git a/src/PyInterp/PyInterp_Dispatcher.cxx b/src/PyInterp/PyInterp_Dispatcher.cxx index acaeafec9..44052093a 100755 --- a/src/PyInterp/PyInterp_Dispatcher.cxx +++ b/src/PyInterp/PyInterp_Dispatcher.cxx @@ -64,9 +64,9 @@ void PyInterp_Request::Destroy( PyInterp_Request* request ) delete request; } -QEvent* PyInterp_Request::createEvent() const +QEvent* PyInterp_Request::createEvent() { - return new PyInterp_Event( PyInterp_Event::ES_NOTIFY, (PyInterp_Request*)this ); + return new PyInterp_Event( PyInterp_Event::ES_NOTIFY, this ); } void PyInterp_Request::processEvent( QObject* o ) diff --git a/src/PyInterp/PyInterp_Event.h b/src/PyInterp/PyInterp_Event.h index cab3dcd8d..44e40ba9c 100644 --- a/src/PyInterp/PyInterp_Event.h +++ b/src/PyInterp/PyInterp_Event.h @@ -55,7 +55,8 @@ class PYINTERP_EXPORT PyInterp_Event : public QEvent public: //Execution state - enum { ES_NOTIFY = QEvent::User + 5000, ES_OK, ES_ERROR, ES_INCOMPLETE, ES_LAST }; + enum { ES_NOTIFY = QEvent::User + 5000, ES_OK, ES_ERROR, ES_INCOMPLETE, + ES_TAB_COMPLETE_OK, ES_TAB_COMPLETE_ERR, ES_LAST }; PyInterp_Event( int type, PyInterp_Request* request ) : QEvent( (QEvent::Type)type ), myRequest( request ) {} diff --git a/src/PyInterp/PyInterp_Request.cxx b/src/PyInterp/PyInterp_Request.cxx index 4ace03545..e8032c1ad 100644 --- a/src/PyInterp/PyInterp_Request.cxx +++ b/src/PyInterp/PyInterp_Request.cxx @@ -25,42 +25,3 @@ #include "PyInterp_Request.h" -/*! - \class ExecCommand - \brief Python command execution request. - \internal -*/ -class ExecCommand : public PyInterp_LockRequest -{ -public: - /*! - \brief Constructor. - - Creates new python command execution request. - \param theInterp python interpreter - \param theCommand python command - \param theListener widget to get the notification messages - \param sync if True the request is processed synchronously - */ - ExecCommand( PyInterp_Interp* theInterp, - const QString& theCommand, - PyConsole_Editor* theListener, - bool sync = false ); - -protected: - /*! - \brief Execute the python command in the interpreter and - get its execution status. - */ - virtual void execute(); - - /*! - \brief Create and return a notification event. - \return new notification event - */ - virtual QEvent* createEvent() const; - - QString myCommand; //!< Python command - int myState; //!< Python command execution status -}; - diff --git a/src/PyInterp/PyInterp_Request.h b/src/PyInterp/PyInterp_Request.h index 15310648b..9a4291b36 100644 --- a/src/PyInterp/PyInterp_Request.h +++ b/src/PyInterp/PyInterp_Request.h @@ -67,7 +67,7 @@ protected: virtual void execute() = 0; // Should be redefined in successors, contains actual request code - virtual QEvent* createEvent() const; + virtual QEvent* createEvent(); // This method can be overridden to customize notification event creation virtual void processEvent( QObject* ); diff --git a/src/SalomeApp/SalomeApp_Application.cxx b/src/SalomeApp/SalomeApp_Application.cxx index 159997416..33d73927d 100644 --- a/src/SalomeApp/SalomeApp_Application.cxx +++ b/src/SalomeApp/SalomeApp_Application.cxx @@ -929,7 +929,7 @@ QWidget* SalomeApp_Application::createWindow( const int flag ) } else if ( flag == WT_PyConsole ) { - PyConsole_Console* pyCons = new PyConsole_Console( desktop(), new SalomeApp_PyInterp() ); + PyConsole_Console* pyCons = new PyConsole_EnhConsole( desktop(), new SalomeApp_PyInterp() ); pyCons->setWindowTitle( tr( "PYTHON_CONSOLE" ) ); pyCons->setFont(resourceMgr()->fontValue( "PyConsole", "font" )); pyCons->setIsShowBanner(resourceMgr()->booleanValue( "PyConsole", "show_banner", true )); diff --git a/src/SalomeApp/SalomeApp_PyInterp.cxx b/src/SalomeApp/SalomeApp_PyInterp.cxx index 185b01f30..a0301b6ca 100755 --- a/src/SalomeApp/SalomeApp_PyInterp.cxx +++ b/src/SalomeApp/SalomeApp_PyInterp.cxx @@ -37,7 +37,7 @@ * initstate & initcontext redefined here. */ SalomeApp_PyInterp::SalomeApp_PyInterp(): - PyConsole_Interp(), myFirstRun( true ) + PyConsole_EnhInterp(), myFirstRun( true ) { } diff --git a/src/SalomeApp/SalomeApp_PyInterp.h b/src/SalomeApp/SalomeApp_PyInterp.h index 84c7906fe..d457cf763 100755 --- a/src/SalomeApp/SalomeApp_PyInterp.h +++ b/src/SalomeApp/SalomeApp_PyInterp.h @@ -27,9 +27,9 @@ #ifndef _SalomeApp_PYINTERP_H_ #define _SalomeApp_PYINTERP_H_ -#include // this include must be first (see PyInterp_base.h)! +#include // this include must be first (see PyInterp_base.h)! -class SalomeApp_PyInterp : public PyConsole_Interp +class SalomeApp_PyInterp : public PyConsole_EnhInterp { public: SalomeApp_PyInterp(); -- 2.39.2