lspclientsymbolview.cpp 21.2 KB
Newer Older
1
/*
2
3
    SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
    SPDX-FileCopyrightText: 2019 Christoph Cullmann <cullmann@kde.org>
4

5
    SPDX-License-Identifier: MIT
6
*/
7
8
9

#include "lspclientsymbolview.h"

10
#include <KLineEdit>
11
#include <KLocalizedString>
12
#include <QSortFilterProxyModel>
13
14
15
16
17
18
19
20

#include <KTextEditor/Document>
#include <KTextEditor/MainWindow>
#include <KTextEditor/View>

#include <QHBoxLayout>
#include <QMenu>
#include <QPointer>
21
#include <QStandardItemModel>
22
23
#include <QTimer>
#include <QTreeView>
24
25

#include <memory>
Filip Gawin's avatar
Filip Gawin committed
26
#include <utility>
27

28
29
#include <kfts_fuzzy_match.h>

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class LSPClientViewTrackerImpl : public LSPClientViewTracker
{
    Q_OBJECT

    typedef LSPClientViewTrackerImpl self_type;

    LSPClientPlugin *m_plugin;
    KTextEditor::MainWindow *m_mainWindow;
    // timers to delay some todo's
    QTimer m_changeTimer;
    int m_change;
    QTimer m_motionTimer;
    int m_motion;
    int m_oldCursorLine = -1;

public:
46
47
48
49
50
    LSPClientViewTrackerImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, int change_ms, int motion_ms)
        : m_plugin(plugin)
        , m_mainWindow(mainWin)
        , m_change(change_ms)
        , m_motion(motion_ms)
51
    {
Waqar Ahmed's avatar
Waqar Ahmed committed
52
        Q_UNUSED(m_plugin);
53
54
        // get updated
        m_changeTimer.setSingleShot(true);
Alexander Lohnau's avatar
Alexander Lohnau committed
55
        auto ch = [this]() {
Christoph Cullmann's avatar
Christoph Cullmann committed
56
            Q_EMIT newState(m_mainWindow->activeView(), TextChanged);
Alexander Lohnau's avatar
Alexander Lohnau committed
57
        };
58
59
60
        connect(&m_changeTimer, &QTimer::timeout, this, ch);

        m_motionTimer.setSingleShot(true);
Alexander Lohnau's avatar
Alexander Lohnau committed
61
        auto mh = [this]() {
Christoph Cullmann's avatar
Christoph Cullmann committed
62
            Q_EMIT newState(m_mainWindow->activeView(), LineChanged);
Alexander Lohnau's avatar
Alexander Lohnau committed
63
        };
64
65
66
67
68
69
70
71
72
73
74
75
76
        connect(&m_motionTimer, &QTimer::timeout, this, mh);

        // track views
        connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::viewChanged);
    }

    void viewChanged(KTextEditor::View *view)
    {
        m_motionTimer.stop();
        m_changeTimer.stop();

        if (view) {
            if (m_motion) {
77
                connect(view, &KTextEditor::View::cursorPositionChanged, this, &self_type::cursorPositionChanged, Qt::UniqueConnection);
78
79
            }
            if (m_change > 0 && view->document()) {
80
                connect(view->document(), &KTextEditor::Document::textChanged, this, &self_type::textChanged, Qt::UniqueConnection);
81
            }
Christoph Cullmann's avatar
Christoph Cullmann committed
82
            Q_EMIT newState(view, ViewChanged);
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
            m_oldCursorLine = view->cursorPosition().line();
        }
    }

    void textChanged()
    {
        m_motionTimer.stop();
        m_changeTimer.start(m_change);
    }

    void cursorPositionChanged(KTextEditor::View *view, const KTextEditor::Cursor &newPosition)
    {
        if (m_changeTimer.isActive()) {
            // change trumps motion
            return;
        }

        if (view && newPosition.line() != m_oldCursorLine) {
            m_oldCursorLine = newPosition.line();
            m_motionTimer.start(m_motion);
        }
    }
};

