[education/labplot] src: [scripting] initial implementation of the completion model, providing auto-complete for pylabplot classes, Python built-ins, and user variables.
Alexander Semke <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 0f8663da0b7aa79ffc0559b4ec5a762aa0f38ba3 by Alexander Semke.
Committed on 01/08/2026 at 20:22.
Pushed by asemke into branch 'master'.
[scripting] initial implementation of the completion model, providing auto-complete for pylabplot classes, Python built-ins, and user variables.
M +4 -2 src/CMakeLists.txt
M +47 -0 src/backend/script/python/PythonScriptRuntime.cpp
M +3 -0 src/backend/script/python/PythonScriptRuntime.h
A +19 -0 src/backend/script/python/PythonScriptingHelper.h [License: GPL(v2.0+)]
A +285 -0 src/frontend/script/ScriptCompletionModel.cpp [License: GPL(v2.0+)]
A +64 -0 src/frontend/script/ScriptCompletionModel.h [License: GPL(v2.0+)]
M +12 -1 src/frontend/script/ScriptEditor.cpp
M +3 -0 src/frontend/script/ScriptEditor.h
https://invent.kde.org/education/labplot/-/commit/0f8663da0b7aa79ffc0559b4ec5a762aa0f38ba3
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e55952b3fd..0e8a1cdd05 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -186,6 +186,7 @@ if(ENABLE_SCRIPTING)
list(APPEND GUI_SOURCES
${FRONTEND_DIR}/dockwidgets/ScriptDock.cpp
${FRONTEND_DIR}/script/ScriptEditor.cpp
+ ${FRONTEND_DIR}/script/ScriptCompletionModel.cpp
${FRONTEND_DIR}/SettingsEditorPage.cpp
${FRONTEND_DIR}/SettingsScriptingPage.cpp)
endif()
@@ -1005,8 +1006,9 @@ if(APPLE) # Apple app package
MACOSX_BUNDLE_SHORT_VERSION_STRING "${labplot_VERSION}"
MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.labplot")
- # Add entitlements for code signing to allow loading Python packages with different signatures
- # Required for user-installed scientific packages (numpy, scipy, etc.) on hardened runtime
+ # Add entitlements for code signing to allow loading Python packages with different signatures.
+ # Required to be able to install user-specific packages (numpy, etc.) and not being blocked by the
+ # check for the TeamID created during the signing step.
set_target_properties(labplot PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/labplot.entitlements"
)
diff --git a/src/backend/script/python/PythonScriptRuntime.cpp b/src/backend/script/python/PythonScriptRuntime.cpp
index c811808881..0f043ab950 100644
--- a/src/backend/script/python/PythonScriptRuntime.cpp
+++ b/src/backend/script/python/PythonScriptRuntime.cpp
@@ -861,3 +861,50 @@ QString PythonScriptRuntime::pyUnicodeToQString(PyObject* obj) {
Py_DECREF(bytes);
return QString::fromUtf8(charPtr);
}
+
+// Global helper function for code completion (avoids Python.h in frontend)
+QStringList getPylabplotSymbolsHelper() {
+ return PythonScriptRuntime::getPylabplotSymbols();
+}
+
+QStringList PythonScriptRuntime::getPylabplotSymbols() {
+ QStringList symbols;
+
+ // Check if Python is initialized
+ if (!Py_IsInitialized())
+ return symbols;
+
+ PyGILState_STATE gil = PyGILState_Ensure();
+
+ // Import pylabplot module
+ PyObject* module = PyImport_ImportModule("pylabplot");
+ if (!module) {
+ PyErr_Clear();
+ PyGILState_Release(gil);
+ return symbols;
+ }
+
+ // Get all symbols from module using dir()
+ PyObject* dirList = PyObject_Dir(module);
+ if (dirList && PyList_Check(dirList)) {
+ Py_ssize_t size = PyList_Size(dirList);
+ for (Py_ssize_t i = 0; i < size; ++i) {
+ PyObject* item = PyList_GetItem(dirList, i); // Borrowed reference
+ if (PyUnicode_Check(item)) {
+ QString name = PythonScriptRuntime::pyUnicodeToQString(item);
+ if (!name.isEmpty()) {
+ // Skip private symbols
+ if (!name.startsWith(QLatin1Char('_'))) {
+ symbols.append(name);
+ }
+ }
+ }
+ }
+ Py_DECREF(dirList);
+ }
+
+ Py_DECREF(module);
+ PyGILState_Release(gil);
+
+ return symbols;
+}
diff --git a/src/backend/script/python/PythonScriptRuntime.h b/src/backend/script/python/PythonScriptRuntime.h
index 7b2596a2f8..8217a107e8 100644
--- a/src/backend/script/python/PythonScriptRuntime.h
+++ b/src/backend/script/python/PythonScriptRuntime.h
@@ -27,6 +27,9 @@ public:
virtual bool cancel() override;
virtual bool exec(const QString&) override;
+ // Static helper for code completion
+ static QStringList getPylabplotSymbols();
+
private:
PythonLogger* m_loggerStdOut{nullptr}; // PythonLogger instance to replace sys.stdout in the python interpreter
PythonLogger* m_loggerStdErr{nullptr}; // PythonLogger instance to replace sys.stderr in the python interpreter
diff --git a/src/backend/script/python/PythonScriptingHelper.h b/src/backend/script/python/PythonScriptingHelper.h
new file mode 100644
index 0000000000..7c91b6073c
--- /dev/null
+++ b/src/backend/script/python/PythonScriptingHelper.h
@@ -0,0 +1,19 @@
+/*
+ File : PythonScriptingHelper.h
+ Project : LabPlot
+ Description : Helper functions for Python scripting (frontend-safe)
+ --------------------------------------------------------------------
+ SPDX-FileCopyrightText: 2026 Alexander Semke <[email protected]>
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef PYTHONSCRIPTINGHELPER_H
+#define PYTHONSCRIPTINGHELPER_H
+
+#include <QStringList>
+
+// Helper function to get pylabplot symbols without requiring Python.h
+// This allows frontend code to query symbols without Python header dependencies
+QStringList getPylabplotSymbolsHelper();
+
+#endif // PYTHONSCRIPTINGHELPER_H
diff --git a/src/frontend/script/ScriptCompletionModel.cpp b/src/frontend/script/ScriptCompletionModel.cpp
new file mode 100644
index 0000000000..b9eeae79ee
--- /dev/null
+++ b/src/frontend/script/ScriptCompletionModel.cpp
@@ -0,0 +1,285 @@
+/*
+ File : ScriptCompletionModel.cpp
+ Project : LabPlot
+ Description : Code completion model for Python script editor
+ --------------------------------------------------------------------
+ SPDX-FileCopyrightText: 2026 Alexander Semke <[email protected]>
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "ScriptCompletionModel.h"
+#include "ScriptEditor.h"
+#include "backend/script/Script.h"
+#include "backend/script/python/PythonScriptingHelper.h"
+#include <backend/lib/macros.h>
+
+#include <KTextEditor/View>
+#include <KTextEditor/Document>
+
+#include <QTimer>
+#include <QSet>
+#include <QRegularExpression>
+#include <algorithm>
+
+ScriptCompletionModel::ScriptCompletionModel(ScriptEditor* parent)
+ : KTextEditor::CodeCompletionModel(parent)
+ , m_editor(parent) {
+ m_debounceTimer = new QTimer(this);
+ m_debounceTimer->setSingleShot(true);
+ m_debounceTimer->setInterval(200);
+ connect(m_debounceTimer, &QTimer::timeout, this, &ScriptCompletionModel::startCompletionRequest);
+
+ initPythonBuiltins(); // initialize Python built-ins
+ initPylabplotSymbols(); // initialize pylabplot symbols
+}
+
+ScriptCompletionModel::~ScriptCompletionModel() {
+ m_debounceTimer->stop();
+}
+
+void ScriptCompletionModel::initPythonBuiltins() {
+ // Common Python built-in functions
+ m_pythonBuiltins = {
+ QStringLiteral("print"),
+ QStringLiteral("len"),
+ QStringLiteral("range"),
+ QStringLiteral("str"),
+ QStringLiteral("int"),
+ QStringLiteral("float"),
+ QStringLiteral("list"),
+ QStringLiteral("dict"),
+ QStringLiteral("set"),
+ QStringLiteral("tuple"),
+ QStringLiteral("bool"),
+ QStringLiteral("sum"),
+ QStringLiteral("min"),
+ QStringLiteral("max"),
+ QStringLiteral("abs"),
+ QStringLiteral("round"),
+ QStringLiteral("sorted"),
+ QStringLiteral("enumerate"),
+ QStringLiteral("zip"),
+ QStringLiteral("map"),
+ QStringLiteral("filter"),
+ QStringLiteral("open"),
+ QStringLiteral("type"),
+ QStringLiteral("isinstance"),
+ QStringLiteral("hasattr"),
+ QStringLiteral("getattr"),
+ QStringLiteral("setattr"),
+ };
+}
+
+bool ScriptCompletionModel::initPylabplotSymbols() {
+ DEBUG(Q_FUNC_INFO)
+
+ // Get pylabplot symbols via global helper (avoids Python.h dependency)
+ QStringList symbolNames = getPylabplotSymbolsHelper();
+
+ if (symbolNames.isEmpty()) {
+ WARN("No pylabplot symbols extracted - Python may not be initialized")
+ return false;
+ }
+
+ DEBUG(Q_FUNC_INFO << ", Found " << symbolNames.size() << " symbols in pylabplot")
+
+ // Create completion items for each symbol
+ for (const QString& name : symbolNames) {
+ CompletionItem item;
+ item.name = name;
+
+ // Heuristic type detection based on naming conventions
+ // Classes typically start with uppercase
+ if (!name.isEmpty() && name[0].isUpper()) {
+ item.isClass = true;
+ }
+ // Function names often contain common patterns
+ else if (name.contains(QLatin1String("get")) || name.contains(QLatin1String("set")) || name == QLatin1String("project")) {
+ item.isFunction = true;
+ }
+
+ m_pylabplotSymbols.append(item);
+ }
+
+ DEBUG(Q_FUNC_INFO << ", Successfully extracted " << m_pylabplotSymbols.size() << " pylabplot symbols")
+ return true;
+}
+
+void ScriptCompletionModel::updateUserVariables(const QString& scriptText) {
+ // Extract variable names from script using simple regex
+ // Matches: variable_name = ...
+ static QRegularExpression assignPattern(QStringLiteral(R"(^(\w+)\s*=)"), QRegularExpression::MultilineOption);
+
+ QSet<QString> variables;
+ QRegularExpressionMatchIterator it = assignPattern.globalMatch(scriptText);
+
+ while (it.hasNext()) {
+ QRegularExpressionMatch match = it.next();
+ QString varName = match.captured(1);
+
+ // Skip if it starts with underscore or is a Python keyword
+ if (!varName.isEmpty() && !varName.startsWith(QLatin1Char('_'))) {
+ // Basic keyword check
+ if (varName != QLatin1String("if") && varName != QLatin1String("for") && varName != QLatin1String("while") && varName != QLatin1String("def")
+ && varName != QLatin1String("class") && varName != QLatin1String("import") && varName != QLatin1String("from")
+ && varName != QLatin1String("return") && varName != QLatin1String("yield") && varName != QLatin1String("with")
+ && varName != QLatin1String("as")) {
+ variables.insert(varName);
+ }
+ }
+ }
+
+ m_userVariables = variables.values();
+ DEBUG(Q_FUNC_INFO << ", Found " << m_userVariables.size() << " user variables")
+}
+
+void ScriptCompletionModel::completionInvoked(KTextEditor::View* view, const KTextEditor::Range& range, InvocationType invocationType) {
+ Q_UNUSED(invocationType)
+
+ m_pendingView = view;
+ m_pendingRange = range;
+ m_debounceTimer->start();
+}
+
+void ScriptCompletionModel::startCompletionRequest() {
+ if (!m_pendingView)
+ return;
+
+ auto currentPos = m_pendingView->cursorPosition();
+
+ // Get current word being typed
+ auto currentWordRange = m_pendingView->document()->wordRangeAt(currentPos);
+ QString prefix = m_pendingView->document()->text(currentWordRange);
+
+ // Update user variables from current script
+ 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
+ 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;
+ }
+ }
+
+ // 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;
+
+ m_matches.append(item);
+ }
+ }
+
+ // Sort alphabetically
+ std::sort(m_matches.begin(), m_matches.end(), [](const auto& a, const auto& b) {
+ return a.name.localeAwareCompare(b.name) < 0;
+ });
+
+ setRowCount(m_matches.size());
+ endResetModel();
+
+ // DEBUG(Q_FUNC_INFO << ", Showing " << m_matches.size() << " completions for prefix '" << prefix << "'")
+}
+
+QVariant ScriptCompletionModel::data(const QModelIndex& index, int role) const {
+ if (!index.isValid() || index.row() >= m_matches.count())
+ return QVariant();
+
+ const CompletionItem& item = m_matches.at(index.row());
+
+ switch (role) {
+ case Qt::DisplayRole:
+ if (index.column() == Name)
+ return item.name;
+ break;
+
+ case Qt::DecorationRole:
+ if (index.column() == Icon) {
+ // Return appropriate icon based on type
+ if (item.isClass)
+ return QIcon::fromTheme(QStringLiteral("code-class"));
+ else if (item.isFunction)
+ return QIcon::fromTheme(QStringLiteral("code-function"));
+ else if (item.isEnum)
+ return QIcon::fromTheme(QStringLiteral("flag"));
+ else if (item.isVariable)
+ return QIcon::fromTheme(QStringLiteral("code-variable"));
+ }
+ break;
+ }
+
+ return QVariant();
+}
+
+void ScriptCompletionModel::executeCompletionItem(KTextEditor::View* view, const KTextEditor::Range& word, const QModelIndex& index) const {
+ if (!index.isValid() || !view)
+ return;
+
+ const CompletionItem& item = m_matches.at(index.row());
+ QString textToInsert = item.name;
+
+ // Add parentheses for functions and classes
+ if (item.isFunction || item.isClass)
+ textToInsert += QStringLiteral("()");
+
+ // Replace the current word with the completion
+ KTextEditor::Range rangeToReplace = view->document()->wordRangeAt(view->cursorPosition());
+ view->document()->replaceText(rangeToReplace, textToInsert);
+
+ // Position cursor inside parentheses for functions/classes
+ if (item.isFunction || item.isClass) {
+ KTextEditor::Cursor newCursorPos = rangeToReplace.start();
+ newCursorPos.setColumn(newCursorPos.column() + item.name.length() + 1);
+ view->setCursorPosition(newCursorPos);
+ }
+}
+
+KTextEditor::Range ScriptCompletionModel::completionRange(KTextEditor::View* view, const KTextEditor::Cursor& cursor) {
+ return view->document()->wordRangeAt(cursor);
+}
+
+bool ScriptCompletionModel::shouldStartCompletion(KTextEditor::View* view, const QString& insertedText, bool userInsertion, const KTextEditor::Cursor& position) {
+ Q_UNUSED(view)
+ Q_UNUSED(userInsertion)
+ Q_UNUSED(position)
+
+ if (!insertedText.isEmpty()) {
+ const QChar lastChar = insertedText.back();
+ // Start completion after typing a letter or underscore
+ if (lastChar.isLetter() || lastChar == QLatin1Char('_'))
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/frontend/script/ScriptCompletionModel.h b/src/frontend/script/ScriptCompletionModel.h
new file mode 100644
index 0000000000..33c2b6928b
--- /dev/null
+++ b/src/frontend/script/ScriptCompletionModel.h
@@ -0,0 +1,64 @@
+/*
+ File : ScriptCompletionModel.h
+ Project : LabPlot
+ Description : Code completion model for Python script editor
+ --------------------------------------------------------------------
+ SPDX-FileCopyrightText: 2026 Alexander Semke <[email protected]>
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef SCRIPTCOMPLETIONMODEL_H
+#define SCRIPTCOMPLETIONMODEL_H
+
+#include <KTextEditor/CodeCompletionModel>
+#include <KTextEditor/CodeCompletionModelControllerInterface>
+#include <KTextEditor/Range>
+
+class ScriptEditor;
+class QTimer;
+
+class ScriptCompletionModel : public KTextEditor::CodeCompletionModel,
+ public KTextEditor::CodeCompletionModelControllerInterface {
+ Q_OBJECT
+ Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface)
+
+public:
+ struct CompletionItem {
+ QString name;
+ bool isFunction = false;
+ bool isClass = false;
+ bool isEnum = false;
+ bool isVariable = false;
+ };
+
+ explicit ScriptCompletionModel(ScriptEditor* parent);
+ ~ScriptCompletionModel() override;
+
+ void completionInvoked(KTextEditor::View*, const KTextEditor::Range&, InvocationType) override;
+ void executeCompletionItem(KTextEditor::View*, const KTextEditor::Range&, const QModelIndex&) const override;
+ QVariant data(const QModelIndex&, int) const override;
+
+ KTextEditor::Range completionRange(KTextEditor::View*, const KTextEditor::Cursor&) override;
+ bool shouldStartCompletion(KTextEditor::View*, const QString&, bool, const KTextEditor::Cursor&) override;
+
+ bool initPylabplotSymbols();
+
+private Q_SLOTS:
+ void startCompletionRequest();
+
+private:
+ ScriptEditor* m_editor{nullptr};
+ QList<CompletionItem> m_matches;
+ QList<CompletionItem> m_pylabplotSymbols;
+ QStringList m_userVariables;
+ QStringList m_pythonBuiltins;
+
+ QTimer* m_debounceTimer{nullptr};
+ KTextEditor::View* m_pendingView{nullptr};
+ KTextEditor::Range m_pendingRange;
+
+ void updateUserVariables(const QString& scriptText);
+ void initPythonBuiltins();
+};
+
+#endif // SCRIPTCOMPLETIONMODEL_H
diff --git a/src/frontend/script/ScriptEditor.cpp b/src/frontend/script/ScriptEditor.cpp
index 1ccddd8c6c..98d8faed55 100644
--- a/src/frontend/script/ScriptEditor.cpp
+++ b/src/frontend/script/ScriptEditor.cpp
@@ -4,10 +4,12 @@
Description : Script editor
--------------------------------------------------------------------
SPDX-FileCopyrightText: 2025 Israel Galadima <[email protected]>
+ SPDX-FileCopyrightText: 2026 Alexander Semke <[email protected]>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "ScriptEditor.h"
+#include "ScriptCompletionModel.h"
#include "backend/script/Script.h"
#include <backend/lib/macros.h>
@@ -67,9 +69,18 @@ ScriptEditor::ScriptEditor(Script* script, QWidget* parent)
// Setup context menu for output
ui.output->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui.output, &QTextBrowser::customContextMenuRequested, this, &ScriptEditor::showOutputContextMenu);
+
+ // Create and register code completion model in KTextEditor view
+ m_completionModel = new ScriptCompletionModel(this);
+ m_kTextEditorView->registerCompletionModel(m_completionModel);
+ m_kTextEditorView->setAutomaticInvocationEnabled(true);
}
-ScriptEditor::~ScriptEditor() {
+ScriptEditor::~ScriptEditor() {
+ // Unregister completion model
+ if (m_completionModel && m_kTextEditorView)
+ m_kTextEditorView->unregisterCompletionModel(m_completionModel);
+
KConfig config;
auto group = config.group(QStringLiteral("ScriptEditor"));
// we dont manage default editor font or themes ourselves, so no need to save in our config
diff --git a/src/frontend/script/ScriptEditor.h b/src/frontend/script/ScriptEditor.h
index f3cf938cf0..f2423e67ef 100644
--- a/src/frontend/script/ScriptEditor.h
+++ b/src/frontend/script/ScriptEditor.h
@@ -4,6 +4,7 @@
Description : Script editor
--------------------------------------------------------------------
SPDX-FileCopyrightText: 2025 Israel Galadima <[email protected]>
+ SPDX-FileCopyrightText: 2026 Alexander Semke <[email protected]>
SPDX-License-Identifier: GPL-2.0-or-later
*/
@@ -18,6 +19,7 @@ class QMenu;
class Script;
class QToolBar;
class QToolButton;
+class ScriptCompletionModel;
namespace KTextEditor{
class View;
}
@@ -54,6 +56,7 @@ private:
QAction* m_clearOutputAction{nullptr};
QAction* m_copySelectedAction{nullptr};
QAction* m_copyAllOutputAction{nullptr};
+ ScriptCompletionModel* m_completionModel{nullptr};
void initActions();
void initMenus();