diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c63071..d43067c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ include(ECMAddAppIcon) include(ECMInstallIcons) include(ECMDeprecationSettings) -find_package(Qt5Widgets CONFIG REQUIRED) +find_package(Qt5 REQUIRED COMPONENTS WebSockets) find_package(KF5 REQUIRED COMPONENTS CoreAddons @@ -26,12 +26,15 @@ find_package(KF5 TextEditor ) -kcoreaddons_add_plugin(${PROJECT_NAME} INSTALL_NAMESPACE "kf5/ktexteditor") -target_link_libraries(${PROJECT_NAME} PRIVATE KF5::TextEditor) +kcoreaddons_add_plugin(${PROJECT_NAME} INSTALL_NAMESPACE "ktexteditor") +target_include_directories(${PROJECT_NAME} PRIVATE) +target_link_libraries(${PROJECT_NAME} PRIVATE KF5::TextEditor Qt5::WebSockets) target_sources( ${PROJECT_NAME} PRIVATE kateai.cpp + kateaiconfigpage.cpp + plugin.qrc ) diff --git a/kateai.cpp b/kateai.cpp new file mode 100644 index 0000000..5bcd4ea --- /dev/null +++ b/kateai.cpp @@ -0,0 +1,244 @@ +#include "kateai.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kateaiconfigpage.h" + +K_PLUGIN_FACTORY_WITH_JSON(KateAiPluginFactory, "kateai.json", registerPlugin();) + +KateAiPlugin::KateAiPlugin(QObject *parent, const QList &) + : KTextEditor::Plugin(parent), m_serverUrl(QStringLiteral("ws://localhost:8642")) +{ + connect(&m_webSocket, &QWebSocket::connected, this, &KateAiPlugin::onConnected); + readConfig(); +} + +void KateAiPlugin::onConnected() +{ + qDebug()<<__func__< webSocket, bool instruct) + : QObject(plugin) + , m_mainWindow(mainwindow) + , m_webSocket(webSocket) + , m_useInstruct(instruct) +{ + KXMLGUIClient::setComponentName(QStringLiteral("kateaiplugin"), QStringLiteral("Git Blame")); + setXMLFile(QStringLiteral("ui.rc")); + QAction *generateAction = actionCollection()->addAction(QStringLiteral("ai_generate")); + generateAction->setText(QStringLiteral("Generate text using AI")); + actionCollection()->setDefaultShortcut(generateAction, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_A)); + m_mainWindow->guiFactory()->addClient(this); + + connect(generateAction, &QAction::triggered, this, &KateAiPluginView::generate); + connect(m_webSocket, &QWebSocket::textMessageReceived, this, &KateAiPluginView::socketMessage); +} + +QPointer KateAiPluginView::activeDocument() const +{ + KTextEditor::View *view = m_mainWindow->activeView(); + if(view && view->document()) + return view->document(); + return nullptr; +} + +KTextEditor::Cursor KateAiPluginView::getCurrentCursor() const +{ + KTextEditor::View *view = m_mainWindow->activeView(); + if(view) + return view->cursorPosition(); + return KTextEditor::Cursor(); +} + +void KateAiPluginView::socketMessage(const QString& message) +{ + QJsonDocument jsonDocument = QJsonDocument::fromJson(message.toUtf8()); + QJsonValue idVal = jsonDocument[QStringLiteral("request_id")]; + if(!idVal.isDouble()) + { + qDebug()<<"Got invalid response on socket"; + return; + } + int id = idVal.toInt(); + + QHash::iterator it = m_requests.find(id); + if(it != m_requests.end()) + { + QJsonValue responseValue = jsonDocument[QStringLiteral("response")]; + if(!responseValue.isString()) + { + qDebug()<<"Got invalid response on socket"; + return; + } + if(it.value().document) + it.value().document->insertText(it.value().cursor, responseValue.toString()); + m_requests.erase(it); + } +} + +QStringList getIncludePaths(const QString& text) +{ + QStringList lines = text.split(U'\n'); + QStringList paths; + + for(const QString& line : lines) + { + if(line.trimmed().startsWith(QStringLiteral("#include"))) + { + int start = line.indexOf(U'<'); + int end; + if(start != -1) + { + end = line.indexOf(U'>', start+1); + if(end == -1) + continue; + } + else + { + start = line.indexOf(U'"'); + if(start == -1) + continue; + + end = line.indexOf(U'"', start+1); + if(end == -1) + continue; + } + paths.push_back(line.mid(start+1, (end-(start+1))).trimmed()); + } + } + return paths; +} + +QString KateAiPluginView::assembleContext(QPointer document, const KTextEditor::Cursor& cursor) +{ + QString mime = document->mimeType(); + QString context; + QString baseText; + + if(!m_useInstruct) + baseText = document->text(KTextEditor::Range(KTextEditor::Cursor(0, 0), cursor)); + else + baseText = document->text(); + if(mime == QStringLiteral("text/x-c++src") || mime == QStringLiteral("text/x-csrc")) + { + QFileInfo documentFileInfo(document->url().path()); + QString directory = documentFileInfo.absolutePath(); + QStringList paths = getIncludePaths(baseText); + qDebug()<<__func__<<"Directory:"<mimeType(); + if(m_webSocket && m_webSocket->isValid()) + { + KTextEditor::Cursor cursor = getCurrentCursor(); + QPointer document = activeDocument(); + QString text = assembleContext(document, cursor); + int id = QRandomGenerator::global()->bounded(0, std::numeric_limits::max()); + + QJsonObject json; + json[QStringLiteral("action")] = QStringLiteral("infer"); + json[QStringLiteral("request_id")] = id; + json[QStringLiteral("text")] = text; + json[QStringLiteral("max_new_tokens")] = 50; + json[QStringLiteral("stream")] = false; + + QJsonDocument jsonDocument(json); + QString requestText = QString::fromUtf8(jsonDocument.toJson(QJsonDocument::JsonFormat::Compact)); + qDebug()<<__func__<<' '<sendTextMessage(requestText); + m_requests.insert(id, {cursor, document}); + } + else + { + QMessageBox box; + box.setText(i18n("The AI server is not connected.")); + box.setInformativeText(i18n("would you like to try and reconnect?")); + box.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + box.setDefaultButton(QMessageBox::Yes); + int ret = box.exec(); + if(ret == QMessageBox::Yes) + reconnect(); + } +} + +KateAiPluginView::~KateAiPluginView() +{ + m_mainWindow->guiFactory()->removeClient(this); +} + +void KateAiPluginView::setInstruct(bool instruct) +{ + m_useInstruct = instruct; +} + +#include "kateai.moc" +#include "moc_kateai.cpp" diff --git a/kateai.h b/kateai.h new file mode 100644 index 0000000..c76039d --- /dev/null +++ b/kateai.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +class KateAiPlugin : public KTextEditor::Plugin +{ + Q_OBJECT +private: + QWebSocket m_webSocket; + QUrl m_serverUrl; + +private: + void onConnected(); + + int configPages() const override + { + return 1; + } + + KTextEditor::ConfigPage *configPage(int number = 0, QWidget *parent = nullptr) override; + +public: + explicit KateAiPlugin(QObject *parent = nullptr, const QList & = QList()); + ~KateAiPlugin() override; + + QObject *createView(KTextEditor::MainWindow *mainWindow) override; + + void readConfig(); + void reconnect(); + Q_SIGNAL void instructChanged(); +}; + +class KateAiPluginView : public QObject, public KXMLGUIClient +{ + Q_OBJECT +private: + struct Request + { + KTextEditor::Cursor cursor; + QPointer document; + }; + + KTextEditor::MainWindow *m_mainWindow; + QPointer m_webSocket; + QHash m_requests; + bool m_useInstruct = false; + + +private: + void generate(); + void socketMessage(const QString& message); + QStringList getIncludePaths(const QString& text); + QString assembleContext(QPointer document, const KTextEditor::Cursor& cursor); + + QPointer activeDocument() const; + KTextEditor::Cursor getCurrentCursor() const; + + +public: + KateAiPluginView(KateAiPlugin *plugin, KTextEditor::MainWindow *mainwindow, QPointer webSocket, bool instruct = false); + ~KateAiPluginView() override; + void setInstruct(bool instruct); + + Q_SIGNAL void reconnect(); +}; diff --git a/kateai.json b/kateai.json new file mode 100644 index 0000000..f440e42 --- /dev/null +++ b/kateai.json @@ -0,0 +1,9 @@ +{ + "KPlugin": { + "Description": "Adds the ability for kate to complete text using a llm", + "Name": "Kate AI", + "ServiceTypes": [ + "KTextEditor/Plugin" + ] + } +} diff --git a/kateaiconfigpage.cpp b/kateaiconfigpage.cpp new file mode 100644 index 0000000..45df86a --- /dev/null +++ b/kateaiconfigpage.cpp @@ -0,0 +1,65 @@ +#include "kateaiconfigpage.h" + +#include +#include +#include + +#include +#include +#include + +KateAiConfigPage::KateAiConfigPage(QWidget *parent, KateAiPlugin *plugin) + : KTextEditor::ConfigPage(parent) + , m_plugin(plugin) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + QHBoxLayout* lineLayout = new QHBoxLayout(this); + QLabel* lineEditLabel = new QLabel(i18n("Url for the WebSockets ExLlama Ai server:"), this); + lineEditLabel->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); + lineLayout->addWidget(lineEditLabel); + lineLayout->addWidget(&lineUrl); + layout->addLayout(lineLayout); + + btnCompletion.setText(i18n("Use the Ai to generate a completion")); + btnInstruct.setText(i18n("Use the Ai to insert a response to a instruction")); + layout->addWidget(&btnCompletion); + layout->addWidget(&btnInstruct); + layout->addStretch(); + + reset(); +} + +QString KateAiConfigPage::name() const +{ + return i18n("Ai"); +} + +QString KateAiConfigPage::fullName() const +{ + return i18n("Ai Settings"); +} + +QIcon KateAiConfigPage::icon() const +{ + return QIcon::fromTheme(QStringLiteral("text-x-generic")); +} + +void KateAiConfigPage::apply() +{ + KConfigGroup config(KSharedConfig::openConfig(), "Ai"); + config.writeEntry("Url", lineUrl.text()); + config.writeEntry("Instruct", btnInstruct.isChecked()); + + config.sync(); + m_plugin->readConfig(); +} + +void KateAiConfigPage::reset() +{ + KConfigGroup config(KSharedConfig::openConfig(), "Ai"); + lineUrl.setText(config.readEntry("Url", "ws://localhost:8642")); + btnInstruct.setChecked(config.readEntry("Instruct", false)); + btnCompletion.setChecked(!btnInstruct.isChecked()); +} diff --git a/kateaiconfigpage.h b/kateaiconfigpage.h new file mode 100644 index 0000000..3e31b2b --- /dev/null +++ b/kateaiconfigpage.h @@ -0,0 +1,33 @@ +#pragma once + +#include "kateai.h" +#include + +#include +#include + +class KateAiConfigPage : public KTextEditor::ConfigPage +{ + Q_OBJECT +private: + QLineEdit lineUrl; + QRadioButton btnCompletion; + QRadioButton btnInstruct; + KateAiPlugin* m_plugin; + +public: + explicit KateAiConfigPage(QWidget *parent = nullptr, KateAiPlugin *plugin = nullptr); + ~KateAiConfigPage() override + { + } + + QString name() const override; + QString fullName() const override; + QIcon icon() const override; + + void apply() override; + void reset() override; + void defaults() override + { + } +}; diff --git a/plugin.qrc b/plugin.qrc new file mode 100644 index 0000000..3f14624 --- /dev/null +++ b/plugin.qrc @@ -0,0 +1,6 @@ + + + + ui.rc + + diff --git a/ui.rc b/ui.rc new file mode 100644 index 0000000..85e8750 --- /dev/null +++ b/ui.rc @@ -0,0 +1,10 @@ + + + + + + &Tools + + + +