[education/labplot] src: [scripting] added the completion for object members.
Alexander Semke <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 18acdd7ca350756dee60a35c0584fb0253dd9c2c by Alexander Semke.
Committed on 01/08/2026 at 20:22.
Pushed by asemke into branch 'master'.
[scripting] added the completion for object members.
M +110 -0 src/backend/script/python/PythonScriptRuntime.cpp
M +1 -1 src/backend/script/python/PythonScriptRuntime.h
M +12 -0 src/backend/script/python/PythonScriptingHelper.h
M +137 -40 src/frontend/script/ScriptCompletionModel.cpp
M +12 -0 src/frontend/script/ScriptCompletionModel.h
M +16 -0 src/frontend/script/ScriptEditor.cpp
https://invent.kde.org/education/labplot/-/commit/18acdd7ca350756dee60a35c0584fb0253dd9c2c
diff --git a/src/backend/script/python/PythonScriptRuntime.cpp b/src/backend/script/python/PythonScriptRuntime.cpp
index 0f043ab950..b4cbeb740b 100644
--- a/src/backend/script/python/PythonScriptRuntime.cpp
+++ b/src/backend/script/python/PythonScriptRuntime.cpp
@@ -10,6 +10,7 @@
#include "PythonScriptRuntime.h"
#include "PythonLogger.h"
+#include "PythonScriptingHelper.h"
#include "backend/core/Project.h"
#include "backend/core/Settings.h"
#include "backend/script/Script.h"
@@ -908,3 +909,112 @@ QStringList PythonScriptRuntime::getPylabplotSymbols() {
return symbols;
}
+
+// Global helper to get class members (avoids Python.h in frontend)
+QList<PylabplotMemberInfo> getPylabplotClassMembersHelper(const QString& className) {
+ QList<PylabplotMemberInfo> members;
+
+ if (!Py_IsInitialized())
+ return members;
+
+ PyGILState_STATE gil = PyGILState_Ensure();
+
+ // Import pylabplot module
+ PyObject* module = PyImport_ImportModule("pylabplot");
+ if (!module) {
+ PyErr_Clear();
+ PyGILState_Release(gil);
+ return members;
+ }
+
+ // Get the class object
+ QByteArray classNameBytes = className.toUtf8();
+ PyObject* classObj = PyObject_GetAttrString(module, classNameBytes.constData());
+ Py_DECREF(module);
+
+ if (!classObj) {
+ PyErr_Clear();
+ PyGILState_Release(gil);
+ return members;
+ }
+
+ // Check if it's actually a class
+ if (!PyType_Check(classObj)) {
+ Py_DECREF(classObj);
+ PyGILState_Release(gil);
+ return members;
+ }
+
+ // Get all attributes using dir()
+ PyObject* dirList = PyObject_Dir(classObj);
+ if (dirList && PyList_Check(dirList)) {
+ Py_ssize_t size = PyList_Size(dirList);
+ for (Py_ssize_t i = 0; i < size; ++i) {
+ PyObject* nameObj = PyList_GetItem(dirList, i); // Borrowed reference
+ if (!PyUnicode_Check(nameObj))
+ continue;
+
+ QString name = PythonScriptRuntime::pyUnicodeToQString(nameObj);
+ if (name.isEmpty() || name.startsWith(QLatin1Char('_')))
+ continue; // Skip private/special methods
+
+ // Get the attribute object
+ QByteArray nameBytes = name.toUtf8();
+ PyObject* attr = PyObject_GetAttrString(classObj, nameBytes.constData());
+ if (!attr) {
+ PyErr_Clear();
+ continue;
+ }
+
+ PylabplotMemberInfo info;
+ info.name = name;
+
+ // Check if it's a callable (method)
+ info.isMethod = PyCallable_Check(attr);
+ info.isProperty = !info.isMethod;
+
+ // Try to extract signature and docstring
+ if (info.isMethod) {
+ // Get __doc__ if available
+ PyObject* docObj = PyObject_GetAttrString(attr, "__doc__");
+ if (docObj && PyUnicode_Check(docObj)) {
+ QString doc = PythonScriptRuntime::pyUnicodeToQString(docObj);
+ // Extract first line as brief doc
+ int newlinePos = doc.indexOf(QLatin1Char('\n'));
+ if (newlinePos > 0)
+ info.docstring = doc.left(newlinePos).trimmed();
+ else
+ info.docstring = doc.trimmed();
+
+ // Try to extract signature from docstring
+ // Common format: "method(arg1, arg2) -> returnType"
+ static QRegularExpression sigPattern(QStringLiteral(R"((\w+\([^)]*\)))"));
+ QRegularExpressionMatch match = sigPattern.match(info.docstring);
+ if (match.hasMatch())
+ info.signature = match.captured(1);
+ }
+ if (docObj)
+ Py_DECREF(docObj);
+
+ // Try to get return type from __annotations__
+ PyObject* annotationsObj = PyObject_GetAttrString(attr, "__annotations__");
+ if (annotationsObj && PyDict_Check(annotationsObj)) {
+ PyObject* returnObj = PyDict_GetItemString(annotationsObj, "return"); // Borrowed ref
+ if (returnObj && PyUnicode_Check(returnObj))
+ info.returnType = PythonScriptRuntime::pyUnicodeToQString(returnObj);
+ }
+ if (annotationsObj)
+ Py_DECREF(annotationsObj);
+ }
+
+ Py_DECREF(attr);
+ members.append(info);
+ }
+ Py_DECREF(dirList);
+ }
+
+ Py_DECREF(classObj);
+ PyGILState_Release(gil);
+
+ return members;
+}
diff --git a/src/backend/script/python/PythonScriptRuntime.h b/src/backend/script/python/PythonScriptRuntime.h
index 8217a107e8..92316145f5 100644
--- a/src/backend/script/python/PythonScriptRuntime.h
+++ b/src/backend/script/python/PythonScriptRuntime.h
@@ -29,6 +29,7 @@ public:
// Static helper for code completion
static QStringList getPylabplotSymbols();
+ static QString pyUnicodeToQString(PyObject*);
private:
PythonLogger* m_loggerStdOut{nullptr}; // PythonLogger instance to replace sys.stdout in the python interpreter
@@ -53,7 +54,6 @@ private:
static PyTypeObject* getPythonLoggerType();
static PyObject* shibokenConvertToPyObject(PythonLogger*);
static int getPyErrorLine();
- static QString pyUnicodeToQString(PyObject*);
// singletons (shared between PythonScripts)
static bool ready;
diff --git a/src/backend/script/python/PythonScriptingHelper.h b/src/backend/script/python/PythonScriptingHelper.h
index 7c91b6073c..0dcb81c80b 100644
--- a/src/backend/script/python/PythonScriptingHelper.h
+++ b/src/backend/script/python/PythonScriptingHelper.h
@@ -12,8 +12,20 @@
#include <QStringList>
+struct PylabplotMemberInfo {
+ QString name;
+ bool isMethod;
+ bool isProperty;
+ QString signature; // For methods: "method(arg1, arg2)"
+ QString docstring; // Brief documentation
+ QString returnType; // Return type annotation if available
+};
+
// Helper function to get pylabplot symbols without requiring Python.h
// This allows frontend code to query symbols without Python header dependencies
QStringList getPylabplotSymbolsHelper();
+// Get members (methods and properties) of a specific pylabplot class
+QList<PylabplotMemberInfo> getPylabplotClassMembersHelper(const QString& className);
+
#endif // PYTHONSCRIPTINGHELPER_H
diff --git a/src/frontend/script/ScriptCompletionModel.cpp b/src/frontend/script/ScriptCompletionModel.cpp
index 353c74bd4f..0ae5528a54 100644
--- a/src/frontend/script/ScriptCompletionModel.cpp
+++ b/src/frontend/script/ScriptCompletionModel.cpp
@@ -147,57 +147,75 @@ void ScriptCompletionModel::startCompletionRequest() {
auto currentPos = m_pendingView->cursorPosition();
- // Get current word being typed
+ // Detect completion context (global vs member)
+ QString objectName;
+ CompletionContext context = detectContext(m_pendingView, currentPos, objectName);
+
+ // Get current word being typed (the filter text)
auto currentWordRange = m_pendingView->document()->wordRangeAt(currentPos);
QString prefix = m_pendingView->document()->text(currentWordRange);
- // Update user variables from current script
+ // Get current script text
QString scriptText = m_pendingView->document()->text();
updateUserVariables(scriptText);
- // Collect all completion candidates
- QSet<QString> allSymbolsSet;
-
- // Add pylabplot symbols
- for (const auto& item : m_pylabplotSymbols)
- allSymbolsSet.insert(item.name);
-
- // Add Python built-ins
- for (const auto& builtin : m_pythonBuiltins)
- allSymbolsSet.insert(builtin);
-
- // Add user variables
- for (const auto& var : m_userVariables)
- allSymbolsSet.insert(var);
-
- // Filter by prefix
+ // Collect completion candidates based on context
beginResetModel();
m_matches.clear();
- for (const QString& symbol : allSymbolsSet) {
- if (prefix.isEmpty() || symbol.startsWith(prefix, Qt::CaseInsensitive)) {
- CompletionItem item;
- item.name = symbol;
-
- // Determine type
- for (const auto& pylabplotItem : m_pylabplotSymbols) {
- if (pylabplotItem.name == symbol) {
- item.isClass = pylabplotItem.isClass;
- item.isFunction = pylabplotItem.isFunction;
- item.isEnum = pylabplotItem.isEnum;
- break;
- }
+ if (context == CompletionContext::Member) {
+ // Member completion - get members of the object
+ QString typeName = inferType(objectName, scriptText);
+ if (!typeName.isEmpty()) {
+ QList<CompletionItem> members = getMembersForType(typeName);
+ // Filter by prefix
+ for (const auto& member : members) {
+ if (prefix.isEmpty() || member.name.startsWith(prefix, Qt::CaseInsensitive))
+ m_matches.append(member);
}
+ }
+ } else {
+ // Global completion - collect all symbols
+ QSet<QString> allSymbolsSet;
+
+ // Add pylabplot symbols
+ for (const auto& item : m_pylabplotSymbols)
+ allSymbolsSet.insert(item.name);
+
+ // Add Python built-ins
+ for (const auto& builtin : m_pythonBuiltins)
+ allSymbolsSet.insert(builtin);
+
+ // Add user variables
+ for (const auto& var : m_userVariables)
+ allSymbolsSet.insert(var);
+
+ // Filter by prefix
+ for (const QString& symbol : allSymbolsSet) {
+ if (prefix.isEmpty() || symbol.startsWith(prefix, Qt::CaseInsensitive)) {
+ CompletionItem item;
+ item.name = symbol;
+
+ // Determine type
+ for (const auto& pylabplotItem : m_pylabplotSymbols) {
+ if (pylabplotItem.name == symbol) {
+ item.isClass = pylabplotItem.isClass;
+ item.isFunction = pylabplotItem.isFunction;
+ item.isEnum = pylabplotItem.isEnum;
+ break;
+ }
+ }
- // Check if it's a Python built-in
- if (m_pythonBuiltins.contains(symbol))
- item.isFunction = true;
+ // Check if it's a Python built-in
+ if (m_pythonBuiltins.contains(symbol))
+ item.isFunction = true;
- // Check if it's a user variable
- if (m_userVariables.contains(symbol))
- item.isVariable = true;
+ // Check if it's a user variable
+ if (m_userVariables.contains(symbol))
+ item.isVariable = true;
- m_matches.append(item);
+ m_matches.append(item);
+ }
}
}
@@ -279,8 +297,8 @@ bool ScriptCompletionModel::shouldStartCompletion(KTextEditor::View* view, const
if (!insertedText.isEmpty()) {
const QChar lastChar = insertedText.back();
- // Start completion after typing a letter or underscore
- if (lastChar.isLetter() || lastChar == QLatin1Char('_'))
+ // Start completion after typing a letter, underscore, or dot (for member access)
+ if (lastChar.isLetter() || lastChar == QLatin1Char('_') || lastChar == QLatin1Char('.'))
return true;
}
@@ -291,3 +309,82 @@ void ScriptCompletionModel::abortCompletion() {
if (m_debounceTimer && m_debounceTimer->isActive())
m_debounceTimer->stop();
}
+
+ScriptCompletionModel::CompletionContext ScriptCompletionModel::detectContext(KTextEditor::View* view,
+ const KTextEditor::Cursor& cursor,
+ QString& objectName) {
+ if (!view || !view->document())
+ return CompletionContext::Global;
+
+ // Get the line up to the cursor
+ QString line = view->document()->line(cursor.line()).left(cursor.column());
+
+ // Check for member access: "object." or "object.prefix"
+ static QRegularExpression memberPattern(QStringLiteral(R"((\w+)\.(\w*)$)"));
+ QRegularExpressionMatch match = memberPattern.match(line);
+
+ if (match.hasMatch()) {
+ objectName = match.captured(1);
+ return CompletionContext::Member;
+ }
+
+ return CompletionContext::Global;
+}
+
+QString ScriptCompletionModel::inferType(const QString& varName, const QString& scriptText) {
+ // Simple type inference based on assignment patterns
+ // Look for patterns like: varName = ClassName(...) or varName = module.ClassName(...)
+
+ // Pattern 1: Direct class instantiation - varName = ClassName()
+ QString pattern1 = QStringLiteral(R"(\b%1\s*=\s*(\w+)\s*\()").arg(QRegularExpression::escape(varName));
+ QRegularExpression directPattern(pattern1);
+ QRegularExpressionMatch match = directPattern.match(scriptText);
+
+ if (match.hasMatch())
+ return match.captured(1); // Return the class name
+
+ // Pattern 2: Module member access - varName = module.ClassName()
+ QString pattern2 = QStringLiteral(R"(\b%1\s*=\s*\w+\.(\w+)\s*\()").arg(QRegularExpression::escape(varName));
+ QRegularExpression modulePattern(pattern2);
+ match = modulePattern.match(scriptText);
+
+ if (match.hasMatch())
+ return match.captured(1); // Return the class name
+
+ return QString(); // Unknown type
+}
+
+QList<ScriptCompletionModel::CompletionItem> ScriptCompletionModel::getMembersForType(const QString& typeName) {
+ QList<CompletionItem> members;
+
+ if (typeName.isEmpty())
+ return members;
+
+ // Check cache first
+ if (m_memberCache.contains(typeName))
+ return m_memberCache[typeName];
+
+ // Use runtime introspection to get real class members from Python
+ QList<PylabplotMemberInfo> pylabplotMembers = getPylabplotClassMembersHelper(typeName);
+
+ // Convert to CompletionItem format
+ for (const auto& memberInfo : pylabplotMembers) {
+ CompletionItem item;
+ item.name = memberInfo.name;
+ item.signature = memberInfo.signature;
+ item.docstring = memberInfo.docstring;
+ item.isFunction = memberInfo.isMethod;
+ item.isVariable = memberInfo.isProperty;
+ item.isMember = true;
+ item.isClass = false;
+ item.isEnum = false;
+
+ members.append(item);
+ }
+
+ // Cache the results
+ if (!members.isEmpty())
+ m_memberCache[typeName] = members;
+
+ return members;
+}
diff --git a/src/frontend/script/ScriptCompletionModel.h b/src/frontend/script/ScriptCompletionModel.h
index eb77b7a469..8089b18835 100644
--- a/src/frontend/script/ScriptCompletionModel.h
+++ b/src/frontend/script/ScriptCompletionModel.h
@@ -25,10 +25,13 @@ class ScriptCompletionModel : public KTextEditor::CodeCompletionModel,
public:
struct CompletionItem {
QString name;
+ QString signature; // For methods: "method(arg1, arg2)"
+ QString docstring; // Brief documentation
bool isFunction = false;
bool isClass = false;
bool isEnum = false;
bool isVariable = false;
+ bool isMember = false; // Is a class member
};
explicit ScriptCompletionModel(ScriptEditor* parent);
@@ -52,11 +55,17 @@ private Q_SLOTS:
void startCompletionRequest();
private:
+ enum class CompletionContext {
+ Global, // Normal completion (variables, functions, classes)
+ Member, // After '.' - show members
+ };
+
ScriptEditor* m_editor{nullptr};
QList<CompletionItem> m_matches;
QList<CompletionItem> m_pylabplotSymbols;
QStringList m_userVariables;
QStringList m_pythonBuiltins;
+ QMap<QString, QList<CompletionItem>> m_memberCache; // Class -> members map
QTimer* m_debounceTimer{nullptr};
KTextEditor::View* m_pendingView{nullptr};
@@ -64,6 +73,9 @@ private:
void updateUserVariables(const QString& scriptText);
void initPythonBuiltins();
+ CompletionContext detectContext(KTextEditor::View* view, const KTextEditor::Cursor& cursor, QString& objectName);
+ QList<CompletionItem> getMembersForType(const QString& typeName);
+ QString inferType(const QString& varName, const QString& scriptText);
};
#endif // SCRIPTCOMPLETIONMODEL_H
diff --git a/src/frontend/script/ScriptEditor.cpp b/src/frontend/script/ScriptEditor.cpp
index 19f869e2c9..6afe551cc8 100644
--- a/src/frontend/script/ScriptEditor.cpp
+++ b/src/frontend/script/ScriptEditor.cpp
@@ -395,6 +395,22 @@ void ScriptEditor::showCustomCompleter() {
auto* listItem = new QListWidgetItem(displayText, m_customCompleter);
listItem->setData(Qt::UserRole, item.name); // Store clean name for insertion
+
+ // Set tooltip with signature and docstring if available
+ QString tooltip;
+ if (!item.signature.isEmpty())
+ tooltip = item.signature;
+ else if (item.isFunction || item.isClass)
+ tooltip = item.name + QStringLiteral("()");
+
+ if (!item.docstring.isEmpty()) {
+ if (!tooltip.isEmpty())
+ tooltip += QStringLiteral("\n\n");
+ tooltip += item.docstring;
+ }
+
+ if (!tooltip.isEmpty())
+ listItem->setToolTip(tooltip);
}
m_customCompleter->setCurrentRow(0);