Commit 2fd9db73 authored by Waqar Ahmed's avatar Waqar Ahmed Committed by Christoph Cullmann
Browse files

urlbar: Show symbols from LSP for active view

parent 0fce7d47
Pipeline #139940 passed with stage
in 3 minutes and 25 seconds
......@@ -451,7 +451,7 @@ class LSPClientActionView : public QObject
QScopedPointer<LSPClientCompletion> m_completion;
QScopedPointer<LSPClientHover> m_hover;
QScopedPointer<KTextEditor::TextHintProvider> m_forwardHover;
QScopedPointer<QObject> m_symbolView;
QScopedPointer<LSPClientSymbolView> m_symbolView;
QPointer<QAction> m_findDef;
QPointer<QAction> m_findDecl;
......@@ -2917,6 +2917,11 @@ public:
m_hoverViews.remove(view);
}
}
QAbstractItemModel *documentSymbolsModel()
{
return m_symbolView->documentSymbolsModel();
}
};
class LSPClientPluginViewImpl : public QObject, public KXMLGUIClient, public KTextEditor::SessionConfigInterface
......@@ -2969,6 +2974,11 @@ public:
m_actionView->sessionDiagnosticSuppressions().writeSessionConfig(config);
}
Q_INVOKABLE QAbstractItemModel *documentSymbolsModel()
{
return m_actionView->documentSymbolsModel();
}
Q_SIGNALS:
/**
* Signal for outgoing message, the host application will handle them!
......
......@@ -17,6 +17,7 @@
#include <QHBoxLayout>
#include <QHeaderView>
#include <QIdentityProxyModel>
#include <QMenu>
#include <QPointer>
#include <QStandardItemModel>
......@@ -28,6 +29,9 @@
#include <kfts_fuzzy_match.h>
// TODO: Make this globally available in shared/
enum SymbolViewRoles { SymbolRange = Qt::UserRole, ScoreRole, IsPlaceholder };
class LSPClientViewTrackerImpl : public LSPClientViewTracker
{
Q_OBJECT
......@@ -133,8 +137,8 @@ protected:
return QSortFilterProxyModel::lessThan(sourceLeft, sourceRight);
}
const int l = sourceLeft.data(WeightRole).toInt();
const int r = sourceRight.data(WeightRole).toInt();
const int l = sourceLeft.data(SymbolViewRoles::ScoreRole).toInt();
const int r = sourceRight.data(SymbolViewRoles::ScoreRole).toInt();
return l < r;
}
......@@ -148,13 +152,24 @@ protected:
const auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
const QString symbol = idx.data().toString();
const bool res = kfts::fuzzy_match(m_pattern, symbol, score);
sourceModel()->setData(idx, score, WeightRole);
sourceModel()->setData(idx, score, SymbolViewRoles::ScoreRole);
return res;
}
private:
QString m_pattern;
static constexpr int WeightRole = Qt::UserRole + 1;
};
class SymbolViewProxyModel : public QIdentityProxyModel
{
Q_OBJECT
public:
using QIdentityProxyModel::QIdentityProxyModel;
int columnCount(const QModelIndex &) const override
{
return 1;
}
};
/*
......@@ -201,6 +216,8 @@ class LSPClientSymbolViewImpl : public QObject, public LSPClientSymbolView
// filter model, setup once
LSPClientSymbolViewFilterProxyModel m_filterModel;
SymbolViewProxyModel *m_identityModel;
// cached icons for model
const QIcon m_icon_pkg = QIcon::fromTheme(QStringLiteral("code-block"));
const QIcon m_icon_class = QIcon::fromTheme(QStringLiteral("code-class"));
......@@ -214,6 +231,7 @@ public:
, m_mainWindow(mainWin)
, m_serverManager(std::move(manager))
, m_outline(new QStandardItemModel())
, m_identityModel(new SymbolViewProxyModel(this))
{
m_toolview.reset(m_mainWindow->createToolView(plugin,
QStringLiteral("lspclient_symbol_outline"),
......@@ -249,6 +267,8 @@ public:
m_symbols->setModel(&m_filterModel);
delete m;
m_identityModel->setSourceModel(m_outline.get());
connect(m_symbols, &QTreeView::customContextMenuRequested, this, &self_type::showContextMenu);
connect(m_symbols, &QTreeView::activated, this, &self_type::goToSymbol);
connect(m_symbols, &QTreeView::clicked, this, &self_type::goToSymbol);
......@@ -375,7 +395,7 @@ public:
auto detail = show_detail && !symbol.detail.isEmpty() ? QStringLiteral(" [%1]").arg(symbol.detail) : QString();
node->setText(symbol.name + detail);
node->setIcon(*icon);
node->setData(QVariant::fromValue<KTextEditor::Range>(symbol.range), Qt::UserRole);
node->setData(QVariant::fromValue<KTextEditor::Range>(symbol.range), SymbolViewRoles::SymbolRange);
static const QChar prefix = QChar::fromLatin1('0');
line->setText(QStringLiteral("%1").arg(symbol.range.start().line(), 7, 10, prefix));
// recurse children
......@@ -407,7 +427,9 @@ public:
m_models[0].model = newModel;
}
} else {
newModel->appendRow(new QStandardItem(problem));
auto item = new QStandardItem(problem);
item->setData(true, SymbolViewRoles::IsPlaceholder);
newModel->appendRow(item);
}
// cache detail info with model
......@@ -459,6 +481,8 @@ public:
// current item tracking
updateCurrentTreeItem();
m_identityModel->setSourceModel(m_outline.get());
}
void refresh(bool clear, bool allow_cache = true, int retry = 0)
......@@ -559,7 +583,7 @@ public:
}
// does the line match our item?
return item->data(Qt::UserRole).value<KTextEditor::Range>().overlapsLine(line) ? item : nullptr;
return item->data(SymbolViewRoles::SymbolRange).value<KTextEditor::Range>().overlapsLine(line) ? item : nullptr;
}
void updateCurrentTreeItem()
......@@ -588,12 +612,17 @@ public:
void goToSymbol(const QModelIndex &index)
{
KTextEditor::View *kv = m_mainWindow->activeView();
const auto range = index.data(Qt::UserRole).value<KTextEditor::Range>();
const auto range = index.data(SymbolViewRoles::SymbolRange).value<KTextEditor::Range>();
if (kv && range.isValid()) {
kv->setCursorPosition(range.start());
}
}
QAbstractItemModel *documentSymbolsModel() override
{
return m_identityModel;
}
private Q_SLOTS:
/**
* React on filter change
......@@ -619,9 +648,11 @@ private Q_SLOTS:
}
};
QObject *LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager)
LSPClientSymbolView *LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager)
{
return new LSPClientSymbolViewImpl(plugin, mainWin, std::move(manager));
}
LSPClientSymbolView::~LSPClientSymbolView() = default;
#include "lspclientsymbolview.moc"
......@@ -17,7 +17,11 @@ class LSPClientSymbolView
{
public:
// only needs a factory; no other public interface
static QObject *new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager);
static LSPClientSymbolView *new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager);
virtual ~LSPClientSymbolView();
virtual class QAbstractItemModel *documentSymbolsModel() = 0;
};
class LSPClientViewTracker : public QObject
......
......@@ -33,6 +33,7 @@
#include <QStyledItemDelegate>
#include <QTimer>
#include <QToolButton>
#include <QTreeView>
#include <QUrl>
class DirFilesModel : public QAbstractListModel
......@@ -213,11 +214,125 @@ private:
DirFilesModel m_model;
};
enum BreadCrumbRole {
PathRole = Qt::UserRole + 1,
IsSeparator = Qt::UserRole + 2,
class SymbolsTreeView : public QMenu
{
Q_OBJECT
public:
// Copied from LSPClientSymbolView
enum Role {
SymbolRange = Qt::UserRole,
ScoreRole, //> Unused here
IsPlaceholder
};
SymbolsTreeView(QWidget *parent)
: QMenu(parent)
, m_tree(new QTreeView(this))
{
m_tree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_tree->setFrameStyle(QFrame::NoFrame);
m_tree->setUniformRowHeights(true);
m_tree->setHeaderHidden(true);
m_tree->setTextElideMode(Qt::ElideRight);
auto *l = new QVBoxLayout(this);
l->setContentsMargins({});
l->addWidget(m_tree);
setFocusProxy(m_tree);
m_tree->installEventFilter(this);
m_tree->viewport()->installEventFilter(this);
connect(m_tree, &QTreeView::clicked, this, &SymbolsTreeView::onClicked);
}
void setSymbolsModel(QAbstractItemModel *model, KTextEditor::View *v, const QString &text)
{
m_activeView = v;
m_tree->setModel(model);
m_tree->expandAll();
const auto idxToSelect = model->match(model->index(0, 0), 0, text, 1, Qt::MatchExactly);
if (!idxToSelect.isEmpty()) {
m_tree->setCurrentIndex(idxToSelect.constFirst());
}
updateGeometry();
}
bool eventFilter(QObject *o, QEvent *e) override
{
// Handling via event filter is necessary to bypass
// QTreeView's own key event handling
if (e->type() == QEvent::KeyPress) {
if (handleKeyPressEvent(static_cast<QKeyEvent *>(e))) {
return true;
}
}
return QMenu::eventFilter(o, e);
}
bool handleKeyPressEvent(QKeyEvent *ke)
{
if (ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return) {
onClicked(m_tree->currentIndex());
return true;
} else if (ke->key() == Qt::Key_Left) {
hide();
Q_EMIT navigateLeftRight(ke->key());
return false;
} else if (ke->key() == Qt::Key_Escape) {
hide();
ke->accept();
return true;
;
}
return false;
}
void updateGeometry()
{
const auto *model = m_tree->model();
const int rows = model->rowCount();
const int rowHeight = m_tree->sizeHintForRow(0);
const int maxHeight = rows * rowHeight;
setFixedSize(350, qMin(600, maxHeight));
}
void onClicked(const QModelIndex &idx)
{
const auto range = idx.data(SymbolRange).value<KTextEditor::Range>();
if (range.isValid()) {
m_activeView->setCursorPosition(range.start());
}
hide();
}
static QModelIndex symbolForCurrentLine(QAbstractItemModel *model, const QModelIndex &index, int line)
{
const int rowCount = model->rowCount(index);
for (int i = 0; i < rowCount; ++i) {
const auto idx = model->index(i, 0, index);
if (idx.data(SymbolRange).value<KTextEditor::Range>().overlapsLine(line)) {
return idx;
} else if (model->hasChildren(idx)) {
const auto childIdx = symbolForCurrentLine(model, idx, line);
if (childIdx.isValid()) {
return childIdx;
}
}
}
return {};
}
private:
QTreeView *m_tree;
QPointer<KTextEditor::View> m_activeView;
Q_SIGNALS:
void navigateLeftRight(int key);
};
enum BreadCrumbRole { PathRole = Qt::UserRole + 1, IsSeparator, IsSymbolCrumb };
class BreadCrumbDelegate : public QStyledItemDelegate
{
Q_OBJECT
......@@ -301,6 +416,8 @@ public:
const auto &dirs = res;
m_model.clear();
m_symbolsModel = nullptr;
int i = 0;
for (const auto &dir : dirs) {
auto item = new QStandardItem(dir.name);
......@@ -319,6 +436,118 @@ public:
}
i++;
}
auto *mainWindow = m_urlBar->viewManager()->mainWindow();
QPointer<QObject> lsp = mainWindow->pluginView(QStringLiteral("lspclientplugin"));
if (lsp) {
addSymbolCrumb(lsp);
}
}
void addSymbolCrumb(QObject *lsp)
{
QAbstractItemModel *model;
QMetaObject::invokeMethod(lsp, "documentSymbolsModel", Q_RETURN_ARG(QAbstractItemModel *, model));
m_symbolsModel = model;
if (!model) {
return;
}
connect(m_symbolsModel, &QAbstractItemModel::modelReset, this, &BreadCrumbView::updateSymbolsCrumb);
if (model->rowCount({}) == 0) {
return;
}
const auto view = m_urlBar->viewManager()->activeView();
disconnect(m_connToView);
if (view) {
m_connToView = connect(view, &KTextEditor::View::cursorPositionChanged, this, &BreadCrumbView::updateSymbolsCrumb);
}
const auto idx = getSymbolCrumbText();
if (!idx.isValid()) {
return;
}
// Add separator
auto sep = new QStandardItem(QIcon::fromTheme(QStringLiteral("arrow-right")), {});
sep->setSelectable(false);
sep->setData(true, BreadCrumbRole::IsSeparator);
m_model.appendRow(sep);
const auto icon = idx.data(Qt::DecorationRole).value<QIcon>();
const auto text = idx.data().toString();
auto *item = new QStandardItem(icon, text);
item->setData(true, BreadCrumbRole::IsSymbolCrumb);
m_model.appendRow(item);
}
void updateSymbolsCrumb()
{
QStandardItem *item = m_model.item(m_model.rowCount() - 1, 0);
if (!m_urlBar->viewSpace()->isActiveSpace()) {
if (item && item->data(BreadCrumbRole::IsSymbolCrumb).toBool()) {
// we are not active viewspace, remove the symbol + separator from breadcrumb
// This is important as LSP only gives us symbols for the current active view
// which atm is in some other viewspace.
// In future we might want to extend LSP to provide us models for documents
// but for now this will do.
qDeleteAll(m_model.takeRow(m_model.rowCount() - 1));
qDeleteAll(m_model.takeRow(m_model.rowCount() - 1));
}
return;
}
const auto idx = getSymbolCrumbText();
if (!idx.isValid()) {
return;
}
if (!item || !item->data(BreadCrumbRole::IsSymbolCrumb).toBool()) {
// Add separator
auto sep = new QStandardItem(QIcon::fromTheme(QStringLiteral("arrow-right")), {});
sep->setSelectable(false);
sep->setData(true, BreadCrumbRole::IsSeparator);
m_model.appendRow(sep);
item = new QStandardItem;
m_model.appendRow(item);
item->setData(true, BreadCrumbRole::IsSymbolCrumb);
}
const auto text = idx.data().toString();
const auto icon = idx.data(Qt::DecorationRole).value<QIcon>();
item->setText(text);
item->setIcon(icon);
}
QModelIndex getSymbolCrumbText()
{
if (!m_symbolsModel) {
return {};
}
QModelIndex first = m_symbolsModel->index(0, 0);
if (first.data(SymbolsTreeView::IsPlaceholder).toBool()) {
return {};
}
const auto view = m_urlBar->viewSpace()->currentView();
int line = view ? view->cursorPosition().line() : 0;
QModelIndex idx;
if (line > 0) {
idx = SymbolsTreeView::symbolForCurrentLine(m_symbolsModel, idx, line);
} else {
idx = first;
}
if (!idx.isValid()) {
idx = first;
}
return idx;
}
static bool IsSeparator(const QModelIndex &idx)
......@@ -342,6 +571,24 @@ public:
void onClicked(const QModelIndex &idx)
{
// Clicked on the symbol?
if (m_symbolsModel && idx.data(BreadCrumbRole::IsSymbolCrumb).toBool()) {
auto activeView = m_urlBar->viewSpace()->currentView();
if (!activeView) {
// View must be there
return;
}
SymbolsTreeView t(this);
connect(&t, &SymbolsTreeView::navigateLeftRight, this, [this](int k) {
onNavigateLeftRight(k, true);
});
const QString symbolName = idx.data().toString();
t.setSymbolsModel(m_symbolsModel, activeView, symbolName);
const auto pos = mapToGlobal(rectForIndex(idx).bottomLeft());
t.setFocus();
t.exec(pos);
}
auto path = idx.data(BreadCrumbRole::PathRole).toString();
if (path.isEmpty()) {
return;
......@@ -427,6 +674,8 @@ private:
KateUrlBar *const m_urlBar;
QStandardItemModel m_model;
QPointer<QAbstractItemModel> m_symbolsModel;
QMetaObject::Connection m_connToView; // Only one conn at a time
Q_SIGNALS:
void unsetFocus();
......@@ -554,6 +803,11 @@ KateViewManager *KateUrlBar::viewManager()
return m_parentViewSpace->viewManger();
}
KateViewSpace *KateUrlBar::viewSpace()
{
return m_parentViewSpace;
}
void KateUrlBar::setupLayout()
{
// Setup the stacked widget
......@@ -569,6 +823,13 @@ void KateUrlBar::setupLayout()
void KateUrlBar::onViewChanged(KTextEditor::View *v)
{
// We are not active but we have a doc? => don't do anything
// we check for a doc because we want to update the KateUrlBar
// when kate starts
if (!viewSpace()->isActiveSpace() && m_currentDoc) {
return;
}
if (!v) {
updateForDocument(nullptr);
m_untitledDocLabel->setText(i18n("Untitled"));
......@@ -607,6 +868,7 @@ void KateUrlBar::updateForDocument(KTextEditor::Document *doc)
if (vm && !vm->showUrlNavBar()) {
return;
}
m_urlBarView->setUrl(doc->url());
}
......
......@@ -18,6 +18,7 @@ public:
void open();
class KateViewManager *viewManager();
class KateViewSpace *viewSpace();
private:
void setupLayout();
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment