Преглед на файлове

Merge pull request #664 from jcfr/1227-python-console-completer-cursor-between-parentheses-rebased

1227 python console completer cursor between parentheses rebased
Jean-Christophe Fillion-Robin преди 9 години
родител
ревизия
335e386b21

+ 100 - 0
Libs/Scripting/Python/Core/Testing/Cpp/ctkAbstractPythonManagerTest.cpp

@@ -55,8 +55,16 @@ private Q_SLOTS:
   void testExecuteFile_data();
 
   //void testPythonAttributes(); // TODO
+
+  void testPythonModule();
+  void testPythonModule_data();
+
+  void testPythonObject();
+  void testPythonObject_data();
 };
 
+Q_DECLARE_METATYPE(PyObject*)
+
 // ----------------------------------------------------------------------------
 void ctkAbstractPythonManagerTester::testDefaults()
 {
@@ -263,5 +271,97 @@ void ctkAbstractPythonManagerTester::testExecuteFile_data()
 }
 
 // ----------------------------------------------------------------------------
+void ctkAbstractPythonManagerTester::testPythonModule()
+{
+  QFETCH(QString, pythonCode);
+  QFETCH(QString, inputModuleList);
+  QFETCH(QString, expectedReturnedString);
+
+  this->PythonManager.executeString(pythonCode);
+  PyObject* returnedPyObject = this->PythonManager.pythonModule(inputModuleList);
+  PyObject* returnedPyString;
+  if(returnedPyObject)
+    {
+    returnedPyString = PyObject_GetAttrString(returnedPyObject, "__name__");
+    }
+  else
+    {
+    returnedPyString = PyString_FromString("");
+    }
+  QString returnedString = PyString_AsString(returnedPyString);
+  QCOMPARE(returnedString, expectedReturnedString);
+}
+
+// ----------------------------------------------------------------------------
+void ctkAbstractPythonManagerTester::testPythonModule_data()
+{
+  QTest::addColumn<QString>("pythonCode");
+  QTest::addColumn<QString>("inputModuleList");
+  QTest::addColumn<QString>("expectedReturnedString");
+
+  QTest::newRow("0") << ""
+                     << "__main__"
+                     << "__main__";
+
+  QTest::newRow("1") << ""
+                     << "__main__.__builtins__"
+                     << "__builtin__";
+
+  QTest::newRow("2") << "class foo: pass"
+                     << "__main__.foo"
+                     << "foo";
+
+  QTest::newRow("3") << ""
+                     << "__main__.NOT_A_MODULE"
+                     << "";
+}
+
+//-----------------------------------------------------------------------------
+void ctkAbstractPythonManagerTester::testPythonObject()
+{
+  QFETCH(QString, pythonCode);
+  QFETCH(QString, inputPythonVariableNameAndFunction);
+  QFETCH(QString, expectedReturnedString);
+
+  this->PythonManager.executeString(pythonCode);
+  PyObject* returnedPyObject = this->PythonManager.pythonObject(inputPythonVariableNameAndFunction);
+  PyObject* returnedPyObjectString;
+  if (returnedPyObject)
+    {
+    returnedPyObjectString = PyObject_GetAttrString(returnedPyObject, "__name__");
+    }
+  else
+    {
+    returnedPyObjectString = PyString_FromString("");
+    }
+  QString returnedString = PyString_AsString(returnedPyObjectString);
+  QCOMPARE(returnedString, expectedReturnedString);
+}
+
+//-----------------------------------------------------------------------------
+void ctkAbstractPythonManagerTester::testPythonObject_data()
+{
+  QTest::addColumn<QString>("pythonCode");
+  QTest::addColumn<QString>("inputPythonVariableNameAndFunction");
+  QTest::addColumn<QString>("expectedReturnedString");
+
+  QTest::newRow("0") << "foo = []"
+                     << "__main__.foo.append"
+                     << "append";
+
+  QTest::newRow("1") << ""
+                     << "__main__.__builtins__.dir"
+                     << "dir";
+
+  QTest::newRow("2") << "class foo: bar = []"
+                     << "__main__.foo.bar.reverse"
+                     << "reverse";
+
+  QTest::newRow("3") << ""
+                     << "__main__.__builtins__.NOT_A_FUNCTION"
+                     << "";
+}
+
+// ----------------------------------------------------------------------------
 CTK_TEST_MAIN(ctkAbstractPythonManagerTest)
 #include "moc_ctkAbstractPythonManagerTest.cpp"

+ 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;

+ 258 - 84
Libs/Scripting/Python/Widgets/ctkPythonConsole.cpp

@@ -82,117 +82,291 @@ 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);
 
-    // Don't try to complete the empty string
-    if (completion.isEmpty())
-      {
-      return;
-      }
+protected:
+  bool isInUserDefinedClass(const QString &pythonFunctionPath);
+  bool isUserDefinedFunction(const QString &pythonFunctionName);
+  bool isBuiltInFunction(const QString &pythonFunctionName);
+  int parameterCountUserDefinedClassFunction(const QString &pythonFunctionName);
+  int parameterCountBuiltInFunction(const QString& pythonFunctionName);
+  int parameterCountUserDefinedFunction(const QString& pythonFunctionName);
+  int parameterCountFromDocumentation(const QString& pythonFunctionPath);
+
+  ctkAbstractPythonManager& PythonManager;
+};
+
+//----------------------------------------------------------------------------
+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("()", "");
     // Search backward through the string for usable characters