107
LSPClientViewTracker *LSPClientViewTracker::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, int change_ms, int motion_ms)
108
109
110
111
{
    return new LSPClientViewTrackerImpl(plugin, mainWin, change_ms, motion_ms);
}

112
113
114
115
116
117
118
119
120
121
class LSPClientSymbolViewFilterProxyModel : public QSortFilterProxyModel
{
public:
    LSPClientSymbolViewFilterProxyModel(QObject *parent = nullptr)
        : QSortFilterProxyModel(parent)
    {
    }

    void setFilterString(const QString &string)
    {
122
        beginResetModel();
123
        m_pattern = string;
124
        endResetModel();
125
126
127
128
129
    }

protected:
    bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
    {
130
131
132
133
134
        // make sure to honour configured sort-order (if no scoring applies)
        if (m_pattern.isEmpty()) {
            return QSortFilterProxyModel::lessThan(sourceLeft, sourceRight);
        }

135
136
137
138
139
140
141
        const int l = sourceLeft.data(WeightRole).toInt();
        const int r = sourceRight.data(WeightRole).toInt();
        return l < r;
    }

    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
    {
142
        if (m_pattern.isEmpty()) {
143
            return true;
144
        }
145
146
147
148
149
150
151
152
153
154
155
156
157
158

        int score = 0;
        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);
        return res;
    }

private:
    QString m_pattern;
    static constexpr int WeightRole = Qt::UserRole + 1;
};

159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*
 * Instantiates and manages the symbol outline toolview.
 */
class LSPClientSymbolViewImpl : public QObject, public LSPClientSymbolView
{
    Q_OBJECT

    typedef LSPClientSymbolViewImpl self_type;

    LSPClientPlugin *m_plugin;
    KTextEditor::MainWindow *m_mainWindow;
    QSharedPointer<LSPClientServerManager> m_serverManager;
    QScopedPointer<QWidget> m_toolview;
    // parent ownership
173
174
    QPointer<QTreeView> m_symbols;
    QPointer<KLineEdit> m_filter;
175
176
177
178
179
180
181
182
    QScopedPointer<QMenu> m_popup;
    // initialized/updated from plugin settings
    // managed by context menu later on
    // parent ownership
    QAction *m_detailsOn;
    QAction *m_expandOn;
    QAction *m_treeOn;
    QAction *m_sortOn;
183
184
    // view tracking
    QScopedPointer<LSPClientViewTracker> m_viewTracker;
185
186
    // outstanding request
    LSPClientServer::RequestHandle m_handle;
187
    // cached outline models
188
    struct ModelData {
189
        QPointer<KTextEditor::Document> document;
190
191
192
193
194
195
        qint64 revision;
        std::shared_ptr<QStandardItemModel> model;
    };
    QList<ModelData> m_models;
    // max number to cache
    static constexpr int MAX_MODELS = 10;
196
    // last outline model we constructed
197
    std::shared_ptr<QStandardItemModel> m_outline;
198
    // filter model, setup once
199
    LSPClientSymbolViewFilterProxyModel m_filterModel;
200
201
202
203
204
205
206

    // cached icons for model
    const QIcon m_icon_pkg = QIcon::fromTheme(QStringLiteral("code-block"));
    const QIcon m_icon_class = QIcon::fromTheme(QStringLiteral("code-class"));
    const QIcon m_icon_typedef = QIcon::fromTheme(QStringLiteral("code-typedef"));
    const QIcon m_icon_function = QIcon::fromTheme(QStringLiteral("code-function"));
    const QIcon m_icon_var = QIcon::fromTheme(QStringLiteral("code-variable"));
207
208

public:
209
210
211
212
213
    LSPClientSymbolViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager)
        : m_plugin(plugin)
        , m_mainWindow(mainWin)
        , m_serverManager(std::move(manager))
        , m_outline(new QStandardItemModel())
