kate: add Ollie context menu to project tree view

Right-click on files/directories in Kate's project panel now shows
Ollie actions:

For directories:
- Ask about directory (explain purpose/structure)
- Start Session Here

For files:
- Ask about file (explain purpose/design)
- Review file (code review)
- Write tests

Constructs file paths by walking up the tree model and joining with
the project base directory.
This commit is contained in:
Levi Neely 2026-10-09 10:51:35 +02:00
parent 2381ba4fbd
commit a4c659f591
2 changed files with 140 additions and 0 deletions

View File

@ -172,6 +172,9 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
if (auto *view = mainWindow->activeView())
setupContextMenu(view);
// Project tree context menu
setupProjectTreeContextMenu();
// Git++ "Review Diff" action (third-party plugin)
setupGitPlusPlusDiffAction();
@ -542,6 +545,140 @@ void OllieKateView::onGitTreeContextMenu(const QPoint &)
// Unused - context menu integration not possible without Kate modification
}
void OllieKateView::setupProjectTreeContextMenu()
{
// Find the project tree view — it's inside the kateproject tool view
QWidget *projectToolView = m_mainWindow->window()->findChild<QWidget *>(QStringLiteral("kateproject"));
if (!projectToolView) {
// Schedule retry — plugin view might not be created yet
QTimer::singleShot(2000, this, &OllieKateView::setupProjectTreeContextMenu);
return;
}
// Find the tree view inside the project tool view
auto *treeView = projectToolView->findChild<QTreeView *>();
if (!treeView) {
QTimer::singleShot(2000, this, &OllieKateView::setupProjectTreeContextMenu);
return;
}
// Avoid duplicate setup
if (m_projectTreeView == treeView) {
return;
}
m_projectTreeView = treeView;
// Enable custom context menu
treeView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(treeView, &QTreeView::customContextMenuRequested,
this, &OllieKateView::onProjectTreeContextMenu);
}
void OllieKateView::onProjectTreeContextMenu(const QPoint &pos)
{
if (!m_projectTreeView) return;
auto *model = m_projectTreeView->model();
if (!model) return;
QModelIndex index = m_projectTreeView->indexAt(pos);
if (!index.isValid()) return;
// Get the file path from the model
// Kate's project model stores the path in UserRole or as a property
QString filePath = model->data(index, Qt::UserRole).toString();
if (filePath.isEmpty()) {
// Try DisplayRole as fallback and construct path
QString name = model->data(index, Qt::DisplayRole).toString();
QString baseDir = projectBaseDir();
if (!baseDir.isEmpty() && !name.isEmpty()) {
// Walk up the tree to construct full path
QStringList parts;
parts.prepend(name);
QModelIndex parent = index.parent();
while (parent.isValid()) {
parts.prepend(model->data(parent, Qt::DisplayRole).toString());
parent = parent.parent();
}
// Remove the project root name (first element) and join
if (!parts.isEmpty()) parts.removeFirst();
filePath = baseDir + QStringLiteral("/") + parts.join(QStringLiteral("/"));
}
}
if (filePath.isEmpty()) return;
QFileInfo fi(filePath);
bool isDir = fi.isDir();
QMenu menu(m_projectTreeView);
if (isDir) {
auto *askAction = menu.addAction(QIcon::fromTheme(QStringLiteral("help-about")),
QStringLiteral("Ollie: Ask about directory"));
connect(askAction, &QAction::triggered, this, [this, filePath]() {
ensureAgent();
QString baseDir = projectBaseDir();
QString relPath = filePath;
if (!baseDir.isEmpty() && filePath.startsWith(baseDir)) {
relPath = filePath.mid(baseDir.length() + 1);
}
QString prompt = QStringLiteral("Explain the purpose and structure of the directory: ") + relPath;
m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8());
});
auto *startAction = menu.addAction(QIcon::fromTheme(QStringLiteral("system-run")),
QStringLiteral("Ollie: Start Session Here"));
connect(startAction, &QAction::triggered, this, [this, filePath]() {
ensureAgent();
QString prompt = QStringLiteral("I want to work on code in: ") + filePath;
m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8());
});
} else {
auto *askAction = menu.addAction(QIcon::fromTheme(QStringLiteral("help-about")),
QStringLiteral("Ollie: Ask about file"));
connect(askAction, &QAction::triggered, this, [this, filePath]() {
ensureAgent();
QString baseDir = projectBaseDir();
QString relPath = filePath;
if (!baseDir.isEmpty() && filePath.startsWith(baseDir)) {
relPath = filePath.mid(baseDir.length() + 1);
}
QString prompt = QStringLiteral("Explain the purpose and design of the file: ") + relPath;
m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8());
});
auto *reviewAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-preview")),
QStringLiteral("Ollie: Review file"));
connect(reviewAction, &QAction::triggered, this, [this, filePath]() {
ensureAgent();
QString baseDir = projectBaseDir();
QString relPath = filePath;
if (!baseDir.isEmpty() && filePath.startsWith(baseDir)) {
relPath = filePath.mid(baseDir.length() + 1);
}
QString prompt = QStringLiteral("Review the code in ") + relPath + QStringLiteral(" for bugs, issues, and improvements.");
m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8());
});
auto *testAction = menu.addAction(QIcon::fromTheme(QStringLiteral("system-run")),
QStringLiteral("Ollie: Write tests"));
connect(testAction, &QAction::triggered, this, [this, filePath]() {
ensureAgent();
QString baseDir = projectBaseDir();
QString relPath = filePath;
if (!baseDir.isEmpty() && filePath.startsWith(baseDir)) {
relPath = filePath.mid(baseDir.length() + 1);
}
QString prompt = QStringLiteral("Write tests for the code in ") + relPath;
m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8());
});
}
menu.exec(m_projectTreeView->viewport()->mapToGlobal(pos));
}
void OllieKateView::onWidgetAdded(QWidget *widget)
{
// Check if this is a DiffWidget by looking for characteristic properties

View File

@ -91,9 +91,11 @@ private:
// Context menu
void setupContextMenu(KTextEditor::View *view);
void setupProjectTreeContextMenu();
void setupGitPlusPlusDiffAction();
void setupProjectGitWidget();
void onDiffWidgetContextMenu(QWidget *diffWidget, const QPoint &pos);
void onProjectTreeContextMenu(const QPoint &pos);
// Ghost text
void setupGhostProvider();
@ -113,6 +115,7 @@ private:
// Git widget integration
QPointer<QTreeView> m_gitTreeView;
QPointer<QTreeView> m_projectTreeView;
};
#endif // OLLIE_KATE_PLUGIN_H