M5: project symbol switcher via ctags (Alt+R)
SymbolIndex (project_lib): parseTags reads Exuberant Ctags 5.9 extended tab format (name/file/line/kind/scope); listSymbols runs ctags -f - -L - --fields=+nK --excmd=number --sort=no with the file list on stdin, paths relative to root. 7 unit tests incl. a live ctags run. Plugin: Alt+R 'Go to Symbol (project)' opens a PaletteWidget of project symbols (label Scope::name, hint 'kind file'), opens the pick via MainWindow::openUrl and jumps to its line. Also in the M-x palette and reachable from a radial by objectName ollie_goto_symbol.
This commit is contained in:
parent
f4b891efa7
commit
42363157d3
15
docs/PLAN.md
15
docs/PLAN.md
|
|
@ -203,7 +203,20 @@ Therefore the palette is replaced, not extended, and the replacement is built on
|
|||
document's directory (fallback: cwd). Activation opens the file via
|
||||
`MainWindow::openUrl` + `activateView`. Also reachable from the M-x palette
|
||||
(`ollie:switch:file`) and, by objectName `ollie_goto_file`, from a radial.
|
||||
- Fuzzy symbol switcher scoped to the folder: TODO (ctags/LSP; ctags present).
|
||||
- Fuzzy symbol switcher scoped to the folder: DONE.
|
||||
`src/project/symbolindex.{h,cpp}` (in `project_lib`, 7 unit tests incl. a
|
||||
live ctags run):
|
||||
- System ctags is **Exuberant 5.9** (no JSON), so `parseTags` parses the
|
||||
classic extended tab format from `ctags -f - -L - --fields=+nK
|
||||
--excmd=number --sort=no` (file list fed on stdin so a big project never
|
||||
overflows ARG_MAX; paths relative to the root). Each `Symbol` carries
|
||||
name / file / line / kind / scope.
|
||||
- Wired into the plugin: **Alt+R** "Go to Symbol (project)" opens a third
|
||||
PaletteWidget of the project's symbols (label = `Scope::name`, hint =
|
||||
`kind file`). Activation opens the file via `MainWindow::openUrl` and
|
||||
jumps the cursor to the symbol's line (ctags 1-based → cursor 0-based).
|
||||
Also in the M-x palette (`ollie:switch:symbol`) and reachable from a radial
|
||||
by objectName `ollie_goto_symbol`.
|
||||
|
||||
## Build and test
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "radialconfig.h"
|
||||
#include "olliecommands.h"
|
||||
#include "projectindex.h"
|
||||
#include "symbolindex.h"
|
||||
|
||||
#include <KTextEditor/MainWindow>
|
||||
#include <KTextEditor/View>
|
||||
|
|
@ -136,6 +137,10 @@ const QLatin1String kCmdPrefix("ollie:cmd:");
|
|||
// the file switcher.
|
||||
const QLatin1String kFilePrefix("ollie:file:");
|
||||
|
||||
// Prefix marking a palette id that is a symbol location ("relpath\tline") to
|
||||
// jump to in the symbol switcher.
|
||||
const QLatin1String kSymbolPrefix("ollie:sym:");
|
||||
|
||||
// The complete (argument-free) command verbs offered in the palette. Verbs that
|
||||
// need free-text arguments (pipe) stay on the command line; `case` is expanded
|
||||
// into one entry per style so each is directly runnable.
|
||||
|
|
@ -236,6 +241,18 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
|||
w->addAction(gotoFileAction);
|
||||
}
|
||||
|
||||
// Project "Go to Symbol" switcher: Alt+R (single modifier). Lists ctags
|
||||
// symbols across the project's files, scoped to the same root as the file
|
||||
// switcher. Activation opens the file and jumps to the symbol's line.
|
||||
auto *gotoSymbolAction = new QAction(i18n("Go to Symbol (project)"), this);
|
||||
gotoSymbolAction->setObjectName(QStringLiteral("ollie_goto_symbol"));
|
||||
gotoSymbolAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_R));
|
||||
gotoSymbolAction->setShortcutContext(Qt::ApplicationShortcut);
|
||||
connect(gotoSymbolAction, &QAction::triggered, this, &OllieView::showSymbolSwitcher);
|
||||
if (QWidget *w = m_mainWindow->window()) {
|
||||
w->addAction(gotoSymbolAction);
|
||||
}
|
||||
|
||||
setupRadials();
|
||||
|
||||
// One application-wide filter serves both the mouse-button radial gesture
|
||||
|
|
@ -248,6 +265,7 @@ OllieView::~OllieView()
|
|||
{
|
||||
delete m_palette;
|
||||
delete m_filePalette;
|
||||
delete m_symbolPalette;
|
||||
delete m_radial;
|
||||
}
|
||||
|
||||
|
|
@ -411,6 +429,15 @@ void OllieView::showCommandPalette()
|
|||
items.push_back(item);
|
||||
}
|
||||
|
||||
// Offer the project symbol switcher as a palette entry too (also on Alt+R).
|
||||
{
|
||||
PaletteItem item;
|
||||
item.id = QStringLiteral("ollie:switch:symbol");
|
||||
item.label = i18n("Go to Symbol (project)");
|
||||
item.group = QStringLiteral("Alt+R");
|
||||
items.push_back(item);
|
||||
}
|
||||
|
||||
m_palette->setItems(items);
|
||||
|
||||
// Center the palette near the top of the active window.
|
||||
|
|
@ -444,6 +471,12 @@ void OllieView::runAction(const QString &actionId)
|
|||
return;
|
||||
}
|
||||
|
||||
// The project symbol switcher, surfaced as a palette entry.
|
||||
if (actionId == QLatin1String("ollie:switch:symbol")) {
|
||||
showSymbolSwitcher();
|
||||
return;
|
||||
}
|
||||
|
||||
QPointer<QAction> action = m_actionsById.value(actionId);
|
||||
if (action) {
|
||||
action->trigger();
|
||||
|
|
@ -506,6 +539,86 @@ void OllieView::openProjectFile(const QString &relativePath)
|
|||
}
|
||||
}
|
||||
|
||||
void OllieView::showSymbolSwitcher()
|
||||
{
|
||||
if (!m_symbolPalette) {
|
||||
m_symbolPalette = new PaletteWidget(m_mainWindow->window());
|
||||
connect(m_symbolPalette, &PaletteWidget::activatedId, this,
|
||||
[this](const QString &id) {
|
||||
if (id.startsWith(kSymbolPrefix)) {
|
||||
goToSymbol(id.mid(QString(kSymbolPrefix).size()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
m_symbolSwitcherRoot = projectRootFor(m_mainWindow);
|
||||
const QStringList files = ProjectIndex::listFiles(m_symbolSwitcherRoot);
|
||||
const QList<Symbol> symbols = SymbolIndex::listSymbols(m_symbolSwitcherRoot, files);
|
||||
|
||||
QList<PaletteItem> items;
|
||||
items.reserve(symbols.size());
|
||||
for (const Symbol &s : symbols) {
|
||||
PaletteItem item;
|
||||
// The id encodes the jump target as "relpath\tline"; the location is
|
||||
// resolved against m_symbolSwitcherRoot on activation.
|
||||
item.id = kSymbolPrefix + s.file + QLatin1Char('\t') + QString::number(s.line);
|
||||
|
||||
// Label: scope-qualified name so "Class::method" matches naturally.
|
||||
QString label = s.name;
|
||||
if (!s.scope.isEmpty()) {
|
||||
label = s.scope + QStringLiteral("::") + s.name;
|
||||
}
|
||||
item.label = label;
|
||||
|
||||
// Trailing hint: kind plus the file for provenance.
|
||||
QString hint = s.kind;
|
||||
if (!s.file.isEmpty()) {
|
||||
hint = hint.isEmpty() ? s.file : (s.kind + QStringLiteral(" ") + s.file);
|
||||
}
|
||||
item.group = hint;
|
||||
|
||||
items.push_back(item);
|
||||
}
|
||||
m_symbolPalette->setItems(items);
|
||||
|
||||
if (QWidget *w = m_mainWindow->window()) {
|
||||
const QSize sz = m_symbolPalette->size();
|
||||
const QPoint topLeft = w->mapToGlobal(QPoint((w->width() - sz.width()) / 2,
|
||||
w->height() / 6));
|
||||
m_symbolPalette->move(topLeft);
|
||||
}
|
||||
|
||||
m_symbolPalette->activate();
|
||||
}
|
||||
|
||||
void OllieView::goToSymbol(const QString &locationId)
|
||||
{
|
||||
if (m_symbolSwitcherRoot.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const int tab = locationId.indexOf(QLatin1Char('\t'));
|
||||
if (tab < 0) {
|
||||
return;
|
||||
}
|
||||
const QString relativePath = locationId.left(tab);
|
||||
const int line = locationId.mid(tab + 1).toInt();
|
||||
if (relativePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString absolute = QDir(m_symbolSwitcherRoot).absoluteFilePath(relativePath);
|
||||
const QUrl url = QUrl::fromLocalFile(absolute);
|
||||
KTextEditor::View *view = m_mainWindow->openUrl(url);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
m_mainWindow->activateView(view->document());
|
||||
if (line > 0) {
|
||||
// ctags lines are 1-based; KTextEditor cursors are 0-based.
|
||||
view->setCursorPosition(KTextEditor::Cursor(line - 1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
QAction *OllieView::findActionByName(const QString &nameOrText) const
|
||||
{
|
||||
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ private Q_SLOTS:
|
|||
void runAction(const QString &actionId);
|
||||
void showFileSwitcher();
|
||||
void openProjectFile(const QString &relativePath);
|
||||
void showSymbolSwitcher();
|
||||
void goToSymbol(const QString &locationId);
|
||||
|
||||
private:
|
||||
// One harvested, runnable action with the provenance needed to build a
|
||||
|
|
@ -109,6 +111,8 @@ private:
|
|||
PaletteWidget *m_palette = nullptr;
|
||||
PaletteWidget *m_filePalette = nullptr; // project "Go to File" switcher
|
||||
QString m_fileSwitcherRoot; // project root for the open switcher
|
||||
PaletteWidget *m_symbolPalette = nullptr; // project "Go to Symbol" switcher
|
||||
QString m_symbolSwitcherRoot; // project root for the symbol switcher
|
||||
RadialMenu *m_radial = nullptr;
|
||||
QList<RadialDef> m_radialDefs;
|
||||
QHash<int, int> m_buttonToRadial; // Qt::MouseButton (int) -> def index
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
add_library(project_lib STATIC
|
||||
projectindex.cpp
|
||||
projectindex.h
|
||||
symbolindex.cpp
|
||||
symbolindex.h
|
||||
)
|
||||
target_link_libraries(project_lib PUBLIC
|
||||
Qt6::Core
|
||||
|
|
@ -11,4 +13,8 @@ if(Qt6Test_FOUND)
|
|||
add_executable(test_projectindex test_projectindex.cpp)
|
||||
target_link_libraries(test_projectindex PRIVATE project_lib Qt6::Core Qt6::Test)
|
||||
add_test(NAME projectindex COMMAND test_projectindex)
|
||||
|
||||
add_executable(test_symbolindex test_symbolindex.cpp)
|
||||
target_link_libraries(test_symbolindex PRIVATE project_lib Qt6::Core Qt6::Test)
|
||||
add_test(NAME symbolindex COMMAND test_symbolindex)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include "symbolindex.h"
|
||||
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
bool SymbolIndex::ctagsAvailable()
|
||||
{
|
||||
return !QStandardPaths::findExecutable(QStringLiteral("ctags")).isEmpty();
|
||||
}
|
||||
|
||||
QList<Symbol> SymbolIndex::parseTags(const QString &ctagsOutput)
|
||||
{
|
||||
QList<Symbol> symbols;
|
||||
const QList<QStringView> lines =
|
||||
QStringView(ctagsOutput).split(QLatin1Char('\n'), Qt::SkipEmptyParts);
|
||||
|
||||
for (const QStringView &lineView : lines) {
|
||||
const QString line = lineView.toString();
|
||||
if (line.startsWith(QLatin1String("!_TAG_"))) {
|
||||
continue; // pseudo-tag header
|
||||
}
|
||||
const QStringList parts = line.split(QLatin1Char('\t'));
|
||||
if (parts.size() < 3) {
|
||||
continue; // malformed
|
||||
}
|
||||
|
||||
Symbol s;
|
||||
s.name = parts.at(0);
|
||||
s.file = parts.at(1);
|
||||
|
||||
// The excmd field (parts[2]) is "<line>;\"" under --excmd=number; parse
|
||||
// its leading integer as a fallback line number.
|
||||
const QString excmd = parts.at(2);
|
||||
int i = 0;
|
||||
while (i < excmd.size() && excmd.at(i).isDigit()) {
|
||||
++i;
|
||||
}
|
||||
if (i > 0) {
|
||||
s.line = QStringView(excmd).left(i).toInt();
|
||||
}
|
||||
|
||||
// Extension fields (parts[3..]): a bare token is the kind; "key:value"
|
||||
// tokens carry line and scope. The authoritative line comes from the
|
||||
// "line:" field when present.
|
||||
for (int p = 3; p < parts.size(); ++p) {
|
||||
const QString &field = parts.at(p);
|
||||
const int colon = field.indexOf(QLatin1Char(':'));
|
||||
if (colon < 0) {
|
||||
s.kind = field; // bare kind long-name
|
||||
continue;
|
||||
}
|
||||
const QString key = field.left(colon);
|
||||
const QString value = field.mid(colon + 1);
|
||||
if (key == QLatin1String("line")) {
|
||||
if (!value.isEmpty()) {
|
||||
s.line = value.toInt();
|
||||
}
|
||||
} else if (key == QLatin1String("kind")) {
|
||||
s.kind = value;
|
||||
} else if (key == QLatin1String("file")) {
|
||||
// file-scope marker; no useful scope value
|
||||
} else if (s.scope.isEmpty() && !value.isEmpty()) {
|
||||
// class:/struct:/namespace:/function:/enum:… — the first such
|
||||
// field names the enclosing scope.
|
||||
s.scope = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.name.isEmpty()) {
|
||||
symbols.push_back(s);
|
||||
}
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
|
||||
QList<Symbol> SymbolIndex::listSymbols(const QString &root, const QStringList &files)
|
||||
{
|
||||
if (root.isEmpty() || files.isEmpty() || !ctagsAvailable()) {
|
||||
return QList<Symbol>();
|
||||
}
|
||||
|
||||
QProcess ctags;
|
||||
ctags.setWorkingDirectory(root);
|
||||
// Classic extended format with kind + line fields, numeric addresses, and
|
||||
// the file list fed on stdin (-L -) so a huge project never overflows the
|
||||
// command line. Paths are relative to the working directory, so the parsed
|
||||
// Symbol::file is already project-relative.
|
||||
ctags.start(QStringLiteral("ctags"),
|
||||
{QStringLiteral("-f"), QStringLiteral("-"),
|
||||
QStringLiteral("-L"), QStringLiteral("-"),
|
||||
QStringLiteral("--fields=+nK"),
|
||||
QStringLiteral("--excmd=number"),
|
||||
QStringLiteral("--sort=no")});
|
||||
if (!ctags.waitForStarted(3000)) {
|
||||
return QList<Symbol>();
|
||||
}
|
||||
ctags.write(files.join(QLatin1Char('\n')).toUtf8());
|
||||
ctags.write("\n");
|
||||
ctags.closeWriteChannel();
|
||||
if (!ctags.waitForFinished(20000)) {
|
||||
ctags.kill();
|
||||
ctags.waitForFinished(1000);
|
||||
return QList<Symbol>();
|
||||
}
|
||||
|
||||
const QString out = QString::fromUtf8(ctags.readAllStandardOutput());
|
||||
return parseTags(out);
|
||||
}
|
||||
|
||||
} // namespace katecustom
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*
|
||||
* SymbolIndex — project-scoped symbol enumeration via ctags.
|
||||
*
|
||||
* Given a project root and a list of files (from ProjectIndex::listFiles), run
|
||||
* ctags over them and parse the classic tab-separated "extended" format into a
|
||||
* flat symbol list suitable for the fuzzy switcher. Exuberant Ctags 5.9 has no
|
||||
* JSON output, so we parse the extended format:
|
||||
*
|
||||
* name <TAB> file <TAB> <line>;" <TAB> <kind> [<TAB> key:value ...]
|
||||
*
|
||||
* The parsing (parseTags) is pure and unit-tested; listSymbols runs the process.
|
||||
*/
|
||||
#ifndef KATECUSTOM_SYMBOLINDEX_H
|
||||
#define KATECUSTOM_SYMBOLINDEX_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
/*! One symbol located in the project. */
|
||||
struct Symbol {
|
||||
QString name; // identifier, e.g. "runVerb"
|
||||
QString file; // path relative to the project root
|
||||
int line = 0; // 1-based line number (0 if unknown)
|
||||
QString kind; // ctags kind long name, e.g. "function", "class"
|
||||
QString scope; // enclosing scope value, e.g. "OllieCommands" (may be empty)
|
||||
|
||||
bool operator==(const Symbol &o) const
|
||||
{
|
||||
return name == o.name && file == o.file && line == o.line
|
||||
&& kind == o.kind && scope == o.scope;
|
||||
}
|
||||
};
|
||||
|
||||
class SymbolIndex
|
||||
{
|
||||
public:
|
||||
/*! True if a ctags binary is usable. */
|
||||
static bool ctagsAvailable();
|
||||
|
||||
/*!
|
||||
* Parse ctags extended tab-separated output into a symbol list. Pure: no
|
||||
* process, no filesystem. Lines that are comments (starting with "!_TAG_")
|
||||
* or malformed (fewer than three tab fields) are skipped.
|
||||
*/
|
||||
static QList<Symbol> parseTags(const QString &ctagsOutput);
|
||||
|
||||
/*!
|
||||
* Run ctags over \a files (relative to \a root) and return the parsed
|
||||
* symbols. Returns empty when ctags is unavailable, \a root is empty, or
|
||||
* \a files is empty.
|
||||
*/
|
||||
static QList<Symbol> listSymbols(const QString &root, const QStringList &files);
|
||||
};
|
||||
|
||||
} // namespace katecustom
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*
|
||||
* Unit tests for SymbolIndex. parseTags is exercised directly against sample
|
||||
* ctags extended output; listSymbols is exercised end-to-end only when a ctags
|
||||
* binary is available (skipped otherwise).
|
||||
*/
|
||||
#include "symbolindex.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
|
||||
using namespace katecustom;
|
||||
|
||||
class TestSymbolIndex : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void parsesNameFileLineKind()
|
||||
{
|
||||
const QString out =
|
||||
QStringLiteral("foo\tsrc/a.cpp\t1;\"\tfunction\tline:1\n")
|
||||
+ QStringLiteral("Bar\tsrc/a.cpp\t2;\"\tclass\tline:2\tfile:\n");
|
||||
const QList<Symbol> syms = SymbolIndex::parseTags(out);
|
||||
QCOMPARE(syms.size(), 2);
|
||||
|
||||
QCOMPARE(syms.at(0).name, QStringLiteral("foo"));
|
||||
QCOMPARE(syms.at(0).file, QStringLiteral("src/a.cpp"));
|
||||
QCOMPARE(syms.at(0).line, 1);
|
||||
QCOMPARE(syms.at(0).kind, QStringLiteral("function"));
|
||||
QVERIFY(syms.at(0).scope.isEmpty());
|
||||
|
||||
QCOMPARE(syms.at(1).name, QStringLiteral("Bar"));
|
||||
QCOMPARE(syms.at(1).kind, QStringLiteral("class"));
|
||||
QCOMPARE(syms.at(1).line, 2);
|
||||
}
|
||||
|
||||
void parsesScopeField()
|
||||
{
|
||||
const QString out =
|
||||
QStringLiteral("m\tsrc/a.cpp\t3;\"\tfunction\tline:3\tclass:Bar\n");
|
||||
const QList<Symbol> syms = SymbolIndex::parseTags(out);
|
||||
QCOMPARE(syms.size(), 1);
|
||||
QCOMPARE(syms.at(0).name, QStringLiteral("m"));
|
||||
QCOMPARE(syms.at(0).scope, QStringLiteral("Bar"));
|
||||
QCOMPARE(syms.at(0).line, 3);
|
||||
QCOMPARE(syms.at(0).kind, QStringLiteral("function"));
|
||||
}
|
||||
|
||||
void lineFallsBackToExcmd()
|
||||
{
|
||||
// No line: field — the numeric excmd supplies the line.
|
||||
const QString out = QStringLiteral("x\tf.c\t42;\"\tvariable\n");
|
||||
const QList<Symbol> syms = SymbolIndex::parseTags(out);
|
||||
QCOMPARE(syms.size(), 1);
|
||||
QCOMPARE(syms.at(0).line, 42);
|
||||
QCOMPARE(syms.at(0).kind, QStringLiteral("variable"));
|
||||
}
|
||||
|
||||
void skipsPseudoTagsAndMalformed()
|
||||
{
|
||||
const QString out =
|
||||
QStringLiteral("!_TAG_PROGRAM_NAME\tCtags\t//\n")
|
||||
+ QStringLiteral("brokenlinewithnotabs\n")
|
||||
+ QStringLiteral("good\tf.c\t1;\"\tfunction\n");
|
||||
const QList<Symbol> syms = SymbolIndex::parseTags(out);
|
||||
QCOMPARE(syms.size(), 1);
|
||||
QCOMPARE(syms.at(0).name, QStringLiteral("good"));
|
||||
}
|
||||
|
||||
void listSymbolsEndToEnd()
|
||||
{
|
||||
if (!SymbolIndex::ctagsAvailable()) {
|
||||
QSKIP("ctags not available");
|
||||
}
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
QDir base(tmp.path());
|
||||
const QString rel = QStringLiteral("a.cpp");
|
||||
QFile f(base.filePath(rel));
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
f.write("int alpha(int x){return x;}\n"
|
||||
"class Beta { public: void gamma(); };\n");
|
||||
f.close();
|
||||
|
||||
const QList<Symbol> syms = SymbolIndex::listSymbols(tmp.path(), {rel});
|
||||
QVERIFY(!syms.isEmpty());
|
||||
|
||||
bool haveAlpha = false, haveBeta = false;
|
||||
for (const Symbol &s : syms) {
|
||||
QCOMPARE(s.file, rel); // relative to root
|
||||
if (s.name == QLatin1String("alpha") && s.kind == QLatin1String("function")) {
|
||||
haveAlpha = true;
|
||||
QCOMPARE(s.line, 1);
|
||||
}
|
||||
if (s.name == QLatin1String("Beta") && s.kind == QLatin1String("class")) {
|
||||
haveBeta = true;
|
||||
}
|
||||
}
|
||||
QVERIFY(haveAlpha);
|
||||
QVERIFY(haveBeta);
|
||||
}
|
||||
|
||||
void listSymbolsEmptyInputs()
|
||||
{
|
||||
QVERIFY(SymbolIndex::listSymbols(QString(), {QStringLiteral("a")}).isEmpty());
|
||||
QVERIFY(SymbolIndex::listSymbols(QStringLiteral("/tmp"), {}).isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSymbolIndex)
|
||||
#include "test_symbolindex.moc"
|
||||
Loading…
Reference in New Issue