214
    {
Alexander Lohnau's avatar
Alexander Lohnau committed
215
216
217
218
219
        m_toolview.reset(m_mainWindow->createToolView(plugin,
                                                      QStringLiteral("lspclient_symbol_outline"),
                                                      KTextEditor::MainWindow::Right,
                                                      QIcon::fromTheme(QStringLiteral("code-context")),
                                                      i18n("LSP Client Symbol Outline")));
220

221
        m_symbols = new QTreeView(m_toolview.data());
222
        m_symbols->setFocusPolicy(Qt::NoFocus);
223
224
225
226
        m_symbols->setLayoutDirection(Qt::LeftToRight);
        m_toolview->layout()->setContentsMargins(0, 0, 0, 0);
        m_toolview->layout()->addWidget(m_symbols);
        m_toolview->layout()->setSpacing(0);
227

228
        // setup filter line edit
229
        m_filter = new KLineEdit(m_toolview.data());
230
231
232
233
234
        m_toolview->layout()->addWidget(m_filter);
        m_filter->setPlaceholderText(i18n("Filter..."));
        m_filter->setClearButtonEnabled(true);
        connect(m_filter, &KLineEdit::textChanged, this, &self_type::filterTextChanged);

235
236
        m_symbols->setContextMenuPolicy(Qt::CustomContextMenu);
        m_symbols->setIndentation(10);
237
238
        m_symbols->setEditTriggers(QAbstractItemView::NoEditTriggers);
        m_symbols->setAllColumnsShowFocus(true);
239

240
241
242
243
244
        // init filter model once, later we only swap the source model!
        QItemSelectionModel *m = m_symbols->selectionModel();
        m_filterModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
        m_filterModel.setSortCaseSensitivity(Qt::CaseInsensitive);
        m_filterModel.setSourceModel(m_outline.get());
245
        m_filterModel.setRecursiveFilteringEnabled(true);
246
247
248
        m_symbols->setModel(&m_filterModel);
        delete m;

249
        connect(m_symbols, &QTreeView::customContextMenuRequested, this, &self_type::showContextMenu);
250
251
        connect(m_symbols, &QTreeView::activated, this, &self_type::goToSymbol);
        connect(m_symbols, &QTreeView::clicked, this, &self_type::goToSymbol);
252
253
254
255
256

        // context menu
        m_popup.reset(new QMenu(m_symbols));
        m_treeOn = m_popup->addAction(i18n("Tree Mode"), this, &self_type::displayOptionChanged);
        m_treeOn->setCheckable(true);
257
        m_expandOn = m_popup->addAction(i18n("Automatically Expand Tree"), this, &self_type::displayOptionChanged);
258
        m_expandOn->setCheckable(true);
259
        m_sortOn = m_popup->addAction(i18n("Sort Alphabetically"), this, &self_type::displayOptionChanged);
260
        m_sortOn->setCheckable(true);
261
        m_detailsOn = m_popup->addAction(i18n("Show Details"), this, &self_type::displayOptionChanged);
262
263
        m_detailsOn->setCheckable(true);
        m_popup->addSeparator();
264
265
        m_popup->addAction(i18n("Expand All"), m_symbols.data(), &QTreeView::expandAll);
        m_popup->addAction(i18n("Collapse All"), m_symbols.data(), &QTreeView::collapseAll);
266
267
268
269
270

        // sync with plugin settings if updated
        connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated);

        // get updated
271
        m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 500, 100));
272
        connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState);
Alexander Lohnau's avatar
Alexander Lohnau committed
273
274
275
        connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, this, [this]() {
            refresh(false);
        });
276

277
278
279
        // limit cached models; will not go beyond capacity set here
        m_models.reserve(MAX_MODELS + 1);

280
        // initial trigger of symbols view update
