Kaynağa Gözat

ENH: ctkPytonConsole: Move cursor between parentheses if parameters detected

It improves the ctk python console so that the cursor is
automatically moved between the parentheses for a function
when autocompleted with TAB (this is only done when the
funtion has arguments).

This commit is based on commit 42b84f1 originally associated with
topic python-autocomplete-parameter-detection from Christopher
Mullins.

It has been rebased and adapted by Mayeul Chassagnard.

It fixes issues reported in http://na-mic.org/Mantis/view.php?id=1227

Co-authored-by: Mayeul Chassagnard <mayeul.chassagnard@kitware.com>
Co-authored-by: Jean-Christophe Fillion-Robin <jchris.fillionr@kitware.com>
Christopher Mullins 9 yıl önce
ebeveyn
işleme
fb587146f3

+ 101 - 1
Libs/Scripting/Python/Core/ctkAbstractPythonManager.cpp

@@ -347,7 +347,8 @@ QStringList ctkAbstractPythonManager::pythonAttributes(const QString& pythonVari
   PyObject* dict = PyImport_GetModuleDict();
 
   // Split module by '.' and retrieve the object associated if the last module
-  PyObject* object = 0;
+  QString precedingModule = module;
+  PyObject* object = ctkAbstractPythonManager::pythonModule(precedingModule);
   PyObject* prevObject = 0;
   QStringList moduleList = module.split(".", QString::SkipEmptyParts);
   foreach(const QString& module, moduleList)
@@ -429,6 +430,105 @@ QStringList ctkAbstractPythonManager::pythonAttributes(const QString& pythonVari
 }
 
 //-----------------------------------------------------------------------------
+PyObject* ctkAbstractPythonManager::pythonObject(const QString& variableNameAndFunction)
+{
+  QStringList variableNameAndFunctionList = variableNameAndFunction.split(".");
+  QString compareFunction = variableNameAndFunctionList.last();
+  variableNameAndFunctionList.removeLast();
+  QString pythonVariableName = variableNameAndFunctionList.last();
+  variableNameAndFunctionList.removeLast();
+  QString precedingModules = variableNameAndFunctionList.join(".");
+
+  Q_ASSERT(PyThreadState_GET()->interp);
+  PyObject* object = ctkAbstractPythonManager::pythonModule(precedingModules);
+  if (!object)
+    {
+    return NULL;
+    }
+  if (!pythonVariableName.isEmpty())
+    {
+    QStringList tmpNames = pythonVariableName.split('.');
+    for (int i = 0; i < tmpNames.size() && object; ++i)
+      {
+      QByteArray tmpName = tmpNames.at(i).toLatin1();
+      PyObject* prevObj = object;
+      if (PyDict_Check(object))
+        {
+        object = PyDict_GetItemString(object, tmpName.data());
+        Py_XINCREF(object);
+        }
+      else
+        {
+        object = PyObject_GetAttrString(object, tmpName.data());
+        }
+        Py_DECREF(prevObj);
+      }
+    }
+  PyObject* finalPythonObject = NULL;
+  if (object)
+    {
+    PyObject* keys = PyObject_Dir(object);
+    if (keys)
+      {
+      PyObject* key;
+      PyObject* value;
+      int nKeys = PyList_Size(keys);
+      for (int i = 0; i < nKeys; ++i)
+        {
+        key = PyList_GetItem(keys, i);
+        value = PyObject_GetAttr(object, key);
+        if (!value)
+          {
+          continue;
+          }
+        QString keyStr = PyString_AsString(key);
+        if (keyStr.operator ==(compareFunction))
+          {
+          finalPythonObject = value;
+          break;
+          }
+        Py_DECREF(value);
+        }
+      Py_DECREF(keys);
+      }
+    Py_DECREF(object);
+    }
+  return finalPythonObject;
+}
+
+//-----------------------------------------------------------------------------
+PyObject* ctkAbstractPythonManager::pythonModule(const QString& module)
+{
+  PyObject* dict = PyImport_GetModuleDict();
+  PyObject* object = 0;
+  PyObject* prevObject = 0;
+  QStringList moduleList = module.split(".", QString::KeepEmptyParts);
+  if (!dict)
+    {
+    return object;
+    }
+  foreach(const QString& module, moduleList)
+    {
+    object = PyDict_GetItemString(dict, module.toAscii().data());
+    if (prevObject)
+      {
+      Py_DECREF(prevObject);
+      }
+    if (!object)
+      {
+      break;
+      }
+    Py_INCREF(object); // This is required, otherwise python destroys object.
+    if (PyObject_HasAttrString(object, "__dict__"))
+      {
+      dict = PyObject_GetAttrString(object, "__dict__");
+      }\
+    prevObject = object;
+    }
+  return object;
+}
+
+//-----------------------------------------------------------------------------
 void ctkAbstractPythonManager::addObjectToPythonMain(const QString& name, QObject* obj)
 {
   PythonQtObjectPtr main = ctkAbstractPythonManager::mainContext();

+ 10 - 0
Libs/Scripting/Python/Core/ctkAbstractPythonManager.h

@@ -26,6 +26,9 @@
 #include <QList>
 #include <QStringList>
 
+// PythonQt includes
+#include <PythonQtPythonInclude.h> // For PyObject
+
 // CTK includes
 #include "ctkScriptingPythonCoreExport.h"
 
@@ -108,6 +111,13 @@ public:
                                const QString& module = QLatin1String("__main__"),
                                bool appendParenthesis = false) const;
 
+  /// Given a string of the form "<modulename1>[.<modulenameN>...]" containing modules, return the final module as a PyObject*
+  static PyObject* pythonModule(const QString &module);
+
+  /// Given a string of the form "<modulename1>[.<modulenameN>...].correspondingObject, return the final object as a PyObject*
+  /// \sa pythonModule
+  static PyObject* pythonObject(const QString& variableNameAndFunction);
+
   /// Returns True if python is initialized
   /// \sa pythonInitialized
   bool isPythonInitialized()const;

+ 214 - 88
Libs/Scripting/Python/Widgets/ctkPythonConsole.cpp

@@ -82,117 +82,243 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 class ctkPythonConsoleCompleter : public ctkConsoleCompleter
 {
 public:
-  ctkPythonConsoleCompleter(ctkAbstractPythonManager& pythonManager)
-    : PythonManager(pythonManager)
-    {
-    this->setParent(&pythonManager);
-    }
+  ctkPythonConsoleCompleter(ctkAbstractPythonManager& pythonManager);
 
-  virtual void updateCompletionModel(const QString& completion)
-    {
-    // Start by clearing the model
-    this->setModel(0);
+  virtual int cursorOffset(const QString& completion);
+  virtual void updateCompletionModel(const QString& completion);
+
+protected:
+  bool isUserDefinedFunction(const QString &pythonFunctionName);
+  bool isBuiltInFunction(const QString &pythonFunctionName);
+  int parameterCountBuiltInFunction(const QString& pythonFunctionName);
+  int parameterCountUserDefinedFunction(const QString& pythonFunctionName);
+  int parameterCountFromDocumentation(const QString& pythonFunctionPath);
+
+  ctkAbstractPythonManager& PythonManager;
+};
 
-    // Don't try to complete the empty string
-    if (completion.isEmpty())
+//----------------------------------------------------------------------------
+ctkPythonConsoleCompleter::ctkPythonConsoleCompleter(ctkAbstractPythonManager& pythonManager)
+  : PythonManager(pythonManager)
+  {
+  this->setParent(&pythonManager);
+  }
+
+//----------------------------------------------------------------------------
+int ctkPythonConsoleCompleter::cursorOffset(const QString& completion)
+{
+  QString allTextFromShell = completion;
+  int parameterCount = 0;
+  int cursorOffset = 0;
+  if (allTextFromShell.contains("()"))
+    {
+    allTextFromShell.replace("()", "");
+    QStringList lineSplit = allTextFromShell.split(".", QString::KeepEmptyParts);
+    QString functionName = lineSplit.at(lineSplit.length()-1);
+    QStringList builtinFunctionPath = QStringList() << "__main__" << "__builtins__";
+    QStringList userDefinedFunctionPath = QStringList() << "__main__";
+    if (this->isBuiltInFunction(functionName))
       {
-      return;
+      parameterCount = this->parameterCountBuiltInFunction(QStringList(builtinFunctionPath+lineSplit).join("."));
       }
-
-    // Search backward through the string for usable characters
-    QString textToComplete;
-    for (int i = completion.length()-1; i >= 0; --i)
+    else if (this->isUserDefinedFunction(functionName))
+      {
+      parameterCount = this->parameterCountUserDefinedFunction(QStringList(userDefinedFunctionPath+lineSplit).join("."));
+      }
+    else
       {
-      QChar c = completion.at(i);
-      if (c.isLetterOrNumber() || c == '.' || c == '_')
+      QStringList variableNameAndFunctionList = userDefinedFunctionPath + lineSplit;
+      QString variableNameAndFunction = variableNameAndFunctionList.join(".");
+      parameterCount = this->parameterCountFromDocumentation(variableNameAndFunction);
+      }
+    }
+  if (parameterCount > 0)
+    {
+    cursorOffset = 1;
+    }
+  return cursorOffset;
+}
+
+
+//---------------------------------------------------------------------------
+bool ctkPythonConsoleCompleter::isUserDefinedFunction(const QString &pythonFunctionName)
+{
+  return this->PythonManager.pythonAttributes(pythonFunctionName).contains("__call__");
+}
+
+//---------------------------------------------------------------------------
+bool ctkPythonConsoleCompleter::isBuiltInFunction(const QString &pythonFunctionName)
+{
+  return this->PythonManager.pythonAttributes(pythonFunctionName, QLatin1String("__main__.__builtins__")).contains("__call__");
+}
+
+//---------------------------------------------------------------------------
+int ctkPythonConsoleCompleter::parameterCountBuiltInFunction(const QString& pythonFunctionName)
+{
+  int parameterCount = 0;
+  qDebug() << "In parameterCountBuiltInFunction";
+  PyObject* pFunction = this->PythonManager.pythonModule(pythonFunctionName);
+  if (pFunction && PyObject_HasAttrString(pFunction, "__doc__"))
+    {
+    PyObject* pDoc = PyObject_GetAttrString(pFunction, "__doc__");
+    QString docString = PyString_AsString(pDoc);
+    QString argumentExtract = docString.mid(docString.indexOf("(")+1, docString.indexOf(")") - docString.indexOf("(")-1);
+    QStringList arguments = argumentExtract.split(",", QString::SkipEmptyParts);
+    parameterCount = arguments.count();
+    Py_DECREF(pDoc);
+    Py_DECREF(pFunction);
+    }
+  return parameterCount;
+}
+
+//----------------------------------------------------------------------------
+int ctkPythonConsoleCompleter::parameterCountUserDefinedFunction(const QString& pythonFunctionName)
+{
+  int parameterCount = 0;
+  PyObject* pFunction = this->PythonManager.pythonModule(pythonFunctionName);
+  if (PyCallable_Check(pFunction))
+    {
+    PyObject* fc = PyObject_GetAttrString(pFunction, "func_code");
+    if (fc)
+       {
+      PyObject* ac = PyObject_GetAttrString(fc, "co_argcount");
+      if (ac)
         {
-        textToComplete.prepend(c);
+        parameterCount = PyInt_AsLong(ac);
+        Py_DECREF(ac);
         }
-      else
+      Py_DECREF(fc);
+       }
+    }
+  return parameterCount;
+}
+
+//----------------------------------------------------------------------------
+int ctkPythonConsoleCompleter::parameterCountFromDocumentation(const QString& pythonFunctionPath)
+{
+  int parameterCount = 0;
+  PyObject* pFunction = this->PythonManager.pythonObject(pythonFunctionPath);
+  if (pFunction)
+    {
+    if (PyObject_HasAttrString(pFunction, "__call__"))
+      {
+      PyObject* pDoc = PyObject_GetAttrString(pFunction, "__doc__");
+      if (PyString_Check(pDoc))
         {
-        break;
+        QString docString = PyString_AsString(pDoc);
+        QString argumentExtract = docString.mid(docString.indexOf("(")+1, docString.indexOf(")") - docString.indexOf("(")-1);
+        QStringList arguments = argumentExtract.split(",", QString::SkipEmptyParts);
+        parameterCount = arguments.count();
         }
       }
+    Py_DECREF(pFunction);
+    }
+  return parameterCount;
+}
+
+void ctkPythonConsoleCompleter::updateCompletionModel(const QString& completion)
+{
+  // Start by clearing the model
+  this->setModel(0);
+
+  // Don't try to complete the empty string
+  if (completion.isEmpty())
+    {
+    return;
+    }
 
-    // Split the string at the last dot, if one exists
-    QString lookup;
-    QString compareText = textToComplete;
-    int dot = compareText.lastIndexOf('.');
-    if (dot != -1)
+  // Search backward through the string for usable characters
+  QString textToComplete;
+  for (int i = completion.length()-1; i >= 0; --i)
+    {
+    QChar c = completion.at(i);
+    if (c.isLetterOrNumber() || c == '.' || c == '_')
       {
-      lookup = compareText.mid(0, dot);
-      compareText = compareText.mid(dot+1);
+      textToComplete.prepend(c);
       }
-
-    // Lookup python names
-    QStringList attrs;
-    if (!lookup.isEmpty() || !compareText.isEmpty())
+    else
       {
-      bool appendParenthesis = true;
-      attrs = this->PythonManager.pythonAttributes(lookup, QLatin1String("__main__"), appendParenthesis);
-      attrs << this->PythonManager.pythonAttributes(lookup, QLatin1String("__main__.__builtins__"),
-                                                    appendParenthesis);
-      attrs.removeDuplicates();
+      break;
       }
+   }
+
+  // Split the string at the last dot, if one exists
+  QString lookup;
+  QString compareText = textToComplete;
+  int dot = compareText.lastIndexOf('.');
+  if (dot != -1)
+    {
+    lookup = compareText.mid(0, dot);
+    compareText = compareText.mid(dot+1);
+    }
 
-    // Initialize the completion model
-    if (!attrs.isEmpty())
+  // Lookup python names
+  QStringList attrs;
+  if (!lookup.isEmpty() || !compareText.isEmpty())
+    {
+    bool appendParenthesis = true;
+    attrs = this->PythonManager.pythonAttributes(lookup, QLatin1String("__main__"), appendParenthesis);
+    attrs << this->PythonManager.pythonAttributes(lookup, QLatin1String("__main__.__builtins__"),
+                                                  appendParenthesis);
+    attrs.removeDuplicates();
+    }
+
+  // Initialize the completion model
+  if (!attrs.isEmpty())
+    {
+    this->setCompletionMode(QCompleter::PopupCompletion);
+    this->setModel(new QStringListModel(attrs, this));
+    this->setCaseSensitivity(Qt::CaseInsensitive);
+    this->setCompletionPrefix(compareText.toLower());
+
+    //qDebug() << "completion" << completion;
+    // If a dot as been entered and if an item of possible
+    // choices matches one of the preference list, it will be selected.
+    QModelIndex preferredIndex = this->completionModel()->index(0, 0);
+    int dotCount = completion.count('.');
+    if (dotCount == 0 || completion.at(completion.count() - 1) == '.')
       {
-      this->setCompletionMode(QCompleter::PopupCompletion);
-      this->setModel(new QStringListModel(attrs, this));
-      this->setCaseSensitivity(Qt::CaseInsensitive);
-      this->setCompletionPrefix(compareText.toLower());
-      
-      //qDebug() << "completion" << completion;
-      // If a dot as been entered and if an item of possible
-      // choices matches one of the preference list, it will be selected.
-      QModelIndex preferredIndex = this->completionModel()->index(0, 0);
-      int dotCount = completion.count('.');
-      if (dotCount == 0 || completion.at(completion.count() - 1) == '.')
+      foreach(const QString& pref, this->AutocompletePreferenceList)
         {
-        foreach(const QString& pref, this->AutocompletePreferenceList)
+        //qDebug() << "pref" << pref;
+        int dotPref = pref.count('.');
+        // Skip if there are dots in pref and if the completion has already more dots
+        // than the pref
+        if ((dotPref != 0) && (dotCount > dotPref))
+          {
+          continue;
+          }
+        // Extract string before the last dot
+        int lastDot = pref.lastIndexOf('.');
+        QString prefBeforeLastDot;
+        if (lastDot != -1)
+          {
+          prefBeforeLastDot = pref.left(lastDot);
+          }
+        //qDebug() << "prefBeforeLastDot" << prefBeforeLastDot;
+        if (!prefBeforeLastDot.isEmpty() && QString::compare(prefBeforeLastDot, lookup) != 0)
           {
-          //qDebug() << "pref" << pref;
-          int dotPref = pref.count('.');
-          // Skip if there are dots in pref and if the completion has already more dots 
-          // than the pref
-          if ((dotPref != 0) && (dotCount > dotPref))
-            {
-            continue;
-            }
-          // Extract string before the last dot
-          int lastDot = pref.lastIndexOf('.');
-          QString prefBeforeLastDot;
-          if (lastDot != -1)
-            {
-            prefBeforeLastDot = pref.left(lastDot);
-            }
-          //qDebug() << "prefBeforeLastDot" << prefBeforeLastDot;
-          if (!prefBeforeLastDot.isEmpty() && QString::compare(prefBeforeLastDot, lookup) != 0)
-            {
-            continue;
-            }
-          QString prefAfterLastDot = pref;
-          if (lastDot != -1 )
-            {
-            prefAfterLastDot = pref.right(pref.size() - lastDot - 1);
-            }
-          //qDebug() << "prefAfterLastDot" << prefAfterLastDot;
-          QModelIndexList list = this->completionModel()->match(
-                this->completionModel()->index(0, 0), Qt::DisplayRole, QVariant(prefAfterLastDot));
-          if (list.count() > 0)
-            {
-            preferredIndex = list.first();
-            break;
-            }
+          continue;
+          }
+        QString prefAfterLastDot = pref;
+        if (lastDot != -1 )
+          {
+          prefAfterLastDot = pref.right(pref.size() - lastDot - 1);
+          }
+        //qDebug() << "prefAfterLastDot" << prefAfterLastDot;
+        QModelIndexList list = this->completionModel()->match(
+              this->completionModel()->index(0, 0), Qt::DisplayRole, QVariant(prefAfterLastDot));
+        if (list.count() > 0)
+          {
+          preferredIndex = list.first();
+          break;
           }
         }
-
-      this->popup()->setCurrentIndex(preferredIndex);
       }
+
+    this->popup()->setCurrentIndex(preferredIndex);
     }
-  ctkAbstractPythonManager& PythonManager;
-};
+}
 
 //----------------------------------------------------------------------------
 // ctkPythonConsolePrivate

+ 10 - 0
Libs/Widgets/ctkConsole.cpp

@@ -737,6 +737,7 @@ void ctkConsolePrivate::printWelcomeMessage()
 //-----------------------------------------------------------------------------
 void ctkConsolePrivate::insertCompletion(const QString& completion)
 {
+  Q_Q(ctkConsole);
   QTextCursor tc = this->textCursor();
   tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
   if (tc.selectedText()==".")
@@ -751,6 +752,15 @@ void ctkConsolePrivate::insertCompletion(const QString& completion)
     tc.insertText(completion);
     this->setTextCursor(tc);
     }
+  tc.movePosition(QTextCursor::StartOfBlock);
+  tc.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
+  QString shellLine = tc.selectedText();
+  shellLine.replace(q->ps1(), "");
+  tc.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor);
+  this->setTextCursor(tc);
+  int cursorOffset = this->Completer->cursorOffset(shellLine);
+  tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, cursorOffset);
+  this->setTextCursor(tc);
   this->updateCommandBuffer();
 }
 

+ 4 - 0
Libs/Widgets/ctkConsole.h

@@ -267,6 +267,10 @@ public:
   /// the line.
   virtual void updateCompletionModel(const QString& str) = 0;
 
+  /// Given the current completion, returns the number by which the
+  /// cursor should be shifted to the left.
+  virtual int cursorOffset(const QString& completion) = 0;
+
   /// Returns the autocomplete preference list
   QStringList autocompletePreferenceList();