769 lines
26 KiB
C++
769 lines
26 KiB
C++
#include "ollie_kate.h"
|
|
#include "ollie_ghost.h"
|
|
|
|
#include <KPluginFactory>
|
|
#include <QKeyEvent>
|
|
#include <KTextEditor/Document>
|
|
#include <KTextEditor/Editor>
|
|
#include <ktexteditor_version.h>
|
|
#if KTEXTEDITOR_VERSION_MAJOR < 6
|
|
#include <KTextEditor/CodeCompletionInterface>
|
|
#endif
|
|
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QPushButton>
|
|
#include <QComboBox>
|
|
#include <QDBusReply>
|
|
#include <QFont>
|
|
#include <QApplication>
|
|
#include <QMenu>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QInputDialog>
|
|
#include <QScrollBar>
|
|
#include <QCheckBox>
|
|
#include <QFormLayout>
|
|
#include <QMainWindow>
|
|
#include <QStatusBar>
|
|
#include <KSharedConfig>
|
|
#include <KConfigGroup>
|
|
|
|
K_PLUGIN_CLASS_WITH_JSON(OllieKatePlugin, "metadata.json")
|
|
|
|
static const char *SERVICE = "org.ollie.SessionManager";
|
|
static const char *OBJ_PATH = "/org/ollie/SessionManager";
|
|
static const char *IFACE = "org.ollie.SessionManager";
|
|
|
|
// --- Plugin ---
|
|
|
|
#if QT_VERSION_MAJOR >= 6
|
|
OllieKatePlugin::OllieKatePlugin(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
|
|
: KTextEditor::Plugin(parent)
|
|
{
|
|
Q_UNUSED(data);
|
|
Q_UNUSED(args);
|
|
loadConfig();
|
|
}
|
|
#else
|
|
OllieKatePlugin::OllieKatePlugin(QObject *parent, const QVariantList &args)
|
|
: KTextEditor::Plugin(parent)
|
|
{
|
|
Q_UNUSED(args);
|
|
loadConfig();
|
|
}
|
|
#endif
|
|
|
|
QObject *OllieKatePlugin::createView(KTextEditor::MainWindow *mainWindow)
|
|
{
|
|
return new OllieKateView(this, mainWindow);
|
|
}
|
|
|
|
int OllieKatePlugin::configPages() const
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
KTextEditor::ConfigPage *OllieKatePlugin::configPage(int number, QWidget *parent)
|
|
{
|
|
if (number != 0) return nullptr;
|
|
|
|
// Inline config page
|
|
class OllieConfigPage : public KTextEditor::ConfigPage
|
|
{
|
|
public:
|
|
OllieConfigPage(OllieKatePlugin *plugin, QWidget *parent)
|
|
: KTextEditor::ConfigPage(parent), m_plugin(plugin)
|
|
{
|
|
auto *layout = new QFormLayout(this);
|
|
|
|
// Default session backend/model
|
|
m_defaultBackendEdit = new QLineEdit(this);
|
|
m_defaultBackendEdit->setPlaceholderText(QStringLiteral("(use OLLIE_BACKEND)"));
|
|
m_defaultModelEdit = new QLineEdit(this);
|
|
m_defaultModelEdit->setPlaceholderText(QStringLiteral("(use OLLIE_MODEL)"));
|
|
|
|
layout->addRow(QStringLiteral("Default backend:"), m_defaultBackendEdit);
|
|
layout->addRow(QStringLiteral("Default model:"), m_defaultModelEdit);
|
|
|
|
auto *defaultNote = new QLabel(
|
|
QStringLiteral("Override OLLIE_BACKEND / OLLIE_MODEL for chat sessions."),
|
|
this);
|
|
defaultNote->setWordWrap(true);
|
|
defaultNote->setStyleSheet(QStringLiteral("color: gray;"));
|
|
layout->addRow(defaultNote);
|
|
|
|
// Completion backend/model
|
|
m_backendEdit = new QLineEdit(this);
|
|
m_backendEdit->setPlaceholderText(QStringLiteral("(use OLLIE_COMPLETE_BACKEND)"));
|
|
m_modelEdit = new QLineEdit(this);
|
|
m_modelEdit->setPlaceholderText(QStringLiteral("(use OLLIE_COMPLETE_MODEL)"));
|
|
|
|
layout->addRow(QStringLiteral("Completion backend:"), m_backendEdit);
|
|
layout->addRow(QStringLiteral("Completion model:"), m_modelEdit);
|
|
|
|
auto *note = new QLabel(
|
|
QStringLiteral("Override OLLIE_COMPLETE_BACKEND / OLLIE_COMPLETE_MODEL for ghost text."),
|
|
this);
|
|
note->setWordWrap(true);
|
|
note->setStyleSheet(QStringLiteral("color: gray;"));
|
|
layout->addRow(note);
|
|
|
|
m_enabledCheck = new QCheckBox(QStringLiteral("Completion enabled on startup"), this);
|
|
layout->addRow(m_enabledCheck);
|
|
|
|
reset();
|
|
|
|
connect(m_enabledCheck, &QCheckBox::toggled, this, &ConfigPage::changed);
|
|
connect(m_backendEdit, &QLineEdit::textChanged, this, &ConfigPage::changed);
|
|
connect(m_modelEdit, &QLineEdit::textChanged, this, &ConfigPage::changed);
|
|
connect(m_defaultBackendEdit, &QLineEdit::textChanged, this, &ConfigPage::changed);
|
|
connect(m_defaultModelEdit, &QLineEdit::textChanged, this, &ConfigPage::changed);
|
|
}
|
|
|
|
QString name() const override { return QStringLiteral("Ollie"); }
|
|
QString fullName() const override { return QStringLiteral("Ollie Completion Settings"); }
|
|
QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("system-run")); }
|
|
|
|
void apply() override {
|
|
m_plugin->setDefaultBackend(m_defaultBackendEdit->text().trimmed());
|
|
m_plugin->setDefaultModel(m_defaultModelEdit->text().trimmed());
|
|
m_plugin->setCompletionBackend(m_backendEdit->text().trimmed());
|
|
m_plugin->setCompletionModel(m_modelEdit->text().trimmed());
|
|
m_plugin->setCompletionEnabled(m_enabledCheck->isChecked());
|
|
}
|
|
void reset() override {
|
|
m_defaultBackendEdit->setText(m_plugin->defaultBackend());
|
|
m_defaultModelEdit->setText(m_plugin->defaultModel());
|
|
m_backendEdit->setText(m_plugin->completionBackend());
|
|
m_modelEdit->setText(m_plugin->completionModel());
|
|
m_enabledCheck->setChecked(m_plugin->completionEnabled());
|
|
}
|
|
void defaults() override {
|
|
m_defaultBackendEdit->clear();
|
|
m_defaultModelEdit->clear();
|
|
m_backendEdit->clear();
|
|
m_modelEdit->clear();
|
|
m_enabledCheck->setChecked(true);
|
|
}
|
|
|
|
private:
|
|
OllieKatePlugin *m_plugin;
|
|
QLineEdit *m_defaultBackendEdit;
|
|
QLineEdit *m_defaultModelEdit;
|
|
QLineEdit *m_backendEdit;
|
|
QLineEdit *m_modelEdit;
|
|
QCheckBox *m_enabledCheck;
|
|
};
|
|
|
|
return new OllieConfigPage(this, parent);
|
|
}
|
|
|
|
QString OllieKatePlugin::completionBackend() const { return m_completionBackend; }
|
|
QString OllieKatePlugin::completionModel() const { return m_completionModel; }
|
|
|
|
void OllieKatePlugin::setCompletionBackend(const QString &backend)
|
|
{
|
|
m_completionBackend = backend;
|
|
saveConfig();
|
|
}
|
|
|
|
void OllieKatePlugin::setCompletionModel(const QString &model)
|
|
{
|
|
m_completionModel = model;
|
|
saveConfig();
|
|
}
|
|
|
|
bool OllieKatePlugin::completionEnabled() const { return m_completionEnabled; }
|
|
|
|
void OllieKatePlugin::setCompletionEnabled(bool enabled)
|
|
{
|
|
m_completionEnabled = enabled;
|
|
saveConfig();
|
|
}
|
|
|
|
QString OllieKatePlugin::defaultBackend() const { return m_defaultBackend; }
|
|
QString OllieKatePlugin::defaultModel() const { return m_defaultModel; }
|
|
|
|
void OllieKatePlugin::setDefaultBackend(const QString &backend)
|
|
{
|
|
m_defaultBackend = backend;
|
|
saveConfig();
|
|
}
|
|
|
|
void OllieKatePlugin::setDefaultModel(const QString &model)
|
|
{
|
|
m_defaultModel = model;
|
|
saveConfig();
|
|
}
|
|
|
|
void OllieKatePlugin::loadConfig()
|
|
{
|
|
KConfigGroup grp(KSharedConfig::openConfig(), QStringLiteral("Ollie"));
|
|
m_completionBackend = grp.readEntry("CompletionBackend", QString());
|
|
m_completionModel = grp.readEntry("CompletionModel", QString());
|
|
m_completionEnabled = grp.readEntry("CompletionEnabled", true);
|
|
m_defaultBackend = grp.readEntry("DefaultBackend", QString());
|
|
m_defaultModel = grp.readEntry("DefaultModel", QString());
|
|
}
|
|
|
|
void OllieKatePlugin::saveConfig()
|
|
{
|
|
KConfigGroup grp(KSharedConfig::openConfig(), QStringLiteral("Ollie"));
|
|
grp.writeEntry("CompletionBackend", m_completionBackend);
|
|
grp.writeEntry("CompletionModel", m_completionModel);
|
|
grp.writeEntry("CompletionEnabled", m_completionEnabled);
|
|
grp.writeEntry("DefaultBackend", m_defaultBackend);
|
|
grp.writeEntry("DefaultModel", m_defaultModel);
|
|
grp.sync();
|
|
}
|
|
|
|
// --- View ---
|
|
|
|
OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
|
: QObject(mainWindow)
|
|
, m_plugin(plugin)
|
|
, m_mainWindow(mainWindow)
|
|
{
|
|
m_iface = new QDBusInterface(SERVICE, OBJ_PATH, IFACE,
|
|
QDBusConnection::sessionBus(), this);
|
|
|
|
// Create tool view (side panel)
|
|
m_toolView = m_mainWindow->createToolView(
|
|
plugin,
|
|
"ollie_panel",
|
|
KTextEditor::MainWindow::Right,
|
|
QIcon::fromTheme("system-run"),
|
|
"Ollie");
|
|
|
|
auto *container = new QWidget(m_toolView);
|
|
auto *layout = new QVBoxLayout(container);
|
|
layout->setContentsMargins(2, 2, 2, 2);
|
|
layout->setSpacing(2);
|
|
|
|
// Session indicator
|
|
m_sessionLabel = new QLabel("No session", container);
|
|
m_sessionLabel->setStyleSheet(QStringLiteral("color: gray; padding: 2px;"));
|
|
m_sessionLabel->setFont(QApplication::font());
|
|
layout->addWidget(m_sessionLabel);
|
|
|
|
// CWD label
|
|
m_cwdLabel = new QLabel("", container);
|
|
QFont smallFont = QApplication::font();
|
|
smallFont.setPointSizeF(smallFont.pointSizeF() * 0.85);
|
|
m_cwdLabel->setStyleSheet(QStringLiteral("color: gray; padding: 0px 2px;"));
|
|
m_cwdLabel->setFont(smallFont);
|
|
m_cwdLabel->setWordWrap(true);
|
|
layout->addWidget(m_cwdLabel);
|
|
|
|
// Completion status indicator (in the main window status bar)
|
|
bool startEnabled = m_plugin->completionEnabled();
|
|
if (auto *win = qobject_cast<QMainWindow *>(m_mainWindow->window())) {
|
|
// Reuse existing widget if another view already created it
|
|
m_completionStatus = win->statusBar()->findChild<QLabel *>(QStringLiteral("ollie_completion_status"));
|
|
if (!m_completionStatus) {
|
|
m_completionStatus = new QLabel();
|
|
m_completionStatus->setObjectName(QStringLiteral("ollie_completion_status"));
|
|
m_completionStatus->setFont(smallFont);
|
|
win->statusBar()->addPermanentWidget(m_completionStatus);
|
|
}
|
|
m_completionStatus->setText(startEnabled
|
|
? QStringLiteral("✦ completion on")
|
|
: QStringLiteral("✦ completion off"));
|
|
m_completionStatus->setStyleSheet(startEnabled
|
|
? QStringLiteral("color: green; padding: 0px 4px;")
|
|
: QStringLiteral("color: gray; padding: 0px 4px;"));
|
|
} else {
|
|
m_completionStatus = nullptr;
|
|
}
|
|
|
|
// Button bar
|
|
auto *buttonBar = new QHBoxLayout();
|
|
|
|
auto *newBtn = new QPushButton("New", container);
|
|
newBtn->setToolTip("New session in current file's directory");
|
|
connect(newBtn, &QPushButton::clicked, this, &OllieKateView::newSession);
|
|
buttonBar->addWidget(newBtn);
|
|
|
|
auto *pickBtn = new QPushButton("Pick", container);
|
|
pickBtn->setToolTip("Switch to an existing session matching the project directory");
|
|
connect(pickBtn, &QPushButton::clicked, this, &OllieKateView::pickSession);
|
|
buttonBar->addWidget(pickBtn);
|
|
|
|
auto *renameBtn = new QPushButton("Rename", container);
|
|
renameBtn->setToolTip("Rename the current session");
|
|
connect(renameBtn, &QPushButton::clicked, this, &OllieKateView::renameSession);
|
|
buttonBar->addWidget(renameBtn);
|
|
|
|
auto *askBtn = new QPushButton("Ask File", container);
|
|
askBtn->setToolTip("Ask Ollie about the current file");
|
|
connect(askBtn, &QPushButton::clicked, this, &OllieKateView::askAboutFile);
|
|
buttonBar->addWidget(askBtn);
|
|
|
|
auto *explainBtn = new QPushButton("Explain", container);
|
|
explainBtn->setToolTip("Explain selected text");
|
|
connect(explainBtn, &QPushButton::clicked, this, &OllieKateView::explainSelection);
|
|
buttonBar->addWidget(explainBtn);
|
|
|
|
buttonBar->addStretch();
|
|
layout->addLayout(buttonBar);
|
|
|
|
// Chat view
|
|
m_chatView = new QTextEdit(container);
|
|
m_chatView->setReadOnly(true);
|
|
QFont monoFont("monospace");
|
|
monoFont.setPointSize(QApplication::font().pointSize());
|
|
m_chatView->setFont(monoFont);
|
|
m_chatView->setPlaceholderText("No active session.\nClick 'New' or use context actions.");
|
|
layout->addWidget(m_chatView);
|
|
|
|
// Prompt input
|
|
auto *promptLayout = new QHBoxLayout();
|
|
m_promptInput = new QLineEdit(container);
|
|
m_promptInput->setPlaceholderText("Message...");
|
|
connect(m_promptInput, &QLineEdit::returnPressed, this, &OllieKateView::submitPrompt);
|
|
promptLayout->addWidget(m_promptInput);
|
|
|
|
auto *sendBtn = new QPushButton("⏎", container);
|
|
sendBtn->setFixedWidth(30);
|
|
connect(sendBtn, &QPushButton::clicked, this, &OllieKateView::submitPrompt);
|
|
promptLayout->addWidget(sendBtn);
|
|
|
|
layout->addLayout(promptLayout);
|
|
|
|
// Connect D-Bus signals
|
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
|
bus.connect(SERVICE, OBJ_PATH, IFACE, "ChatUpdated",
|
|
this, SLOT(onChatUpdated(QString,qlonglong,QString)));
|
|
bus.connect(SERVICE, OBJ_PATH, IFACE, "StateChanged",
|
|
this, SLOT(onStateChanged(QString,QString)));
|
|
bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionCreated",
|
|
this, SLOT(onSessionCreated(QString)));
|
|
bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionKilled",
|
|
this, SLOT(onSessionKilled(QString)));
|
|
|
|
// Pick up existing session if any
|
|
refreshSessions();
|
|
|
|
// Set up ghost-text completion on active view
|
|
setupGhostProvider();
|
|
connect(mainWindow, &KTextEditor::MainWindow::viewChanged,
|
|
this, [this](KTextEditor::View *) { setupGhostProvider(); });
|
|
}
|
|
|
|
OllieKateView::~OllieKateView()
|
|
{
|
|
delete m_ghost;
|
|
delete m_toolView;
|
|
}
|
|
|
|
// --- D-Bus signal handlers ---
|
|
|
|
void OllieKateView::onChatUpdated(const QString &session_id, qlonglong offset, const QString &new_text)
|
|
{
|
|
if (session_id != m_sessionId) return;
|
|
|
|
if (offset == m_chatOffset) {
|
|
appendChat(new_text);
|
|
m_chatOffset += new_text.size();
|
|
} else {
|
|
// Offset mismatch — refetch
|
|
QDBusMessage msg = QDBusMessage::createMethodCall(SERVICE, OBJ_PATH, IFACE, "GetChat");
|
|
msg << m_sessionId << (qlonglong)0;
|
|
QDBusMessage reply = QDBusConnection::sessionBus().call(msg);
|
|
if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() >= 2) {
|
|
m_chatView->clear();
|
|
m_chatView->setPlainText(reply.arguments()[0].toString());
|
|
m_chatOffset = reply.arguments()[1].toLongLong();
|
|
}
|
|
}
|
|
}
|
|
|
|
void OllieKateView::onStateChanged(const QString &session_id, const QString &new_state)
|
|
{
|
|
if (session_id != m_sessionId) return;
|
|
QString dot;
|
|
if (new_state == "idle") dot = "○ ";
|
|
else if (new_state.startsWith("calling")) dot = "⚡ ";
|
|
else dot = "● ";
|
|
m_sessionLabel->setText(dot + m_sessionId + " [" + new_state + "]");
|
|
}
|
|
|
|
void OllieKateView::onSessionCreated(const QString &session_id)
|
|
{
|
|
// If we have no session, adopt this one
|
|
if (m_sessionId.isEmpty()) {
|
|
m_sessionId = session_id;
|
|
m_chatView->clear();
|
|
m_chatOffset = 0;
|
|
}
|
|
}
|
|
|
|
void OllieKateView::onSessionKilled(const QString &session_id)
|
|
{
|
|
if (session_id == m_sessionId) {
|
|
m_sessionId.clear();
|
|
m_chatOffset = 0;
|
|
m_chatView->setPlainText("");
|
|
m_sessionLabel->setText("No session");
|
|
m_cwdLabel->setText("");
|
|
refreshSessions();
|
|
}
|
|
}
|
|
|
|
// --- Actions ---
|
|
|
|
void OllieKateView::submitPrompt()
|
|
{
|
|
QString text = m_promptInput->text().trimmed();
|
|
if (text.isEmpty()) return;
|
|
|
|
ensureSession();
|
|
if (m_sessionId.isEmpty()) return;
|
|
|
|
m_iface->call("Submit", m_sessionId, text);
|
|
m_promptInput->clear();
|
|
}
|
|
|
|
void OllieKateView::askAboutFile()
|
|
{
|
|
auto *view = m_mainWindow->activeView();
|
|
if (!view || !view->document()) return;
|
|
|
|
QString filePath = view->document()->url().toLocalFile();
|
|
if (filePath.isEmpty()) return;
|
|
|
|
ensureSession();
|
|
if (m_sessionId.isEmpty()) return;
|
|
|
|
QString prompt = "Describe and explain this file: " + filePath;
|
|
m_iface->call("Submit", m_sessionId, prompt);
|
|
}
|
|
|
|
void OllieKateView::explainSelection()
|
|
{
|
|
auto *view = m_mainWindow->activeView();
|
|
if (!view) return;
|
|
|
|
QString selection = view->selectionText();
|
|
if (selection.isEmpty()) {
|
|
// No selection — explain current line
|
|
auto cursor = view->cursorPosition();
|
|
selection = view->document()->line(cursor.line());
|
|
}
|
|
if (selection.isEmpty()) return;
|
|
|
|
ensureSession();
|
|
if (m_sessionId.isEmpty()) return;
|
|
|
|
QString filePath = view->document()->url().toLocalFile();
|
|
QString context = filePath.isEmpty() ? "" : " (from " + filePath + ")";
|
|
QString prompt = "Explain this code" + context + ":\n\n```\n" + selection + "\n```";
|
|
m_iface->call("Submit", m_sessionId, prompt);
|
|
}
|
|
|
|
void OllieKateView::newSession()
|
|
{
|
|
QString cwd = projectBaseDir();
|
|
if (cwd.isEmpty()) {
|
|
cwd = QDir::homePath();
|
|
}
|
|
|
|
QString backend = m_plugin->defaultBackend();
|
|
QString model = m_plugin->defaultModel();
|
|
QDBusReply<QString> reply = m_iface->call("CreateSession", cwd, backend, model, "default");
|
|
if (reply.isValid()) {
|
|
m_sessionId = reply.value();
|
|
m_chatView->clear();
|
|
m_chatOffset = 0;
|
|
m_sessionLabel->setText("○ " + m_sessionId + " [idle]");
|
|
m_cwdLabel->setText(cwd);
|
|
} else {
|
|
m_chatView->setPlainText("Error creating session: " + reply.error().message());
|
|
}
|
|
}
|
|
|
|
void OllieKateView::pickSession()
|
|
{
|
|
QString baseDir = projectBaseDir();
|
|
if (baseDir.isEmpty()) {
|
|
m_chatView->setPlainText("Cannot determine project directory.");
|
|
return;
|
|
}
|
|
|
|
QDBusReply<QStringList> reply = m_iface->call("ListSessions");
|
|
if (!reply.isValid() || reply.value().isEmpty()) {
|
|
m_chatView->setPlainText("No active sessions found.");
|
|
return;
|
|
}
|
|
|
|
// Find first session whose cwd matches the project base dir
|
|
for (const QString &entry : reply.value()) {
|
|
QStringList parts = entry.split('\t');
|
|
if (parts.isEmpty()) continue;
|
|
QString id = parts[0];
|
|
|
|
QDBusReply<QString> cfgReply = m_iface->call("GetConfig", id);
|
|
if (!cfgReply.isValid()) continue;
|
|
|
|
for (const QString &line : cfgReply.value().split('\n')) {
|
|
if (line.startsWith("cwd=")) {
|
|
QString cwd = line.mid(4);
|
|
if (cwd == baseDir) {
|
|
switchToSession(id);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_chatView->setPlainText("No session found with cwd: " + baseDir);
|
|
}
|
|
|
|
void OllieKateView::renameSession()
|
|
{
|
|
if (m_sessionId.isEmpty()) {
|
|
m_chatView->setPlainText("No active session to rename.");
|
|
return;
|
|
}
|
|
|
|
bool ok;
|
|
QString newName = QInputDialog::getText(
|
|
m_toolView, "Rename Session",
|
|
"New name for session " + m_sessionId + ":",
|
|
QLineEdit::Normal, m_sessionId, &ok);
|
|
|
|
if (!ok || newName.trimmed().isEmpty()) return;
|
|
newName = newName.trimmed();
|
|
|
|
QDBusReply<bool> reply = m_iface->call("RenameSession", m_sessionId, newName);
|
|
if (reply.isValid() && reply.value()) {
|
|
m_sessionId = newName;
|
|
QString dot = "○ ";
|
|
QDBusReply<QString> stateReply = m_iface->call("GetState", m_sessionId);
|
|
if (stateReply.isValid()) {
|
|
QString state = stateReply.value();
|
|
dot = (state == "idle") ? "○ " : (state.startsWith("calling") ? "⚡ " : "● ");
|
|
m_sessionLabel->setText(dot + m_sessionId + " [" + state + "]");
|
|
} else {
|
|
m_sessionLabel->setText(dot + m_sessionId + " [idle]");
|
|
}
|
|
} else {
|
|
m_chatView->setPlainText("Failed to rename session.");
|
|
}
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
QString OllieKateView::projectBaseDir()
|
|
{
|
|
// Try Kate's project plugin first
|
|
QObject *projectView = m_mainWindow->pluginView("kateprojectplugin");
|
|
if (projectView) {
|
|
QString dir = projectView->property("projectBaseDir").toString();
|
|
if (!dir.isEmpty()) return dir;
|
|
}
|
|
|
|
// Fall back to active file's directory
|
|
auto *view = m_mainWindow->activeView();
|
|
if (view && view->document() && !view->document()->url().toLocalFile().isEmpty()) {
|
|
return QFileInfo(view->document()->url().toLocalFile()).absolutePath();
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
void OllieKateView::switchToSession(const QString &sessionId)
|
|
{
|
|
m_sessionId = sessionId;
|
|
m_chatView->clear();
|
|
m_chatOffset = 0;
|
|
|
|
// Fetch state
|
|
QDBusReply<QString> stateReply = m_iface->call("GetState", m_sessionId);
|
|
QString state = stateReply.isValid() ? stateReply.value() : "idle";
|
|
QString dot = (state == "idle") ? "○ " : (state.startsWith("calling") ? "⚡ " : "● ");
|
|
m_sessionLabel->setText(dot + m_sessionId + " [" + state + "]");
|
|
|
|
// Fetch cwd
|
|
QDBusReply<QString> cfgReply = m_iface->call("GetConfig", m_sessionId);
|
|
if (cfgReply.isValid()) {
|
|
for (const QString &line : cfgReply.value().split('\n')) {
|
|
if (line.startsWith("cwd=")) {
|
|
m_cwdLabel->setText(line.mid(4));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch existing chat
|
|
QDBusMessage msg = QDBusMessage::createMethodCall(SERVICE, OBJ_PATH, IFACE, "GetChat");
|
|
msg << m_sessionId << (qlonglong)0;
|
|
QDBusMessage chatReply = QDBusConnection::sessionBus().call(msg);
|
|
if (chatReply.type() == QDBusMessage::ReplyMessage && chatReply.arguments().size() >= 2) {
|
|
m_chatView->setPlainText(chatReply.arguments()[0].toString());
|
|
m_chatOffset = chatReply.arguments()[1].toLongLong();
|
|
}
|
|
}
|
|
|
|
void OllieKateView::ensureSession()
|
|
{
|
|
if (!m_sessionId.isEmpty()) return;
|
|
if (!m_iface->isValid()) {
|
|
m_chatView->setPlainText("Error: ollie-kde daemon not running.\nStart with: ollie-kde or rc-service --user ollie-kde start");
|
|
return;
|
|
}
|
|
newSession();
|
|
}
|
|
|
|
QString OllieKateView::activeSessionId()
|
|
{
|
|
return m_sessionId;
|
|
}
|
|
|
|
void OllieKateView::appendChat(const QString &text)
|
|
{
|
|
QTextCursor cursor = m_chatView->textCursor();
|
|
cursor.movePosition(QTextCursor::End);
|
|
cursor.insertText(text);
|
|
m_chatView->verticalScrollBar()->setValue(m_chatView->verticalScrollBar()->maximum());
|
|
}
|
|
|
|
void OllieKateView::refreshSessions()
|
|
{
|
|
QDBusReply<QStringList> reply = m_iface->call("ListSessions");
|
|
if (!reply.isValid()) return;
|
|
|
|
QStringList sessions = reply.value();
|
|
if (sessions.isEmpty()) {
|
|
m_sessionId.clear();
|
|
return;
|
|
}
|
|
|
|
// If we already have a session, keep it
|
|
if (!m_sessionId.isEmpty()) return;
|
|
|
|
// Try to find a session matching the project base dir
|
|
QString baseDir = projectBaseDir();
|
|
if (!baseDir.isEmpty()) {
|
|
for (const QString &entry : sessions) {
|
|
QStringList parts = entry.split('\t');
|
|
if (parts.isEmpty()) continue;
|
|
QString id = parts[0];
|
|
|
|
QDBusReply<QString> cfgReply = m_iface->call("GetConfig", id);
|
|
if (!cfgReply.isValid()) continue;
|
|
|
|
for (const QString &line : cfgReply.value().split('\n')) {
|
|
if (line.startsWith("cwd=")) {
|
|
if (line.mid(4) == baseDir) {
|
|
switchToSession(id);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to first session
|
|
QStringList parts = sessions.first().split('\t');
|
|
if (!parts.isEmpty()) {
|
|
switchToSession(parts[0]);
|
|
}
|
|
}
|
|
|
|
void OllieKateView::setupGhostProvider()
|
|
{
|
|
auto *view = m_mainWindow->activeView();
|
|
if (!view)
|
|
return;
|
|
|
|
// If ghost provider is already on this view, keep it
|
|
if (m_ghost && m_ghost->view() == view)
|
|
return;
|
|
|
|
// Remove old provider
|
|
delete m_ghost;
|
|
m_ghost = nullptr;
|
|
|
|
// Create new ghost provider for the active view
|
|
m_ghost = new OllieGhostProvider(view, m_iface, m_plugin, view);
|
|
m_ghost->setEnabled(m_plugin->completionEnabled());
|
|
|
|
// Install event filter on the view widget for Tab interception
|
|
if (QWidget *proxy = view->focusProxy()) {
|
|
proxy->installEventFilter(this);
|
|
}
|
|
}
|
|
|
|
bool OllieKateView::eventFilter(QObject *obj, QEvent *event)
|
|
{
|
|
if (event->type() == QEvent::KeyPress && m_ghost) {
|
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
|
|
|
// Second key of Meta+O chord
|
|
if (m_ghostPrefix) {
|
|
m_ghostPrefix = false;
|
|
switch (keyEvent->key()) {
|
|
case Qt::Key_N:
|
|
m_ghost->cycle(+1);
|
|
return true;
|
|
case Qt::Key_P:
|
|
m_ghost->cycle(-1);
|
|
return true;
|
|
case Qt::Key_C:
|
|
m_ghost->setEnabled(!m_ghost->isEnabled());
|
|
if (m_ghost->isEnabled()) {
|
|
m_completionStatus->setText(QStringLiteral("\u2726 completion on"));
|
|
m_completionStatus->setStyleSheet(QStringLiteral("color: green; padding: 0px 2px;"));
|
|
} else {
|
|
m_completionStatus->setText(QStringLiteral("\u2726 completion off"));
|
|
m_completionStatus->setStyleSheet(QStringLiteral("color: gray; padding: 0px 2px;"));
|
|
}
|
|
return true;
|
|
case Qt::Key_D:
|
|
m_ghost->dismiss();
|
|
return true;
|
|
case Qt::Key_Tab:
|
|
m_ghost->requestCompletion();
|
|
return true;
|
|
default:
|
|
// Not a recognized chord — pass through
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Meta+O starts the chord prefix
|
|
if (keyEvent->key() == Qt::Key_O && keyEvent->modifiers() == Qt::MetaModifier) {
|
|
m_ghostPrefix = true;
|
|
return true;
|
|
}
|
|
|
|
if (m_ghost->hasSuggestion()) {
|
|
// Don't intercept keys while a popup or completion widget is active
|
|
auto *activeView = m_mainWindow->activeView();
|
|
#if KTEXTEDITOR_VERSION_MAJOR >= 6
|
|
if (QApplication::activePopupWidget() ||
|
|
(activeView && activeView->isCompletionActive()))
|
|
return QObject::eventFilter(obj, event);
|
|
#else
|
|
auto *ccIface = activeView ? dynamic_cast<KTextEditor::CodeCompletionInterface *>(activeView) : nullptr;
|
|
if (QApplication::activePopupWidget() ||
|
|
(ccIface && ccIface->isCompletionActive()))
|
|
return QObject::eventFilter(obj, event);
|
|
#endif
|
|
|
|
if (keyEvent->key() == Qt::Key_Tab && keyEvent->modifiers() == Qt::NoModifier) {
|
|
if (m_ghost->accept()) {
|
|
return true; // consumed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return QObject::eventFilter(obj, event);
|
|
}
|
|
|
|
#include "ollie_kate.moc"
|