281
282
283
284
285
286
        configUpdated();
    }

    void displayOptionChanged()
    {
        m_expandOn->setEnabled(m_treeOn->isChecked());
287
        refresh(false);
288
289
290
291
292
293
294
295
296
297
298
    }

    void configUpdated()
    {
        m_treeOn->setChecked(m_plugin->m_symbolTree);
        m_detailsOn->setChecked(m_plugin->m_symbolDetails);
        m_expandOn->setChecked(m_plugin->m_symbolExpand);
        m_sortOn->setChecked(m_plugin->m_symbolSort);
        displayOptionChanged();
    }

299
300
301
302
    void showContextMenu(const QPoint &)
    {
        m_popup->popup(QCursor::pos(), m_treeOn);
    }
303

304
    void onViewState(KTextEditor::View *, LSPClientViewTracker::State newState)
305
    {
306
        switch (newState) {
307
308
309
310
311
312
313
314
315
        case LSPClientViewTracker::ViewChanged:
            refresh(true);
            break;
        case LSPClientViewTracker::TextChanged:
            refresh(false);
            break;
        case LSPClientViewTracker::LineChanged:
            updateCurrentTreeItem();
            break;
316
317
318
        }
    }

319
    void makeNodes(const QList<LSPSymbolInformation> &symbols, bool tree, bool show_detail, QStandardItemModel *model, QStandardItem *parent, bool &details)
320
    {
321
        const QIcon *icon = nullptr;
322
        for (const auto &symbol : symbols) {
323
            switch (symbol.kind) {
324
325
326
327
            case LSPSymbolKind::File:
            case LSPSymbolKind::Module:
            case LSPSymbolKind::Namespace:
            case LSPSymbolKind::Package:
328
                if (symbol.children.count() == 0) {
329
                    continue;
330
                }
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
                icon = &m_icon_pkg;
                break;
            case LSPSymbolKind::Class:
            case LSPSymbolKind::Interface:
                icon = &m_icon_class;
                break;
            case LSPSymbolKind::Enum:
                icon = &m_icon_typedef;
                break;
            case LSPSymbolKind::Method:
            case LSPSymbolKind::Function:
            case LSPSymbolKind::Constructor:
                icon = &m_icon_function;
                break;
            // all others considered/assumed Variable
            case LSPSymbolKind::Variable:
            case LSPSymbolKind::Constant:
            case LSPSymbolKind::String:
            case LSPSymbolKind::Number:
            case LSPSymbolKind::Property:
            case LSPSymbolKind::Field:
            default:
                // skip local variable
                // property, field, etc unlikely in such case anyway
355
                if (parent && parent->icon().cacheKey() == m_icon_function.cacheKey()) {
356
                    continue;
357
                }
358
                icon = &m_icon_var;
359
            }
360
361

            auto node = new QStandardItem();
362
            auto line = new QStandardItem();
363
            if (parent && tree) {
364
                parent->appendRow({node, line});
365
            } else {
366
                model->appendRow({node, line});
367
            }
368

369
            if (!symbol.detail.isEmpty()) {
370
                details = true;
371
            }
372
373
374
375
            auto detail = show_detail ? symbol.detail : QString();
            node->setText(symbol.name + detail);
            node->setIcon(*icon);
            node->setData(QVariant::fromValue<KTextEditor::Range>(symbol.range), Qt::UserRole);
376
377
            static const QChar prefix = QChar::fromLatin1('0');
            line->setText(QStringLiteral("%1").arg(symbol.range.start().line(), 7, 10, prefix));
378
            // recurse children
379
            makeNodes(symbol.children, tree, show_detail, model, node, details);
380
381
382
        }
    }

383
384
    void onDocumentSymbols(const QList<LSPSymbolInformation> &outline)
    {
385
        onDocumentSymbolsOrProblem(outline, QString(), true);
386
387
    }

388
    void onDocumentSymbolsOrProblem(const QList<LSPSymbolInformation> &outline, const QString &problem = QString(), bool cache = false)
