Commit 42d5ca13 authored by Waqar Ahmed's avatar Waqar Ahmed Committed by Christoph Cullmann
Browse files

Add Quick go-to symbol support in CTags plugin

parent 8903a672
......@@ -22,6 +22,10 @@ target_sources(
ctagskinds.cpp
kate_ctags_view.cpp
kate_ctags_plugin.cpp
gotosymbolmodel.cpp
gotoglobalsymbolmodel.cpp
gotosymboltreeview.cpp
gotosymbolwidget.cpp
plugin.qrc
)
......
......@@ -107,10 +107,8 @@ static CTagsExtensionMapping extensionMapping[] = {
{"sh", kindMappingSh}, {"SH", kindMappingSh}, {"bsh", kindMappingSh}, {"bash", kindMappingSh}, {"ksh", kindMappingSh}, {"zsh", kindMappingSh}, {"sl", kindMappingSlang}, {"tcl", kindMappingTcl},
{"wish", kindMappingTcl}, {"vim", kindMappingVim}, {nullptr, nullptr}};
static const CTagsKindMapping *findKindMapping(const QString &extension)
static const CTagsKindMapping *findKindMapping(const char* pextension)
{
const char *pextension = extension.toLocal8Bit().constData();
CTagsExtensionMapping *pem = extensionMapping;
while (pem->extension != nullptr) {
if (strcmp(pem->extension, pextension) == 0)
......@@ -123,10 +121,10 @@ static const CTagsKindMapping *findKindMapping(const QString &extension)
QString CTagsKinds::findKind(const char *kindChar, const QString &extension)
{
if (kindChar == nullptr)
if (kindChar == nullptr || extension.isEmpty())
return QString();
const CTagsKindMapping *kindMapping = findKindMapping(extension);
const CTagsKindMapping *kindMapping = findKindMapping(extension.toLocal8Bit().constData());
if (kindMapping) {
const CTagsKindMapping *pkm = kindMapping;
while (pkm->verbose != nullptr) {
......@@ -138,3 +136,21 @@ QString CTagsKinds::findKind(const char *kindChar, const QString &extension)
return QString();
}
QString CTagsKinds::findKindNoi18n(const char *kindChar, const QStringRef &extension)
{
if (kindChar == nullptr || extension.isEmpty())
return QString();
const CTagsKindMapping *kindMapping = findKindMapping(extension.toLocal8Bit().constData());
if (kindMapping) {
const CTagsKindMapping *pkm = kindMapping;
while (pkm->verbose != nullptr) {
if (pkm->abbrev == *kindChar)
return QString::fromLocal8Bit(pkm->verbose);
++pkm;
}
}
return QString();
}
......@@ -15,6 +15,7 @@ class CTagsKinds
{
public:
static QString findKind(const char *kindChar, const QString &extension);
static QString findKindNoi18n(const char *kindChar, const QStringRef &extension);
};
#endif
/*
SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "gotoglobalsymbolmodel.h"
#include <QFileInfo>
#include <QIcon>
GotoGlobalSymbolModel::GotoGlobalSymbolModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
int GotoGlobalSymbolModel::columnCount(const QModelIndex&) const
{
return 1;
}
int GotoGlobalSymbolModel::rowCount(const QModelIndex&) const
{
return m_rows.size();
}
QString GotoGlobalSymbolModel::filterName(QString tagName) const
{
// remove anon namespace
int __anonIdx = tagName.indexOf(QStringLiteral("__anon"));
if (__anonIdx != -1) {
int scopeOpIdx = tagName.indexOf(QStringLiteral("::"), __anonIdx) + 2;
tagName.remove(__anonIdx, scopeOpIdx - __anonIdx);
}
return tagName;
}
QVariant GotoGlobalSymbolModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return QVariant();
}
static const QIcon defIcon = QIcon::fromTheme(QStringLiteral("code-block"));
static const QIcon funcIcon = QIcon::fromTheme(QStringLiteral("code-function"));
static const QIcon varIcon = QIcon::fromTheme(QStringLiteral("code-variable"));
const Tags::TagEntry &row = m_rows.at(index.row());
if (role == Qt::DisplayRole) {
if (index.column() == 0)
return QString(filterName(row.tag) + QStringLiteral("&nbsp;<sub style=\"font-size: 16px;color: gray;\">") + QFileInfo(row.file).fileName() + QStringLiteral("</sub>"));
} else if (role == Qt::UserRole) {
return row.tag;
} else if (role == Qt::DecorationRole) {
if (row.type == QLatin1String("function") || row.type == QLatin1String("member")) {
return funcIcon;
} else if (row.type.startsWith(QLatin1String("var"))) {
return varIcon;
} else {
return defIcon;
}
} else if (role == Pattern) {
return row.pattern;
} else if (role == FileUrl) {
return row.file;
}
return QVariant();
}
/*
SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef GOTOGLOBALSYMBOLMODEL_H
#define GOTOGLOBALSYMBOLMODEL_H
#include "tags.h"
#include <QAbstractTableModel>
#include <QPair>
class GotoGlobalSymbolModel : public QAbstractTableModel
{
Q_OBJECT
public:
struct GSymbolItem
{
QString name;
QString fileName;
QString fileUrl;
QString pattern;
QString type;
};
enum Roles {
Name = Qt::UserRole,
Pattern,
FileUrl
};
explicit GotoGlobalSymbolModel(QObject* parent = nullptr);
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
/**
* @brief removes useless symbols like anon namespace etc for better UI
*/
QString filterName(QString tagName) const;
void setSymbolsData(Tags::TagList rows)
{
beginResetModel();
m_rows = std::move(rows);
endResetModel();
}
private:
Tags::TagList m_rows;
};
#endif // GOTOGLOBALSYMBOLMODEL_H
/*
SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "gotosymbolmodel.h"
#include <QProcess>
#include <QDebug>
#include <KLocalizedString>
GotoSymbolModel::GotoSymbolModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
int GotoSymbolModel::columnCount(const QModelIndex&) const
{
return 1;
}
int GotoSymbolModel::rowCount(const QModelIndex&) const
{
return m_rows.count();
}
QVariant GotoSymbolModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
const auto &row = m_rows.at(index.row());
if (role == Qt::DisplayRole) {
if (index.column() == 0)
return row.name;
} else if (role == Qt::DecorationRole) {
if (index.column() == 0)
return row.icon;
} else if (role == Qt::UserRole) {
return row.line;
}
return QVariant();
}
void GotoSymbolModel::refresh(const QString& filePath)
{
static const QIcon nsIcon = QIcon::fromTheme(QStringLiteral("code-block"));
static const QIcon classIcon = QIcon::fromTheme(QStringLiteral("code-class"));
static const QIcon funcIcon = QIcon::fromTheme(QStringLiteral("code-function"));
static const QIcon varIcon = QIcon::fromTheme(QStringLiteral("code-variable"));
static const QIcon defIcon = nsIcon;
beginResetModel();
m_rows.clear();
endResetModel();
QProcess p;
p.start(QStringLiteral("ctags"),
{
QStringLiteral("-x"),
QStringLiteral("--_xformat=%{name}%{signature}\t%{kind}\t%{line}"),
filePath
});
QByteArray out;
if (p.waitForFinished()) {
out = p.readAllStandardOutput();
} else {
qWarning() << "Ctags failed";
beginResetModel();
m_rows.append(SymbolItem{i18n("CTags executable not found."), -1, QIcon()});
endResetModel();
return;
}
QVector<SymbolItem> symItems;
const auto tags = out.split('\n');
symItems.reserve(tags.size());
for (const auto& tag : tags) {
const auto items = tag.split('\t');
if (items.isEmpty() || items.count() < 3){
continue;
}
SymbolItem item;
item.name = QLatin1String(items.at(0));
// this happens in markdown names for some reason
if (item.name.endsWith(QLatin1Char('-'))) {
item.name.chop(1);
}
switch (items.at(1).at(0)) {
case 'f':
item.icon = funcIcon;
break;
case 'm':
if (items.at(1) == "method")
item.icon = funcIcon;
else
item.icon = defIcon;
break;
case 'g':
if (items.at(1) == "getter")
item.icon = funcIcon;
else
item.icon = defIcon;
break;
case 'c':
case 's':
if (items.at(1) == "class" || items.at(1) == "struct")
item.icon = classIcon;
else
item.icon = defIcon;
break;
case 'n':
if (items.at(1) == "namespace")
item.icon = nsIcon;
break;
case 'v':
item.icon = varIcon;
break;
default:
item.icon = defIcon;
break;
}
item.line = items.at(2).toInt();
symItems.append(item);
}
beginResetModel();
if (!symItems.isEmpty()) {
m_rows = std::move(symItems);
} else {
m_rows.append(SymbolItem{i18n("CTags was unable to parse this file."), -1, QIcon()});
}
endResetModel();
}
/*
SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef GOTOSYMBOLMODEL_H
#define GOTOSYMBOLMODEL_H
#include <QString>
#include <QVector>
#include <QAbstractTableModel>
#include <QIcon>
struct SymbolItem
{
QString name;
int line;
QIcon icon;
};
class GotoSymbolModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit GotoSymbolModel(QObject* parent = nullptr);
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
void refresh(const QString &filePath);
private:
QVector<SymbolItem> m_rows;
};
#endif // GOTOSYMBOLMODEL_H
/*
SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "gotosymboltreeview.h"
#include <KTextEditor/Cursor>
#include <KTextEditor/MainWindow>
#include <KTextEditor/View>
#include <QHeaderView>
GotoSymbolTreeView::GotoSymbolTreeView(KTextEditor::MainWindow *mainWindow, QWidget *parent)
: QTreeView(parent),
m_mainWindow(mainWindow)
{
setSelectionBehavior(QAbstractItemView::SelectRows);
setSelectionMode(QAbstractItemView::SingleSelection);
setTextElideMode(Qt::ElideRight);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setHeaderHidden(true);
setRootIsDecorated(false);
}
int GotoSymbolTreeView::sizeHintWidth() const
{
return sizeHintForColumn(0);
}
void GotoSymbolTreeView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
{
if (globalMode)
return QTreeView::currentChanged(current, previous);
int line = current.data(Qt::UserRole).toInt();
KTextEditor::Cursor c(--line, 0);
if (c.isValid()) {
auto view = m_mainWindow->activeView();
if (view) {
view->setCursorPosition(c);
}
}
return QTreeView::currentChanged(current, previous);
}
#ifndef GOTOSYMBOLTREEVIEW_H
#define GOTOSYMBOLTREEVIEW_H
/*
SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QTreeView>
namespace KTextEditor {
class MainWindow;
}
class GotoSymbolTreeView : public QTreeView
{
Q_OBJECT
public:
GotoSymbolTreeView(KTextEditor::MainWindow* mainWindow, QWidget* parent = nullptr);
int sizeHintWidth() const;
void setGlobalMode(bool value)
{
globalMode = value;
}
protected:
void currentChanged(const QModelIndex &current, const QModelIndex &previous) override;
private:
KTextEditor::MainWindow* m_mainWindow;
bool globalMode = false;
};
#endif // GOTOSYMBOLTREEVIEW_H
/*
SPDX-FileCopyrightText: 2014-2019 Dominik Haumann <dhaumann@kde.org>
SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "gotosymbolwidget.h"
#include "gotosymboltreeview.h"
#include "gotosymbolmodel.h"
#include "gotoglobalsymbolmodel.h"
#include "tags.h"
#include "kate_ctags_view.h"
#include <QLineEdit>
#include <QVBoxLayout>
#include <QKeyEvent>
#include <QScrollBar>
#include <QSortFilterProxyModel>
#include <QCoreApplication>
#include <QStyledItemDelegate>
#include <QTextDocument>
#include <QPainter>
#include <QPropertyAnimation>
#include <KTextEditor/MainWindow>
#include <KTextEditor/View>
class QuickOpenFilterProxyModel : public QSortFilterProxyModel {
public:
QuickOpenFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent)
{}
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
const QString fileName = sourceModel()->index(sourceRow, 0, sourceParent).data().toString();
for (const QString& str : m_filterStrings) {
if (!fileName.contains(str, Qt::CaseInsensitive)) {
return false;
}
}
return true;
}
QStringList filterStrings() const
{
return m_filterStrings;
}
public Q_SLOTS:
void setFilterText(const QString& text)
{
m_filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
invalidateFilter();
}
private:
QStringList m_filterStrings;
};
class GotoStyleDelegate : public QStyledItemDelegate {
public:
GotoStyleDelegate(QObject* parent = nullptr)
: QStyledItemDelegate(parent)
{}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
QTextDocument doc;
QString str = index.data().toString();
for (const auto& string : m_filterStrings) {
const QRegularExpression re (QStringLiteral("(")+QRegularExpression::escape(string)+QStringLiteral(")"), QRegularExpression::CaseInsensitiveOption);
str.replace(re, QStringLiteral("<b>\\1</b>"));
}
doc.setHtml(str);
doc.setDocumentMargin(2);
painter->save();
// paint background
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
} else {
painter->fillRect(option.rect, option.palette.base());
}
options.text = QString(); // clear old text
options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
// draw text
painter->translate(option.rect.x(), option.rect.y());
if (index.column() == 0) {
painter->translate(25, 0);
}
doc.drawContents(painter);
painter->restore();
}
public Q_SLOTS:
void setFilterStrings(const QString& text)
{
m_filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
}
private:
QStringList m_filterStrings;
};
GotoSymbolWidget::GotoSymbolWidget(KTextEditor::MainWindow* mainWindow, KateCTagsView *pluginView, QWidget *widget)
: QWidget(widget),
ctagsPluginView(pluginView),
m_mainWindow(mainWindow),
oldPos(-1, -1)
{
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
mode = Local;
m_treeView = new GotoSymbolTreeView(mainWindow, this);
m_styleDelegate = new GotoStyleDelegate(this);
m_treeView->setItemDelegate(m_styleDelegate);
m_lineEdit = new QLineEdit(this);
setFocusProxy(m_lineEdit);
m_proxyModel = new QuickOpenFilterProxyModel(this);
m_proxyModel->setSortRole(Qt::DisplayRole);
m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setFilterRole(Qt::DisplayRole);
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setFilterKeyColumn(0);
m_symbolsModel = new GotoSymbolModel(this);
m_globalSymbolsModel = new GotoGlobalSymbolModel(this);
m_proxyModel->setSourceModel(m_symbolsModel);
m_treeView->setModel(m_proxyModel);
connect(m_lineEdit, &QLineEdit::textChanged, m_proxyModel, &QuickOpenFilterProxyModel::setFilterText);
connect(m_lineEdit, &QLineEdit::textChanged, m_styleDelegate, &GotoStyleDelegate::setFilterStrings);
connect(m_lineEdit, &QLineEdit::textChanged, this, [this](){ m_treeView->viewport()->update(); });