-    QString textToComplete;
-    for (int i = completion.length()-1; i >= 0; --i)
+    QString currentCompletionText;
+    for (int i = allTextFromShell.length()-1; i >= 0; --i)
       {
-      QChar c = completion.at(i);
+      QChar c = allTextFromShell.at(i);
       if (c.isLetterOrNumber() || c == '.' || c == '_')
         {
-        textToComplete.prepend(c);
+        currentCompletionText.prepend(c);
         }
       else
         {
         break;
         }
       }
+    QStringList lineSplit = currentCompletionText.split(".", QString::KeepEmptyParts);
+    QString functionName = lineSplit.at(lineSplit.length()-1);
+    QStringList builtinFunctionPath = QStringList() << "__main__" << "__builtins__";
+    QStringList userDefinedFunctionPath = QStringList() << "__main__";
+    if (this->isBuiltInFunction(functionName))
+      {
+      parameterCount = this->parameterCountBuiltInFunction(QStringList(builtinFunctionPath+lineSplit).join("."));
+      }
+    else if (this->isUserDefinedFunction(functionName))
+      {
+      parameterCount = this->parameterCountUserDefinedFunction(QStringList(userDefinedFunctionPath+lineSplit).join("."));
+      }
+    else if (this->isInUserDefinedClass(currentCompletionText))
+      {
+      // "self" parameter can be ignored
+      parameterCount = this->parameterCountUserDefinedClassFunction(QStringList(userDefinedFunctionPath+lineSplit).join(".")) - 1;
+      }
+    else
+      {
+      QStringList variableNameAndFunctionList = userDefinedFunctionPath + lineSplit;
+      QString variableNameAndFunction = variableNameAndFunctionList.join(".");
+      parameterCount = this->parameterCountFromDocumentation(variableNameAndFunction);
+      }
+    }
+  if (parameterCount > 0)
+    {
+    cursorOffset = 1;
+    }
+  return cursorOffset;
+}
+
+
+//---------------------------------------------------------------------------
+bool ctkPythonConsoleCompleter::isInUserDefinedClass(const QString &pythonFunctionPath)
+{
+  return this->PythonManager.pythonAttributes(pythonFunctionPath).contains("__func__");
+}
+
+//---------------------------------------------------------------------------
+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;
+  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)
+        {
+        parameterCount = PyInt_AsLong(ac);
+        Py_DECREF(ac);
+        }
+      Py_DECREF(fc);
+       }
+    }
+  return parameterCount;
+}
+
+//----------------------------------------------------------------------------
+int ctkPythonConsoleCompleter::parameterCountUserDefinedClassFunction(const QString& pythonFunctionName)
+{
+  int parameterCount = 0;
+  PyObject* pFunction = this->PythonManager.pythonObject(pythonFunctionName);
+  if (PyCallable_Check(pFunction))
+    {
+    PyObject* fc = PyObject_GetAttrString(pFunction, "func_code");
+    if (fc)
+      {
+      PyObject* ac = PyObject_GetAttrString(fc, "co_argcount");
+      if (ac)
+        {
+        parameterCount = PyInt_AsLong(ac);
+        Py_DECREF(ac);
+        }
+      Py_DECREF(fc);
+      }
+    }
+  return parameterCount;
+}
 
-    // Split the string at the last dot, if one exists
-    QString lookup;
-    QString compareText = textToComplete;
-    int dot = compareText.lastIndexOf('.');
-    if (dot != -1)
+//----------------------------------------------------------------------------
+int ctkPythonConsoleCompleter::parameterCountFromDocumentation(const QString& pythonFunctionPath)
+{
+  int parameterCount = 0;
+  PyObject* pFunction = this->PythonManager.pythonObject(pythonFunctionPath);
+  if (pFunction)
+    {
+    if (PyObject_HasAttrString(pFunction, "__call__"))
       {
-      lookup = compareText.mid(0, dot);
-      compareText = compareText.mid(dot+1);
+      PyObject* pDoc = PyObject_GetAttrString(pFunction, "__doc__");
+      if (PyString_Check(pDoc))
+        {
+        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;
+    }
 
-    // Lookup python names
-    QStringList attrs;
-    if (!lookup.isEmpty() || !compareText.isEmpty())
+  // 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 == '_')
+      {
+      textToComplete.prepend(c);
+      }
+    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)
           {
-          //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;
-            }
+          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;
           }
         }
-
-      this->popup()->setCurrentIndex(preferredIndex);
       }
+
+    this->popup()->setCurrentIndex(preferredIndex);
     }
-  ctkAbstractPythonManager& PythonManager;
-};
+}
 
 //----------------------------------------------------------------------------
 // ctkPythonConsolePrivate

+ 11 - 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,16 @@ 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(), "");
+  shellLine.replace(q->ps2(), "");
+  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();