389
    {
390
        if (!m_symbols) {
391
            return;
392
        }
393

394
        // construct new model for data
395
        auto newModel = std::make_shared<QStandardItemModel>();
396
397
398
399

        // if we have some problem, just report that, else construct model
        bool details = false;
        if (problem.isEmpty()) {
400
            makeNodes(outline, m_treeOn->isChecked(), m_detailsOn->isChecked(), newModel.get(), nullptr, details);
401
402
403
404
405
            if (cache) {
                // last request has been placed at head of model list
                Q_ASSERT(!m_models.isEmpty());
                m_models[0].model = newModel;
            }
406
407
        } else {
            newModel->appendRow(new QStandardItem(problem));
408
        }
409

410
411
412
        // cache detail info with model
        newModel->invisibleRootItem()->setData(details);

413
        // fixup headers
414
        QStringList headers{i18n("Symbols")};
415
416
        newModel->setHorizontalHeaderLabels(headers);

417
418
419
        setModel(newModel);
    }

420
    void setModel(const std::shared_ptr<QStandardItemModel> &newModel)
421
422
423
    {
        Q_ASSERT(newModel);

424
        // update filter model, do this before the assignment below deletes the old model!
425
        m_filterModel.setSourceModel(newModel.get());
426
427

        // delete old outline if there, keep our new one alive
428
        m_outline = newModel;
429

Christoph Cullmann's avatar
Christoph Cullmann committed
430
431
432
        // fixup sorting
        if (m_sortOn->isChecked()) {
            m_symbols->setSortingEnabled(true);
433
            m_symbols->sortByColumn(0, Qt::AscendingOrder);
Christoph Cullmann's avatar
Christoph Cullmann committed
434
        } else {
435
436
437
438
            // most servers provide items in reasonable file/input order
            // however sadly not all, so let's sort by hidden line number column to make sure
            m_symbols->setSortingEnabled(true);
            m_symbols->sortByColumn(1, Qt::AscendingOrder);
Christoph Cullmann's avatar
Christoph Cullmann committed
439
        }
440
441
        // no need to show internal info
        m_symbols->setColumnHidden(1, true);
442
443
444
445
446
447

        // handle auto-expansion
        if (m_expandOn->isChecked()) {
            m_symbols->expandAll();
        }

448
449
450
        // recover detail info from model data
        bool details = newModel->invisibleRootItem()->data().toBool();

451
452
        // disable detail setting if no such info available
        // (as an indication there is nothing to show anyway)
453
454
        m_detailsOn->setEnabled(details);

455
456
457
458
        // current item tracking
        updateCurrentTreeItem();
    }

459
    void refresh(bool clear)
