lspclientsymbolview.cpp 20.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*  SPDX-License-Identifier: MIT

    Copyright (C) 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
    Copyright (C) 2019 Christoph Cullmann <cullmann@kde.org>

    Permission is hereby granted, free of charge, to any person obtaining
    a copy of this software and associated documentation files (the
    "Software"), to deal in the Software without restriction, including
    without limitation the rights to use, copy, modify, merge, publish,
    distribute, sublicense, and/or sell copies of the Software, and to
    permit persons to whom the Software is furnished to do so, subject to
    the following conditions:

    The above copyright notice and this permission notice shall be included
    in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
25
26
27

#include "lspclientsymbolview.h"

28
#include <KLineEdit>
29
#include <KLocalizedString>
30
#include <QSortFilterProxyModel>
31
32
33
34
35
36
37
38

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

#include <QHBoxLayout>
#include <QMenu>
#include <QPointer>
39
#include <QStandardItemModel>
40
41
#include <QTimer>
#include <QTreeView>
42
43

#include <memory>
Filip Gawin's avatar
Filip Gawin committed
44
#include <utility>
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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:
62
63
64
65
66
    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)
67
68
69
    {
        // get updated
        m_changeTimer.setSingleShot(true);
70
        auto ch = [this]() { emit newState(m_mainWindow->activeView(), TextChanged); };
71
72
73
        connect(&m_changeTimer, &QTimer::timeout, this, ch);

        m_motionTimer.setSingleShot(true);
74
        auto mh = [this]() { emit newState(m_mainWindow->activeView(), LineChanged); };
75
76
77
78
79
80
81
82
83
84
85
86
87
        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) {
88
                connect(view, &KTextEditor::View::cursorPositionChanged, this, &self_type::cursorPositionChanged, Qt::UniqueConnection);
89
90
            }
            if (m_change > 0 && view->document()) {
91
                connect(view->document(), &KTextEditor::Document::textChanged, this, &self_type::textChanged, Qt::UniqueConnection);
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
            }
            emit newState(view, ViewChanged);
            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);
        }
    }
};

118
LSPClientViewTracker *LSPClientViewTracker::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, int change_ms, int motion_ms)
119
120
121
122
{
    return new LSPClientViewTrackerImpl(plugin, mainWin, change_ms, motion_ms);
}

