[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);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.