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
version
tags
mte90
2.0.0
kate
kde
text-editor
plugin
c++
qt
kde-frameworks
C++ plugins for Kate using KTextEditor Framework, KDE Frameworks, and Qt.
Kate plugins extend functionality through C++ using KTextEditor framework.
/usr/share/kate/plugins/ # System
~/.local/share/kate/plugins/ # User
#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)
#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 (const QString &pat, const KTextEditor::Cursor &start) { return m_doc->searchText (pat, start); }
private:
KTextEditor::Document *m_doc;
};
class ViewHandler {
public:
explicit ViewHandler (KTextEditor::View *view) : m_view(view) {}
KTextEditor::Cursor getCursorPosition () const { return m_view->cursorPosition (); }
void setCursorPosition (int line, int col) { m_view->setCursorPosition (line, col); }
QString getSelection () const { return m_view->selectionText (); }
void setSelection (const KTextEditor::Range &range) { m_view->setSelection (range); }
void clearSelection () { m_view->clearSelection (); }
void toggleFolding (const KTextEditor::Range &range) { m_view->toggleFolding (range); }
private:
KTextEditor::View *m_view;
};
void positions (KTextEditor::Document *doc) {
KTextEditor::Cursor cursor (10 , 5 );
KTextEditor::Range range (10 , 0 , 10 , 20 );
if (range.isValid ()) { QString text = range.text (doc); bool contains = range.contains (cursor); }
}
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 ();
processTask (task);
}
}
private:
void processTask (const QString &task) { emit taskCompleted (task, " Result: " + task); }
signals:
void taskCompleted (const QString &task, const QString &result);
private:
QQueue<QString> m_tasks; QMutex m_mutex; QWaitCondition m_condition; bool m_running;
};
class AgentManager : public QObject {
Q_OBJECT
public:
explicit AgentManager (QObject *parent = nullptr ) : QObject(parent), m_agentThread(new AgentThread(this )) {
connect (m_agentThread.data (), &AgentThread::taskCompleted, this , &AgentManager::onTaskCompleted, Qt::QueuedConnection);
m_agentThread->start ();
}
void enqueueAgentTask (const QString &task) { m_agentThread->enqueueTask (task); }
private slots:
void onTaskCompleted (const QString &task, const QString &result) { qDebug () << " Task:" << task; emit resultReady (task, result); }
signals:
void resultReady (const QString &task, const QString &result);
private:
QScopedPointer<AgentThread> m_agentThread;
};
struct AgentTask { QString id; QString data; };
struct AgentResult { QString id; QString output; };
class AgentLoop : public QObject {
Q_OBJECT
public:
explicit AgentLoop (QObject *parent = nullptr ) : QObject(parent), m_thread(new QThread(this )) {
m_processor = new AgentProcessor ();
m_processor->moveToThread (m_thread);
connect (this , &AgentLoop::taskEnqueued, m_processor, &AgentProcessor::processTask, Qt::QueuedConnection);
connect (m_processor, &AgentProcessor::taskFinished, this , &AgentLoop::onTaskFinished, Qt::QueuedConnection);
m_thread->start ();
}
~AgentLoop () override { m_thread->quit (); m_thread->wait (); }
void enqueue (const AgentTask &task) { QMutexLocker locker (&m_mutex); m_queue.enqueue (task); emit taskEnqueued (task); }
private slots:
void onTaskFinished (const AgentResult &result) {
emit taskCompleted (result);
QMutexLocker locker (&m_mutex);
if (!m_queue.isEmpty ()) emit taskEnqueued (m_queue.dequeue ());
}
signals:
void taskEnqueued (const AgentTask &task);
void taskCompleted (const AgentResult &result);
private:
QThread *m_thread; AgentProcessor *m_processor; QQueue<AgentTask> m_queue; QMutex m_mutex;
};
#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) { load (); }
void store (const QString &key, const QVariant &value) override { m_data[key] = QJsonValue::fromVariant (value); save (); }
QVariant retrieve (const QString &key) override { return m_data.contains (key) ? m_data[key].toVariant () : QVariant (); }
bool has (const QString &key) override { return m_data.contains (key); }
void remove (const QString &key) override { m_data.remove (key); save (); }
void clear () override { m_data = QJsonObject (); save (); }
private:
void load () { QFile f (m_filePath); if (f.open (QIODevice::ReadOnly)) m_data = QJsonDocument::fromJson (f.readAll ()).object (); }
void save () { QFile f (m_filePath); if (f.open (QIODevice::WriteOnly)) f.write (QJsonDocument (m_data).toJson ()); }
QString m_filePath; QJsonObject m_data;
};
class MemoryThreadStorage : public IThreadStorage {
public:
void store (const QString &key, const QVariant &value) override { m_data[key] = value; }
QVariant retrieve (const QString &key) override { return m_data.value (key); }
bool has (const QString &key) override { return m_data.contains (key); }
void remove (const QString &key) override { m_data.remove (key); }
void clear () override { m_data.clear (); }
private:
QMap<QString, QVariant> m_data;
};
class AgentService {
public:
explicit AgentService (std::unique_ptr<IThreadStorage> storage) : m_storage(std::move(storage)) {}
void runAgent (const QString &taskId) { m_storage->store (" lastTask" , taskId); m_storage->store (" lastResult" , processTask (taskId)); }
private:
QString processTask (const QString &id) { return " Processed: " + 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 () const { QJsonObject obj; obj[" enabled" ] = m_enabled; QJsonObject t; t[" max" ] = m_maxThreads; obj[" threads" ] = t; return obj; }
void fromJsonObject (const QJsonObject &obj) { m_enabled = obj[" enabled" ].toBool (true ); if (obj.contains (" threads" )) m_maxThreads = obj[" threads" ].toObject ()[" max" ].toInt (4 ); }
private:
bool m_enabled = true ; int m_maxThreads = 4 ;
};
class ConfigMigrator {
public:
static void migrate (KConfigGroup &group) {
int version = group.readEntry (" ConfigVersion" , 0 );
if (version < 2 ) { group.writeEntry (" Plugin.Enabled" , group.readEntry (" Enabled" , true )); group.writeEntry (" ConfigVersion" , 2 ); }
}
};
#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 (Permission perm);
void permissionDenied (Permission perm);
private:
QSet<Permission> m_granted, m_denied;
};
class DocumentAccessController {
public:
explicit DocumentAccessController (PermissionManager *pm) : m_permManager(pm) {}
void requestWriteAccess (const QString &reason) {
auto decision = m_permManager->requestPermission (Permission::WriteDocument, reason);
if (decision == PermissionManager::Decision::Granted) performWrite ();
}
private:
void performWrite () {}
PermissionManager *m_permManager;
};
class PluginActions : public QObject {
Q_OBJECT
public:
explicit PluginActions (KTextEditor::MainWindow *mainWindow, PermissionManager *pm)
: QObject(mainWindow), m_mainWindow(mainWindow), m_permManager(pm) {
auto *action = m_mainWindow->actionCollection ()->addAction (" write" );
connect (action, &KAction::triggered, this , &PluginActions::onWriteRequested);
}
private slots:
void onWriteRequested () {
if (!m_permManager->hasPermission (Permission::WriteDocument)) {
m_permManager->requestPermission (Permission::WriteDocument, " Needs write access" ); return ;
}
performWrite ();
}
void performWrite () {}
private:
KTextEditor::MainWindow *m_mainWindow; PermissionManager *m_permManager;
};
#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" ));
delete doc;
}
};
QTEST_APPLESS_MAIN (DocumentHandlerTest)
#include " test_document_handler.moc"
class MockDocument : public QObject {
Q_OBJECT
public:
QString text () const { return m_text; }
void setText (const QString &t) { m_text = t; }
void insertText (const KTextEditor::Cursor &, const QString &text) { m_text += text; emit textChanged (this ); }
signals:
void textChanged (KTextEditor::Document *doc);
private:
QString m_text;
};
class AgentThreadTest : public QObject {
Q_OBJECT
private slots:
void testTaskProcessing () {
AgentThread thread; thread.start ();
bool completed = false ;
connect (&thread, &AgentThread::taskCompleted, [&completed](const QString &, const QString &) { completed = true ; });
thread.enqueueTask (" test-task" );
QTest::qWait (1000 );
QVERIFY (completed);
thread.quit (); thread.wait ();
}
};
QTEST_MAIN (AgentThreadTest)
#include " test_agent_thread.moc"
enable_testing ()
find_package(Qt5 REQUIRED COMPONENTS Test)
add_executable(test_myplugin test_main.cpp test_document_handler.cpp test_agent_thread.cpp)
target_link_libraries(test_myplugin PRIVATE Qt5::Test KF5 ::KTextEditor myplugin)
add_test(NAME myplugin_tests COMMAND test_myplugin)
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
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