460
    {
461
        // cancel old request!
462
        m_handle.cancel();
463
464

        // check if we have some server for the current view => trigger request
465
        auto view = m_mainWindow->activeView();
466
467
468
        if (auto server = m_serverManager->findServer(view)) {
            // clear current model in any case
            // this avoids that we show stuff not matching the current view
469
470
471
            // but let's only do it if needed, e.g. when changing view
            // so as to avoid unhealthy flickering in other cases
            if (clear) {
472
473
474
475
476
477
478
                onDocumentSymbolsOrProblem(QList<LSPSymbolInformation>(), QString(), false);
            }

            // check (valid) cache
            auto doc = view->document();
            auto revision = m_serverManager->revision(doc);
            auto it = m_models.begin();
479
            for (; it != m_models.end();) {
480
                if (it->document == doc) {
481
482
                    break;
                }
483
484
485
486
487
                if (!it->document) {
                    it = m_models.erase(it);
                    continue;
                }
                ++it;
488
489
            }
            if (it != m_models.end()) {
490
                // move to most recently used head
491
                m_models.move(it - m_models.begin(), 0);
492
                auto &model = m_models.front();
493
                // re-use if possible
494
495
496
                // reloaded document recycles revision number, so avoid stale cache
                // (clear := view switch)
                if (revision == model.revision && model.model && (clear || revision > 0)) {
497
498
499
500
                    setModel(model.model);
                    return;
                }
                it->revision = revision;
501
            } else {
502
                m_models.insert(0, {doc, revision, nullptr});
503
504
505
                if (m_models.size() > MAX_MODELS) {
                    m_models.pop_back();
                }
506
            }
507

508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
            // a cancelled request or modified content might result in error response,
            // so arrange to process it as an error rather than an empty result,
            // since the latter would (temporarily) clear the symbol outline
            // and lead to flicker until the next/final request has a proper result again
            auto eh = [this](const LSPResponseError &err) {
                switch (err.code) {
                case LSPErrorCode::ContentModified:
                case LSPErrorCode::RequestCancelled:
                    break;
                default:
                    onDocumentSymbols({});
                    break;
                }
            };

            m_handle = server->documentSymbols(doc->url(), this, utils::mem_fun(&self_type::onDocumentSymbols, this), eh);
524
525

            return;
526
        }
527
528

        // else: inform that no server is there
529
        onDocumentSymbolsOrProblem(QList<LSPSymbolInformation>(), i18n("No LSP server for this document."));
530
531
    }

532
    QStandardItem *getCurrentItem(QStandardItem *item, int line)
533
    {
534
535
        // first traverse the child items to have deepest match!
        // only do this if our stuff is expanded
536
        if (item == m_outline->invisibleRootItem() || m_symbols->isExpanded(m_filterModel.mapFromSource(m_outline->indexFromItem(item)))) {
537
538
539
540
541
            for (int i = 0; i < item->rowCount(); i++) {
                if (auto citem = getCurrentItem(item->child(i), line)) {
                    return citem;
                }
            }
542
543
        }

544
        // does the line match our item?
545
        return item->data(Qt::UserRole).value<KTextEditor::Range>().overlapsLine(line) ? item : nullptr;
546
547
548
549
    }

    void updateCurrentTreeItem()
    {
550
        KTextEditor::View *editView = m_mainWindow->activeView();
551
552
553
554
        if (!editView || !m_symbols) {
            return;
        }

555
556
557
        /**
         * get item if any
         */
558
        QStandardItem *item = getCurrentItem(m_outline->invisibleRootItem(), editView->cursorPositionVirtual().line());
559
560
        if (!item) {
            return;
561
562
        }

563
564
565
566
567
        /**
         * select it
         */
        QModelIndex index = m_filterModel.mapFromSource(m_outline->indexFromItem(item));
        m_symbols->scrollTo(index);
568
        m_symbols->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Clear | QItemSelectionModel::Select);
569
570
    }

571
    void goToSymbol(const QModelIndex &index)
572
    {
573
574
575
576
577
        KTextEditor::View *kv = m_mainWindow->activeView();
        const auto range = index.data(Qt::UserRole).value<KTextEditor::Range>();
        if (kv && range.isValid()) {
            kv->setCursorPosition(range.start());
        }
578
579
    }

580
581
582
583
584
585
private Q_SLOTS:
    /**
     * React on filter change
     * @param filterText new filter text
     */
    void filterTextChanged(const QString &filterText)
586
    {
587
        if (!m_symbols) {
588
            return;
589
        }
590

591
592
593
        /**
         * filter
         */
594
        m_filterModel.setFilterString(filterText);
595

596
597
598
599
600
601
        /**
         * expand
         */
        if (!filterText.isEmpty()) {
            QTimer::singleShot(100, m_symbols, &QTreeView::expandAll);
        }
602
603
604
    }
};

605
QObject *LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager)
606
{
Filip Gawin's avatar
Filip Gawin committed
607
    return new LSPClientSymbolViewImpl(plugin, mainWin, std::move(manager));
608
609
610
}

#include "lspclientsymbolview.moc"