649 lines
21 KiB
C++
649 lines
21 KiB
C++
#include "ollie_kate.h"
|
|
#include "ollie_ghost.h"
|
|
|
|
#include <KPluginFactory>
|
|
#include <KSharedConfig>
|
|
#include <KConfigGroup>
|
|
#include <KTextEditor/Document>
|
|
#include <KTextEditor/Editor>
|
|
#include <ktexteditor_version.h>
|
|
#if KTEXTEDITOR_VERSION_MAJOR < 6
|
|
#include <KTextEditor/CodeCompletionInterface>
|
|
#endif
|
|
|
|
#include <QApplication>
|
|
#include <QCheckBox>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QHash>
|
|
#include <QKeyEvent>
|
|
#include <QMainWindow>
|
|
#include <QMenu>
|
|
#include <QStatusBar>
|
|
#include <QVBoxLayout>
|
|
|
|
K_PLUGIN_CLASS_WITH_JSON(OllieKatePlugin, "metadata.json")
|
|
|
|
// --- 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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void OllieKatePlugin::saveConfig()
|
|
{
|
|
KConfigGroup grp(KSharedConfig::openConfig(), QStringLiteral("Ollie"));
|
|
grp.writeEntry("CompletionEnabled", m_completionEnabled);
|
|
grp.sync();
|
|
}
|
|
|
|
void OllieKatePlugin::setCompletionEnabled(bool enabled)
|
|
{
|
|
m_completionEnabled = enabled;
|
|
saveConfig();
|
|
}
|
|
|
|
int OllieKatePlugin::configPages() const
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
KTextEditor::ConfigPage *OllieKatePlugin::configPage(int number, QWidget *parent)
|
|
{
|
|
if (number != 0) return nullptr;
|
|
|
|
class OllieConfigPage : public KTextEditor::ConfigPage
|
|
{
|
|
public:
|
|
OllieConfigPage(OllieKatePlugin *plugin, QWidget *parent)
|
|
: KTextEditor::ConfigPage(parent), m_plugin(plugin)
|
|
{
|
|
auto *layout = new QVBoxLayout(this);
|
|
m_enabledCheck = new QCheckBox(QStringLiteral("Automatic completion (ghost text)"), this);
|
|
layout->addWidget(m_enabledCheck);
|
|
layout->addStretch();
|
|
reset();
|
|
connect(m_enabledCheck, &QCheckBox::toggled, this, &ConfigPage::changed);
|
|
}
|
|
|
|
QString name() const override { return QStringLiteral("Ollie"); }
|
|
QString fullName() const override { return QStringLiteral("Ollie Settings"); }
|
|
QIcon icon() const override { return QIcon::fromTheme(QStringLiteral("system-run")); }
|
|
|
|
void apply() override {
|
|
m_plugin->setCompletionEnabled(m_enabledCheck->isChecked());
|
|
}
|
|
void reset() override {
|
|
m_enabledCheck->setChecked(m_plugin->completionEnabled());
|
|
}
|
|
void defaults() override {
|
|
m_enabledCheck->setChecked(true);
|
|
}
|
|
|
|
private:
|
|
OllieKatePlugin *m_plugin;
|
|
QCheckBox *m_enabledCheck;
|
|
};
|
|
|
|
return new OllieConfigPage(this, parent);
|
|
}
|
|
|
|
// --- View ---
|
|
|
|
OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
|
: QObject(mainWindow)
|
|
, m_plugin(plugin)
|
|
, m_mainWindow(mainWindow)
|
|
, m_9p(new Lib9pClient(this))
|
|
, m_heartbeatTimer(new QTimer(this))
|
|
{
|
|
// Connect to 9P server
|
|
m_connected = m_9p->connectDefault();
|
|
if (!m_connected) {
|
|
qWarning() << "Ollie: failed to connect to 9P server:" << m_9p->lastError();
|
|
}
|
|
|
|
// Heartbeat timer — detect server death and reconnect
|
|
m_heartbeatTimer->setInterval(5000);
|
|
connect(m_heartbeatTimer, &QTimer::timeout, this, &OllieKateView::heartbeat);
|
|
m_heartbeatTimer->start();
|
|
|
|
// Status bar indicator
|
|
if (auto *win = qobject_cast<QMainWindow *>(m_mainWindow->window())) {
|
|
m_statusLabel = new QLabel();
|
|
m_statusLabel->setObjectName(QStringLiteral("ollie_status"));
|
|
QFont smallFont = QApplication::font();
|
|
smallFont.setPointSizeF(smallFont.pointSizeF() * 0.85);
|
|
m_statusLabel->setFont(smallFont);
|
|
m_statusLabel->setStyleSheet(QStringLiteral("color: gray; padding: 0px 4px;"));
|
|
win->statusBar()->addPermanentWidget(m_statusLabel);
|
|
}
|
|
|
|
// Auto-select agent for current project
|
|
selectAgentForProject();
|
|
updateStatusLabel();
|
|
|
|
// Ghost text completion
|
|
setupGhostProvider();
|
|
|
|
// React to view/project changes
|
|
connect(mainWindow, &KTextEditor::MainWindow::viewChanged,
|
|
this, [this](KTextEditor::View *view) {
|
|
setupGhostProvider();
|
|
if (view) setupContextMenu(view);
|
|
selectAgentForProject();
|
|
updateStatusLabel();
|
|
});
|
|
|
|
QObject *projectView = mainWindow->pluginView("kateprojectplugin");
|
|
if (projectView) {
|
|
QObject::connect(projectView, SIGNAL(projectChanged()),
|
|
this, SLOT(onProjectChanged()));
|
|
}
|
|
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 78, 0)
|
|
connect(mainWindow, &KTextEditor::MainWindow::pluginViewCreated,
|
|
this, [this](const QString &name, QObject *view) {
|
|
if (name == "kateprojectplugin") {
|
|
QObject::connect(view, SIGNAL(projectChanged()),
|
|
this, SLOT(onProjectChanged()));
|
|
selectAgentForProject();
|
|
updateStatusLabel();
|
|
}
|
|
});
|
|
#endif
|
|
|
|
// Context menu on initial view
|
|
if (auto *view = mainWindow->activeView())
|
|
setupContextMenu(view);
|
|
|
|
// Git++ "Review Diff" action
|
|
setupGitPlusPlusDiffAction();
|
|
}
|
|
|
|
OllieKateView::~OllieKateView()
|
|
{
|
|
m_heartbeatTimer->stop();
|
|
delete m_ghost;
|
|
if (m_statusLabel) {
|
|
delete m_statusLabel;
|
|
}
|
|
}
|
|
|
|
// --- Agent Management ---
|
|
|
|
void OllieKateView::ensureAgent()
|
|
{
|
|
if (!m_agentId.isEmpty()) return;
|
|
|
|
// Ensure kate session exists
|
|
if (m_sessionId.isEmpty()) {
|
|
m_9p->write("session/new", "name=kate\n");
|
|
m_sessionId = "kate";
|
|
}
|
|
|
|
// Look for an existing agent in the kate session
|
|
QStringList agents = m_9p->ls("session/kate/agent");
|
|
agents.removeAll("new");
|
|
agents.removeAll("idx");
|
|
|
|
if (!agents.isEmpty()) {
|
|
m_agentId = agents.first();
|
|
} else {
|
|
// Create one
|
|
QString cwd = projectBaseDir();
|
|
if (cwd.isEmpty()) cwd = QDir::homePath();
|
|
|
|
QString agentCfg = "cwd=" + cwd + "\n";
|
|
QByteArray resp = m_9p->rdwr("session/kate/agent/new", agentCfg.toUtf8());
|
|
if (!resp.isEmpty()) {
|
|
m_agentId = QString::fromUtf8(resp).trimmed();
|
|
}
|
|
}
|
|
updateStatusLabel();
|
|
}
|
|
|
|
bool OllieKateView::selectAgentForProject()
|
|
{
|
|
// Check if kate session exists
|
|
QByteArray sessCheck = m_9p->read("session/kate/id");
|
|
if (sessCheck.isEmpty()) return false;
|
|
m_sessionId = "kate";
|
|
|
|
// Find first agent in the session
|
|
QStringList agents = m_9p->ls("session/kate/agent");
|
|
agents.removeAll("new");
|
|
agents.removeAll("idx");
|
|
|
|
if (agents.isEmpty()) return false;
|
|
|
|
QString agentId = agents.first();
|
|
if (agentId != m_agentId) {
|
|
m_agentId = agentId;
|
|
}
|
|
|
|
// Update cwd to match current project
|
|
QString baseDir = projectBaseDir();
|
|
if (!baseDir.isEmpty() && !m_agentId.isEmpty()) {
|
|
m_9p->write(agentPath() + "/ctl", ("cwd " + baseDir).toUtf8());
|
|
}
|
|
|
|
updateStatusLabel();
|
|
return true;
|
|
}
|
|
|
|
void OllieKateView::switchToAgent(const QString &agentId)
|
|
{
|
|
m_sessionId = "kate";
|
|
m_agentId = agentId;
|
|
updateStatusLabel();
|
|
}
|
|
|
|
void OllieKateView::onProjectChanged()
|
|
{
|
|
// Update the agent's cwd to the new project directory
|
|
QString baseDir = projectBaseDir();
|
|
if (!baseDir.isEmpty() && !m_agentId.isEmpty()) {
|
|
m_9p->write(agentPath() + "/ctl", ("cwd " + baseDir).toUtf8());
|
|
}
|
|
updateStatusLabel();
|
|
}
|
|
|
|
QString OllieKateView::projectBaseDir()
|
|
{
|
|
QObject *projectView = m_mainWindow->pluginView("kateprojectplugin");
|
|
if (projectView) {
|
|
QString dir = projectView->property("projectBaseDir").toString();
|
|
if (!dir.isEmpty()) return dir;
|
|
}
|
|
|
|
auto *view = m_mainWindow->activeView();
|
|
if (view && view->document() && !view->document()->url().toLocalFile().isEmpty()) {
|
|
return QFileInfo(view->document()->url().toLocalFile()).absolutePath();
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
QString OllieKateView::agentPath() const
|
|
{
|
|
if (m_sessionId.isEmpty() || m_agentId.isEmpty()) return {};
|
|
return "session/" + m_sessionId + "/agent/" + m_agentId;
|
|
}
|
|
|
|
void OllieKateView::updateStatusLabel()
|
|
{
|
|
if (!m_statusLabel) return;
|
|
if (!m_connected) {
|
|
m_statusLabel->setText(QStringLiteral("✗ ollie: disconnected"));
|
|
m_statusLabel->setStyleSheet(QStringLiteral("color: red; padding: 0px 4px;"));
|
|
} else if (m_agentId.isEmpty()) {
|
|
m_statusLabel->setText(QStringLiteral("○ ollie: no agent"));
|
|
m_statusLabel->setStyleSheet(QStringLiteral("color: gray; padding: 0px 4px;"));
|
|
} else {
|
|
m_statusLabel->setText(QStringLiteral("● ollie: kate/") + m_agentId);
|
|
m_statusLabel->setStyleSheet(QStringLiteral("color: green; padding: 0px 4px;"));
|
|
}
|
|
}
|
|
|
|
// --- Connection Recovery ---
|
|
|
|
void OllieKateView::heartbeat()
|
|
{
|
|
// Try a cheap read to verify daemon is alive
|
|
if (!m_9p->isConnected()) {
|
|
if (!m_9p->connectDefault()) {
|
|
if (m_connected) {
|
|
m_connected = false;
|
|
updateStatusLabel();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
QByteArray result = m_9p->read(QStringLiteral("help"));
|
|
bool alive = !result.isEmpty();
|
|
|
|
if (alive && !m_connected) {
|
|
// Recovered — re-establish state
|
|
m_connected = true;
|
|
selectAgentForProject();
|
|
updateStatusLabel();
|
|
} else if (!alive && m_connected) {
|
|
// Lost connection
|
|
m_connected = false;
|
|
m_9p->disconnect();
|
|
updateStatusLabel();
|
|
}
|
|
}
|
|
|
|
// --- Context Menu ---
|
|
|
|
void OllieKateView::setupContextMenu(KTextEditor::View *view)
|
|
{
|
|
if (!view) return;
|
|
|
|
connect(view, &KTextEditor::View::contextMenuAboutToShow,
|
|
this, [this](KTextEditor::View *v, QMenu *menu) {
|
|
// Remove previous ollie actions
|
|
const auto actions = menu->actions();
|
|
for (auto *a : actions) {
|
|
if (a->objectName().startsWith(QStringLiteral("ollie_ctx_"))) {
|
|
menu->removeAction(a);
|
|
delete a;
|
|
}
|
|
}
|
|
|
|
auto *sep = menu->addSeparator();
|
|
sep->setObjectName(QStringLiteral("ollie_ctx_sep"));
|
|
|
|
auto *askAction = menu->addAction(QIcon::fromTheme(QStringLiteral("help-about")),
|
|
QStringLiteral("Ollie: Ask about file"));
|
|
askAction->setObjectName(QStringLiteral("ollie_ctx_ask"));
|
|
connect(askAction, &QAction::triggered, this, &OllieKateView::askAboutFile);
|
|
|
|
// "Review Diff" when document is a diff
|
|
if (v->document()->highlightingMode() == QStringLiteral("Diff")) {
|
|
auto *reviewDiffAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-preview")),
|
|
QStringLiteral("Ollie: Review Diff"));
|
|
reviewDiffAction->setObjectName(QStringLiteral("ollie_ctx_review_diff"));
|
|
connect(reviewDiffAction, &QAction::triggered, this, &OllieKateView::reviewDiff);
|
|
}
|
|
|
|
auto *startAction = menu->addAction(QIcon::fromTheme(QStringLiteral("system-run")),
|
|
QStringLiteral("Ollie: Start Session"));
|
|
startAction->setObjectName(QStringLiteral("ollie_ctx_start"));
|
|
connect(startAction, &QAction::triggered, this, &OllieKateView::startSession);
|
|
|
|
if (!v->selection()) return;
|
|
|
|
auto *explainAction = menu->addAction(QIcon::fromTheme(QStringLiteral("help-contents")),
|
|
QStringLiteral("Ollie: Explain this"));
|
|
explainAction->setObjectName(QStringLiteral("ollie_ctx_explain"));
|
|
connect(explainAction, &QAction::triggered, this, &OllieKateView::explainSelection);
|
|
|
|
auto *fixAction = menu->addAction(QIcon::fromTheme(QStringLiteral("tools-wizard")),
|
|
QStringLiteral("Ollie: Fix this"));
|
|
fixAction->setObjectName(QStringLiteral("ollie_ctx_fix"));
|
|
connect(fixAction, &QAction::triggered, this, [this]() { submitSelectionPrompt(QStringLiteral("Fix")); });
|
|
|
|
auto *refactorAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-rename")),
|
|
QStringLiteral("Ollie: Refactor this"));
|
|
refactorAction->setObjectName(QStringLiteral("ollie_ctx_refactor"));
|
|
connect(refactorAction, &QAction::triggered, this, [this]() { submitSelectionPrompt(QStringLiteral("Refactor")); });
|
|
|
|
auto *testAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-new")),
|
|
QStringLiteral("Ollie: Add tests for this"));
|
|
testAction->setObjectName(QStringLiteral("ollie_ctx_test"));
|
|
connect(testAction, &QAction::triggered, this, [this]() { submitSelectionPrompt(QStringLiteral("Write unit tests for")); });
|
|
|
|
auto *docAction = menu->addAction(QIcon::fromTheme(QStringLiteral("documentation")),
|
|
QStringLiteral("Ollie: Document this"));
|
|
docAction->setObjectName(QStringLiteral("ollie_ctx_doc"));
|
|
connect(docAction, &QAction::triggered, this, [this]() { submitSelectionPrompt(QStringLiteral("Add documentation comments to")); });
|
|
|
|
auto *verbatimAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-send")),
|
|
QStringLiteral("Ollie: Send verbatim"));
|
|
verbatimAction->setObjectName(QStringLiteral("ollie_ctx_verbatim"));
|
|
connect(verbatimAction, &QAction::triggered, this, &OllieKateView::sendVerbatim);
|
|
});
|
|
}
|
|
|
|
void OllieKateView::setupGitPlusPlusDiffAction()
|
|
{
|
|
QObject *gitView = m_mainWindow->pluginView(QStringLiteral("gitplusplus"));
|
|
if (!gitView) return;
|
|
|
|
auto *action = new QAction(QIcon::fromTheme(QStringLiteral("document-preview")),
|
|
QStringLiteral("Ollie: Review Diff"), this);
|
|
connect(action, &QAction::triggered, this, &OllieKateView::reviewDiff);
|
|
|
|
QMetaObject::invokeMethod(gitView, "registerDiffAction",
|
|
Q_ARG(QAction *, action));
|
|
}
|
|
|
|
// --- Actions ---
|
|
|
|
void OllieKateView::askAboutFile()
|
|
{
|
|
auto *view = m_mainWindow->activeView();
|
|
if (!view || !view->document()) return;
|
|
|
|
QString filePath = view->document()->url().toLocalFile();
|
|
if (filePath.isEmpty()) return;
|
|
|
|
ensureAgent();
|
|
if (m_agentId.isEmpty()) return;
|
|
|
|
QString prompt = "Describe and explain this file: " + filePath;
|
|
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
|
}
|
|
|
|
void OllieKateView::explainSelection()
|
|
{
|
|
auto *view = m_mainWindow->activeView();
|
|
if (!view) return;
|
|
|
|
QString selection = view->selectionText();
|
|
if (selection.isEmpty()) {
|
|
auto cursor = view->cursorPosition();
|
|
selection = view->document()->line(cursor.line());
|
|
}
|
|
if (selection.isEmpty()) return;
|
|
|
|
ensureAgent();
|
|
if (m_agentId.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_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
|
}
|
|
|
|
void OllieKateView::submitSelectionPrompt(const QString &action)
|
|
{
|
|
auto *view = m_mainWindow->activeView();
|
|
if (!view || !view->selection()) return;
|
|
|
|
QString selection = view->selectionText();
|
|
if (selection.isEmpty()) return;
|
|
|
|
ensureAgent();
|
|
if (m_agentId.isEmpty()) return;
|
|
|
|
QString filePath = view->document()->url().toLocalFile();
|
|
auto cursor = view->selectionRange().start();
|
|
QString location;
|
|
if (!filePath.isEmpty()) {
|
|
location = " in `" + filePath + ":" + QString::number(cursor.line() + 1) + "`";
|
|
}
|
|
|
|
QString prompt = action + " this code" + location + ":\n\n```\n" + selection + "\n```";
|
|
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
|
}
|
|
|
|
void OllieKateView::sendVerbatim()
|
|
{
|
|
auto *view = m_mainWindow->activeView();
|
|
if (!view || !view->selection()) return;
|
|
|
|
QString selection = view->selectionText();
|
|
if (selection.isEmpty()) return;
|
|
|
|
ensureAgent();
|
|
if (m_agentId.isEmpty()) return;
|
|
|
|
// Derive fence language from document highlighting mode
|
|
QString mode = view->document()->highlightingMode().toLower();
|
|
// Map common Kate highlighting names to fence languages
|
|
static const QHash<QString, QString> modeMap = {
|
|
{"c++", "cpp"}, {"c", "c"}, {"cmake", "cmake"},
|
|
{"javascript", "javascript"}, {"typescript", "typescript"},
|
|
{"python", "python"}, {"rust", "rust"}, {"go", "go"},
|
|
{"bash", "bash"}, {"zsh", "bash"}, {"shell", "bash"},
|
|
{"json", "json"}, {"yaml", "yaml"}, {"toml", "toml"},
|
|
{"html", "html"}, {"css", "css"}, {"sql", "sql"},
|
|
{"java", "java"}, {"kotlin", "kotlin"}, {"ruby", "ruby"},
|
|
{"perl", "perl"}, {"lua", "lua"}, {"diff", "diff"},
|
|
{"markdown", "markdown"}, {"xml", "xml"},
|
|
};
|
|
QString lang = modeMap.value(mode, mode);
|
|
|
|
QString prompt = "```" + lang + "\n" + selection + "\n```";
|
|
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
|
}
|
|
|
|
void OllieKateView::reviewDiff()
|
|
{
|
|
QString diff;
|
|
|
|
QObject *gitView = m_mainWindow->pluginView(QStringLiteral("gitplusplus"));
|
|
if (gitView) {
|
|
QByteArray data = gitView->property("lastDiff").toByteArray();
|
|
if (!data.isEmpty())
|
|
diff = QString::fromUtf8(data);
|
|
}
|
|
|
|
if (diff.isEmpty()) {
|
|
auto *view = m_mainWindow->activeView();
|
|
if (view && view->document())
|
|
diff = view->document()->text();
|
|
}
|
|
|
|
if (diff.trimmed().isEmpty()) return;
|
|
|
|
ensureAgent();
|
|
if (m_agentId.isEmpty()) return;
|
|
|
|
QString prompt = QStringLiteral("Review this diff. Point out potential issues, bugs, "
|
|
"or improvements:\n\n```diff\n") + diff + QStringLiteral("\n```");
|
|
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
|
|
}
|
|
|
|
void OllieKateView::startSession()
|
|
{
|
|
ensureAgent();
|
|
// Update cwd to current project
|
|
QString baseDir = projectBaseDir();
|
|
if (!baseDir.isEmpty() && !m_agentId.isEmpty()) {
|
|
m_9p->write(agentPath() + "/ctl", ("cwd " + baseDir).toUtf8());
|
|
}
|
|
updateStatusLabel();
|
|
}
|
|
|
|
// --- Ghost Text ---
|
|
|
|
void OllieKateView::setupGhostProvider()
|
|
{
|
|
auto *view = m_mainWindow->activeView();
|
|
if (!view) return;
|
|
|
|
if (m_ghost && m_ghost->view() == view) return;
|
|
|
|
if (m_ghost) {
|
|
delete m_ghost;
|
|
m_ghost = nullptr;
|
|
}
|
|
|
|
m_ghost = new OllieGhostProvider(view, m_plugin, this);
|
|
m_ghost->setEnabled(m_plugin->completionEnabled());
|
|
|
|
if (QWidget *proxy = view->focusProxy()) {
|
|
proxy->installEventFilter(this);
|
|
}
|
|
}
|
|
|
|
// --- Event Filter (ghost text key handling) ---
|
|
|
|
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_statusLabel) {
|
|
// Update status to reflect completion state
|
|
updateStatusLabel();
|
|
}
|
|
return true;
|
|
case Qt::Key_D:
|
|
m_ghost->dismiss();
|
|
return true;
|
|
case Qt::Key_Tab:
|
|
m_ghost->requestCompletion();
|
|
return true;
|
|
default:
|
|
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()) {
|
|
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;
|
|
}
|
|
}
|
|
if (keyEvent->key() == Qt::Key_Escape) {
|
|
m_ghost->dismiss();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return QObject::eventFilter(obj, event);
|
|
}
|
|
|
|
#include "ollie_kate.moc"
|