123
124
125
126
127
128
129
130
131
132
133
134
135
136
/*
 * 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
137
138
    QPointer<QTreeView> m_symbols;
    QPointer<KLineEdit> m_filter;
139
140
141
142
143
144
145
146
    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;
147
148
    // view tracking
    QScopedPointer<LSPClientViewTracker> m_viewTracker;
149
150
    // outstanding request
    LSPClientServer::RequestHandle m_handle;
151
    // cached outline models
152
    struct ModelData {
153
        QPointer<KTextEditor::Document> document;
154
155
156
157
158
159
        qint64 revision;
        std::shared_ptr<QStandardItemModel> model;
    };
    QList<ModelData> m_models;
    // max number to cache
    static constexpr int MAX_MODELS = 10;
160
    // last outline model we constructed
161
    std::shared_ptr<QStandardItemModel> m_outline;
162
    // filter model, setup once
163
    QSortFilterProxyModel m_filterModel;
164
165
166
167
168
169
170

    // 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"));
171
172

public:
173
174
175
176
177
    LSPClientSymbolViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager)
        : m_plugin(plugin)
        , m_mainWindow(mainWin)
        , m_serverManager(std::move(manager))
        , m_outline(new QStandardItemModel())
178
    {
179
        m_toolview.reset(m_mainWindow->createToolView(plugin, QStringLiteral("lspclient_symbol_outline"), KTextEditor::MainWindow::Right, QIcon::fromTheme(QStringLiteral("code-context")), i18n("LSP Client Symbol Outline")));
180

181
        m_symbols = new QTreeView(m_toolview.data());
182
        m_symbols->setFocusPolicy(Qt::NoFocus);
183
184
185
186
        m_symbols->setLayoutDirection(Qt::LeftToRight);
        m_toolview->layout()->setContentsMargins(0, 0, 0, 0);
        m_toolview->layout()->addWidget(m_symbols);
        m_toolview->layout()->setSpacing(0);
187

188
        // setup filter line edit
189
        m_filter = new KLineEdit(m_toolview.data());
190
191
192
193
194
        m_toolview->layout()->addWidget(m_filter);
        m_filter->setPlaceholderText(i18n("Filter..."));
        m_filter->setClearButtonEnabled(true);
        connect(m_filter, &KLineEdit::textChanged, this, &self_type::filterTextChanged);

195
196
        m_symbols->setContextMenuPolicy(Qt::CustomContextMenu);
        m_symbols->setIndentation(10);
197
198
        m_symbols->setEditTriggers(QAbstractItemView::NoEditTriggers);
        m_symbols->setAllColumnsShowFocus(true);
199

200
201
202
203
204
        // 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());
205
        m_filterModel.setRecursiveFilteringEnabled(true);
206
207
208
        m_symbols->setModel(&m_filterModel);
        delete m;

209
        connect(m_symbols, &QTreeView::customContextMenuRequested, this, &self_type::showContextMenu);
210
211
        connect(m_symbols, &QTreeView::activated, this, &self_type::goToSymbol);
        connect(m_symbols, &QTreeView::clicked, this, &self_type::goToSymbol);
212
213
214
215
216

        // 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);
217
        m_expandOn = m_popup->addAction(i18n("Automatically Expand Tree"), this, &self_type::displayOptionChanged);
218
        m_expandOn->setCheckable(true);
219
        m_sortOn = m_popup->addAction(i18n("Sort Alphabetically"), this, &self_type::displayOptionChanged);
220
        m_sortOn->setCheckable(true);
221
        m_detailsOn = m_popup->addAction(i18n("Show Details"), this, &self_type::displayOptionChanged);
222
223
        m_detailsOn->setCheckable(true);
        m_popup->addSeparator();
224
225
        m_popup->addAction(i18n("Expand All"), m_symbols.data(), &QTreeView::expandAll);
        m_popup->addAction(i18n("Collapse All"), m_symbols.data(), &QTreeView::collapseAll);
226
227
228
229
230

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

        // get updated
231
        m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 500, 100));
232
233
        connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState);
        connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, this, [this]() { refresh(false); });
234

235
236
237
        // limit cached models; will not go beyond capacity set here
        m_models.reserve(MAX_MODELS + 1);

238
        // initial trigger of symbols view update
239
240
241
242
243
244
        configUpdated();
    }

    void displayOptionChanged()
    {
        m_expandOn->setEnabled(m_treeOn->isChecked());
245
        refresh(false);
246
247
248
249
250
251
252
253
254
255
256
    }

    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();
    }

257
258
259
260
    void showContextMenu(const QPoint &)
    {
        m_popup->popup(QCursor::pos(), m_treeOn);
    }
261

262
    void onViewState(KTextEditor::View *, LSPClientViewTracker::State newState)
263
    {
264
        switch (newState) {
265
266
267
268
269
270
271
272
273
        case LSPClientViewTracker::ViewChanged:
            refresh(true);
            break;
        case LSPClientViewTracker::TextChanged:
            refresh(false);
            break;
        case LSPClientViewTracker::LineChanged:
            updateCurrentTreeItem();
            break;
274
275
276
        }
    }

277
    void makeNodes(const QList<LSPSymbolInformation> &symbols, bool tree, bool show_detail, QStandardItemModel *model, QStandardItem *parent, bool &details)
278
    {
279
        const QIcon *icon = nullptr;
280
        for (const auto &symbol : symbols) {
281
            switch (symbol.kind) {
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
            case LSPSymbolKind::File:
            case LSPSymbolKind::Module:
            case LSPSymbolKind::Namespace:
            case LSPSymbolKind::Package:
                if (symbol.children.count() == 0)
                    continue;
                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
                if (parent && parent->icon().cacheKey() == m_icon_function.cacheKey())
                    continue;
                icon = &m_icon_var;
315
            }
316
317

            auto node = new QStandardItem();
318
            auto line = new QStandardItem();
319
            if (parent && tree)
320
                parent->appendRow({node, line});
321
            else
322
                model->appendRow({node, line});
323
324
325
326
327
328
329

            if (!symbol.detail.isEmpty())
                details = true;
            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);
330
331
            static const QChar prefix = QChar::fromLatin1('0');
            line->setText(QStringLiteral("%1").arg(symbol.range.start().line(), 7, 10, prefix));
332
            // recurse children
333
            makeNodes(symbol.children, tree, show_detail, model, node, details);
334
335
336
        }
    }

337
338
    void onDocumentSymbols(const QList<LSPSymbolInformation> &outline)
    {
339
        onDocumentSymbolsOrProblem(outline, QString(), true);
340
341
    }

342
    void onDocumentSymbolsOrProblem(const QList<LSPSymbolInformation> &outline, const QString &problem = QString(), bool cache = false)
343
344
345
346
    {
        if (!m_symbols)
            return;

347
        // construct new model for data
348
        auto newModel = std::make_shared<QStandardItemModel>();
349
350
351
352

        // if we have some problem, just report that, else construct model
        bool details = false;
        if (problem.isEmpty()) {
353
            makeNodes(outline, m_treeOn->isChecked(), m_detailsOn->isChecked(), newModel.get(), nullptr, details);
354
355
356
357
358
            if (cache) {
                // last request has been placed at head of model list
                Q_ASSERT(!m_models.isEmpty());
                m_models[0].model = newModel;
            }
359
360
        } else {
            newModel->appendRow(new QStandardItem(problem));
361
        }
362

363
364
365
        // cache detail info with model
        newModel->invisibleRootItem()->setData(details);

366
        // fixup headers
367
        QStringList headers {i18n("Symbols")};
368
369
        newModel->setHorizontalHeaderLabels(headers);

370
371
372
        setModel(newModel);
    }

373
    void setModel(const std::shared_ptr<QStandardItemModel> &newModel)
374
375
376
    {
        Q_ASSERT(newModel);

377
        // update filter model, do this before the assignment below deletes the old model!
378
        m_filterModel.setSourceModel(newModel.get());
379
380

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

Christoph Cullmann's avatar
Christoph Cullmann committed
383
384
385
        // fixup sorting
        if (m_sortOn->isChecked()) {
            m_symbols->setSortingEnabled(true);
386
            m_symbols->sortByColumn(0, Qt::AscendingOrder);
Christoph Cullmann's avatar
Christoph Cullmann committed
387
        } else {
388
389
390
391
            // 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
392
        }
393
394
        // no need to show internal info
        m_symbols->setColumnHidden(1, true);
395
396
397
398
399
400

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

401
402
403
        // recover detail info from model data
        bool details = newModel->invisibleRootItem()->data().toBool();

404
405
        // disable detail setting if no such info available
        // (as an indication there is nothing to show anyway)
406
407
        m_detailsOn->setEnabled(details);

408
409
410
411
        // current item tracking
        updateCurrentTreeItem();
    }

412
    void refresh(bool clear)
413
    {
414
        // cancel old request!
415
        m_handle.cancel();
416
417

        // check if we have some server for the current view => trigger request
418
        auto view = m_mainWindow->activeView();
419
420
421
        if (auto server = m_serverManager->findServer(view)) {
            // clear current model in any case
            // this avoids that we show stuff not matching the current view
422
423
424
            // but let's only do it if needed, e.g. when changing view
            // so as to avoid unhealthy flickering in other cases
            if (clear) {
425
426
427
428
429
430
431
                onDocumentSymbolsOrProblem(QList<LSPSymbolInformation>(), QString(), false);
            }

            // check (valid) cache
            auto doc = view->document();
            auto revision = m_serverManager->revision(doc);
            auto it = m_models.begin();
432
            for (; it != m_models.end();) {
433
                if (it->document == doc) {
434
435
                    break;
                }
436
437
438
439
440
                if (!it->document) {
                    it = m_models.erase(it);
                    continue;
                }
                ++it;
441
442
            }
            if (it != m_models.end()) {
443
                // move to most recently used head
444
                m_models.move(it - m_models.begin(), 0);
445
                auto &model = m_models.front();
446
                // re-use if possible
447
448
449
                // reloaded document recycles revision number, so avoid stale cache
                // (clear := view switch)
                if (revision == model.revision && model.model && (clear || revision > 0)) {
450
451
452
453
                    setModel(model.model);
                    return;
                }
                it->revision = revision;
454
            } else {
455
                m_models.insert(0, {doc, revision, nullptr});
456
457
458
                if (m_models.size() > MAX_MODELS) {
                    m_models.pop_back();
                }
459
            }
460

461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
            // 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);
477
478

            return;
479
        }
480
481

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

485
    QStandardItem *getCurrentItem(QStandardItem *item, int line)
486
    {
487
488
        // first traverse the child items to have deepest match!
        // only do this if our stuff is expanded
489
        if (item == m_outline->invisibleRootItem() || m_symbols->isExpanded(m_filterModel.mapFromSource(m_outline->indexFromItem(item)))) {
490
491
492
493
494
            for (int i = 0; i < item->rowCount(); i++) {
                if (auto citem = getCurrentItem(item->child(i), line)) {
                    return citem;
                }
            }
495
496
        }

497
        // does the line match our item?
498
        return item->data(Qt::UserRole).value<KTextEditor::Range>().overlapsLine(line) ? item : nullptr;
499
500
501
502
    }

    void updateCurrentTreeItem()
    {
503
        KTextEditor::View *editView = m_mainWindow->activeView();
504
505
506
507
        if (!editView || !m_symbols) {
            return;
        }

508
509
510
        /**
         * get item if any
         */
