| name | kate-plugin |
| description | Develop C++ plugins for Kate text editor using KTextEditor Framework, CMake with ECM, Qt threading, and KDE plugin architecture |
| metadata | {"author":"mte90","version":"2.0.0","tags":["kate","kde","text-editor","plugin","c++","qt","kde-frameworks"]} |
Kate Plugin Development
C++ plugins for Kate using KTextEditor Framework, KDE Frameworks, and Qt.
Overview
Kate plugins extend functionality through C++ using KTextEditor framework.
/usr/share/kate/plugins/ # System
~/.local/share/kate/plugins/ # User
Plugin Architecture
#include <KPluginFactory>
#include <KTextEditor/Plugin>
class MyPlugin : public KTextEditor::Plugin {
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.kde.KTextEditor.Plugin" FILE "metadata.json")
public:
explicit MyPlugin(QObject *parent, const QVariantList &)
: KTextEditor::Plugin(parent) {}
~MyPlugin() override = default;
QWidget *createConfigWidget(QWidget *p) override { return new ConfigWidget(p); }
void readConfig(KConfigGroup &g) override { m_enabled = g.readEntry("Enabled", true); }
void writeConfig(KConfigGroup &g) override { g.writeEntry("Enabled", m_enabled); }
void init() override {}
void addView(KTextEditor::MainWindow *mw) override {
auto *v = mw->activeView();
if (v) connect(v->document(), &KTextEditor::Document::textChanged, this, &MyPlugin::onTextChanged);
}
void removeView(KTextEditor::MainWindow *) override {}
private slots:
void onTextChanged(KTextEditor::Document *doc) { qDebug() << doc->url(); }
private:
bool m_enabled = true;
};
K_PLUGIN_FACTORY_WITH_JSON(MyPluginFactory, "metadata.json", registerPlugin<MyPlugin>();)
{
"KPlugin": {
"Name": "My Plugin",
"Description": "A C++ plugin for Kate",
"Icon": "kate",
"Authors": [{"Name": "Dev", "Email": "dev@example.com"}],
"Version": "1.0.0",
"License": "GPL-2.0-or-later",
"ServiceTypes": ["KTextEditor/Plugin"],
"Category": "Language"
},
"X-KTextEditor": {"APIVersion": "5"}
}
cmake_minimum_required(VERSION 3.16)
project(myplugin VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
find_package(ECM 5.90 REQUIRED NO_MODULE)
find_package(KF5 REQUIRED COMPONENTS CoreAddons Config KTextEditor WidgetsAddons)
include(KDEInstallDirs)
include(KDECMakeSettings)
kcoreaddons_add_plugin(myplugin INSTALL_NAMESPACE "kate"
SOURCES myplugin.cpp configwidget.cpp
LIBRARIES KF5::CoreAddons KF5::ConfigGui KF5::KTextEditor)
install(FILES metadata.json DESTINATION ${KDE_INSTALL_KTPLUGINDIR}/myplugin)
KTextEditor API
#include <KTextEditor/Document>
#include <KTextEditor/View>
#include <KTextEditor/Range>
#include <KTextEditor/Cursor>
class DocumentHandler {
public:
explicit DocumentHandler(KTextEditor::Document *doc) : m_doc(doc) {}
void insertText(const KTextEditor::Cursor &cur, const QString &text) { m_doc->insertText(cur, text); }
void replaceText(const KTextEditor::Range &range, const QString &text) { m_doc->replaceText(range, text); }
QString getText() const { return m_doc->text(); }
QString getLine(int n) const { return m_doc->line(n); }
int lineCount() const { return m_doc->lines(); }
KTextEditor::Range search( QString &pat, KTextEditor::Cursor &start) { m_doc->(pat, start); }
:
KTextEditor::Document *m_doc;
};
{
:
}
{ m_view->(); }
{ m_view->(line, col); }
{ m_view->(); }
{ m_view->(range); }
{ m_view->(); }
{ m_view->(range); }
:
KTextEditor::View *m_view;
};
{
;
;
(range.()) { QString text = range.(doc); contains = range.(cursor); }
}
CMake Build System
cmake_minimum_required(VERSION 3.16)
project(myplugin VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(ECM 5.90 REQUIRED NO_MODULE)
find_package(KF5 REQUIRED COMPONENTS CoreAddons Config KTextEditor WidgetsAddons)
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings)
kcoreaddons_add_plugin(myplugin INSTALL_NAMESPACE "kate"
SOURCES main.cpp myplugin.cpp configwidget.cpp
LIBRARIES KF5::CoreAddons KF5::ConfigGui KF5::KTextEditor)
enable_testing()
find_package(Qt5 REQUIRED COMPONENTS Test)
qt5_add_tests(test_myplugin SOURCES test_myplugin.cpp LIBRARIES Qt5::Test KF5::KTextEditor)
install(TARGETS myplugin LIBRARY DESTINATION ${KDE_INSTALL_KTPLUGINDIR}/myplugin)
install(FILES metadata.json DESTINATION ${KDE_INSTALL_KTPLUGINDIR}/myplugin)
#include <QJsonObject>
#include <QJsonDocument>
#include <QFile>
QJsonObject loadConfig(const QString &path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) return {};
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
return doc.object();
}
void saveConfig(const QString &path, const QJsonObject &config) {
QFile file(path);
if (file.open(QIODevice::WriteOnly)) file.write(QJsonDocument(config).toJson(QJsonDocument::Indented));
}
Threaded Agent Architecture
#include <QThread>
#include <QQueue>
#include <QMutex>
#include <QWaitCondition>
class AgentThread : public QThread {
Q_OBJECT
public:
explicit AgentThread(QObject *parent = nullptr) : QThread(parent), m_running(true) {}
~AgentThread() override { m_running = false; m_condition.wakeOne(); wait(); }
void enqueueTask(const QString &task) { QMutexLocker locker(&m_mutex); m_tasks.enqueue(task); m_condition.wakeOne(); }
protected:
void run() override {
while (m_running) {
QMutexLocker locker(&m_mutex);
while (m_tasks.isEmpty() && m_running) m_condition.wait(&m_mutex);
if (!m_running) break;
QString task = m_tasks.dequeue();
locker.unlock();
(task);
}
}
:
{ ; }
signals:
;
:
QQueue<QString> m_tasks; QMutex m_mutex; QWaitCondition m_condition; m_running;
};
: QObject {
Q_OBJECT
:
(m_agentThread.(), &AgentThread::taskCompleted, , &AgentManager::onTaskCompleted, Qt::QueuedConnection);
m_agentThread->();
}
{ m_agentThread->(task); }
slots:
{ () << << task; ; }
signals:
;
:
QScopedPointer<AgentThread> m_agentThread;
};
{ QString id; QString data; };
{ QString id; QString output; };
: QObject {
Q_OBJECT
:
m_processor = ();
m_processor->(m_thread);
(, &AgentLoop::taskEnqueued, m_processor, &AgentProcessor::processTask, Qt::QueuedConnection);
(m_processor, &AgentProcessor::taskFinished, , &AgentLoop::onTaskFinished, Qt::QueuedConnection);
m_thread->();
}
~() { m_thread->(); m_thread->(); }
{ ; m_queue.(task); ; }
slots:
{
;
;
(!m_queue.()) ;
}
signals:
;
;
:
QThread *m_thread; AgentProcessor *m_processor; QQueue<AgentTask> m_queue; QMutex m_mutex;
};
Interface-Based Design
#include <memory>
#include <QString>
#include <QVariant>
class IThreadStorage {
public:
virtual ~IThreadStorage() = default;
virtual void store(const QString &key, const QVariant &value) = 0;
virtual QVariant retrieve(const QString &key) = 0;
virtual bool has(const QString &key) = 0;
virtual void remove(const QString &key) = 0;
virtual void clear() = 0;
};
class JsonThreadStorage : public IThreadStorage {
public:
explicit JsonThreadStorage(const QString &filePath) : m_filePath(filePath) { (); }
{ m_data[key] = QJsonValue::(value); (); }
{ m_data.(key) ? m_data[key].() : (); }
{ m_data.(key); }
{ m_data.(key); (); }
{ m_data = (); (); }
:
{ ; (f.(QIODevice::ReadOnly)) m_data = QJsonDocument::(f.()).(); }
{ ; (f.(QIODevice::WriteOnly)) f.((m_data).()); }
QString m_filePath; QJsonObject m_data;
};
: IThreadStorage {
:
{ m_data[key] = value; }
{ m_data.(key); }
{ m_data.(key); }
{ m_data.(key); }
{ m_data.(); }
:
QMap<QString, QVariant> m_data;
};
{
:
}
{ m_storage->(, taskId); m_storage->(, (taskId)); }
:
{ + id; }
std::unique_ptr<IThreadStorage> m_storage;
};
Configuration with KConfig/QSettings
#include <KSharedConfig>
#include <KConfigGroup>
class PluginConfig {
public:
explicit PluginConfig() : m_config(KSharedConfig::openConfig()), m_group(m_config->group("Plugin")) {}
void load() { m_enabled = m_group.readEntry("Enabled", true); m_maxThreads = m_group.readEntry("MaxThreads", 4); }
void save() { m_group.writeEntry("Enabled", m_enabled); m_group.writeEntry("MaxThreads", m_maxThreads); m_config->sync(); }
bool isEnabled() const { return m_enabled; }
void setEnabled(bool v) { m_enabled = v; }
private:
KSharedConfigPtr m_config; KConfigGroup m_group;
bool m_enabled = true; int m_maxThreads = 4;
};
class StructuredConfig {
public:
QJsonObject toJsonObject { QJsonObject obj; obj[] = m_enabled; QJsonObject t; t[] = m_maxThreads; obj[] = t; obj; }
{ m_enabled = obj[].(); (obj.()) m_maxThreads = obj[].()[].(); }
:
m_enabled = ; m_maxThreads = ;
};
{
:
{
version = group.(, );
(version < ) { group.(, group.(, )); group.(, ); }
}
};
Permission System
#include <QSet>
enum class Permission { ReadDocument, WriteDocument, AccessNetwork };
class PermissionManager {
public:
enum class Decision { Granted, Denied, Pending };
Decision requestPermission(Permission perm, const QString &reason) {
if (m_granted.contains(perm)) return Decision::Granted;
emit permissionRequested(perm, reason);
return Decision::Pending;
}
void grantPermission(Permission perm) { m_granted.insert(perm); emit permissionGranted(perm); }
void denyPermission(Permission perm) { m_denied.insert(perm); emit permissionDenied(perm); }
bool hasPermission(Permission perm) const { return m_granted.contains(perm); }
signals:
void permissionRequested(Permission perm, const QString &reason);
void permissionGranted;
;
:
QSet<Permission> m_granted, m_denied;
};
{
:
}
{
decision = m_permManager->(Permission::WriteDocument, reason);
(decision == PermissionManager::Decision::Granted) ();
}
:
{}
PermissionManager *m_permManager;
};
: QObject {
Q_OBJECT
:
*action = m_mainWindow->()->();
(action, &KAction::triggered, , &PluginActions::onWriteRequested);
}
slots:
{
(!m_permManager->(Permission::WriteDocument)) {
m_permManager->(Permission::WriteDocument, ); ;
}
();
}
{}
:
KTextEditor::MainWindow *m_mainWindow; PermissionManager *m_permManager;
};
Testing C++ Plugins
#include <QTest>
#include <QObject>
class DocumentHandlerTest : public QObject {
Q_OBJECT
private slots:
void testInsertText() {
KTextEditor::Document *doc = new KTextEditor::Document(nullptr);
DocumentHandler handler(doc);
handler.insertText(KTextEditor::Cursor(0, 0), "Hello");
QCOMPARE(doc->text(), QString("Hello"));
delete doc;
}
void testReplaceText() {
KTextEditor::Document *doc = new KTextEditor::Document(nullptr);
doc->setText("Hello World");
DocumentHandler handler(doc);
handler.replaceText(KTextEditor::Range(0, 0, 0, 5), "Hi");
QCOMPARE(doc->text(), QString("Hi World"));
doc;
}
};
(DocumentHandlerTest)
: QObject {
Q_OBJECT
:
{ m_text; }
{ m_text = t; }
{ m_text += text; ; }
signals:
;
:
QString m_text;
};
: QObject {
Q_OBJECT
slots:
{
AgentThread thread; thread.();
completed = ;
(&thread, &AgentThread::taskCompleted, [&completed]( QString &, QString &) { completed = ; });
thread.();
QTest::();
(completed);
thread.(); thread.();
}
};
(AgentThreadTest)
()
(Qt5 REQUIRED COMPONENTS Test)
(test_myplugin test_main.cpp test_document_handler.cpp test_agent_thread.cpp)
(test_myplugin PRIVATE Qt5::Test KF5::KTextEditor myplugin)
(NAME myplugin_tests COMMAND test_myplugin)
Common Issues
| Issue | Solution |
|---|
| Plugin not loading | Verify metadata.json and KPluginFactory |
| KTextEditor symbols undefined | Link KF5::KTextEditor |
| Signal/slot not connecting | Ensure Q_OBJECT and moc |
| Crash on addView | Check null view/document |
| Config not persisting | Call KConfigGroup::sync() |
| Thread safety issues | Use Qt::QueuedConnection |
| AgentLoop tasks stuck | Verify task queue and signals |
| Permission requests hanging | Implement async callback |
Best Practices
- Use Q_PLUGIN_METADATA with JSON file for registration
- Always check for null pointers on view/document
- Use Qt::QueuedConnection for cross-thread signals
- Implement cleanup in removeView() matching addView()
- Store config in KConfig with namespaced groups
- Use smart pointers for interface-based DI
- Process long tasks in worker threads
- Emit results via queued signals for UI updates
- Use QTest with mock KTextEditor interfaces
- Follow KDE coding style
References