511
        QStandardItem *item = getCurrentItem(m_outline->invisibleRootItem(), editView->cursorPositionVirtual().line());
512
513
        if (!item) {
            return;
514
515
        }

516
517
518
519
520
        /**
         * select it
         */
        QModelIndex index = m_filterModel.mapFromSource(m_outline->indexFromItem(item));
        m_symbols->scrollTo(index);
521
        m_symbols->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Clear | QItemSelectionModel::Select);
522
523
    }

524
    void goToSymbol(const QModelIndex &index)
525
    {
526
527
528
529
530
        KTextEditor::View *kv = m_mainWindow->activeView();
        const auto range = index.data(Qt::UserRole).value<KTextEditor::Range>();
        if (kv && range.isValid()) {
            kv->setCursorPosition(range.start());
        }
531
532
    }

533
534
535
536
537
538
private Q_SLOTS:
    /**
     * React on filter change
     * @param filterText new filter text
     */
    void filterTextChanged(const QString &filterText)
539
    {
540
        if (!m_symbols) {
541
            return;
542
        }
543

544
545
546
547
        /**
         * filter
         */
        m_filterModel.setFilterFixedString(filterText);
548

549
550
551
552
553
554
        /**
         * expand
         */
        if (!filterText.isEmpty()) {
            QTimer::singleShot(100, m_symbols, &QTreeView::expandAll);
        }
555
556
557
    }
};

558
QObject *LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager)
559
{
Filip Gawin's avatar
Filip Gawin committed
560
    return new LSPClientSymbolViewImpl(plugin, mainWin, std::move(manager));
561
562
563
}

#include "lspclientsymbolview.moc"