lspclientsymbolview.cpp 19.1 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
154
155
156
157
158
159
        KTextEditor::Document *document;
        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
318

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

            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);
329
            // recurse children
330
            makeNodes(symbol.children, tree, show_detail, model, node, details);
331
332
333
        }
    }

334
335
    void onDocumentSymbols(const QList<LSPSymbolInformation> &outline)
    {
336
        onDocumentSymbolsOrProblem(outline, QString(), true);
337
338
    }

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

344
        // construct new model for data
345
        auto newModel = std::make_shared<QStandardItemModel>();
346
347
348
349

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

360
361
362
        // cache detail info with model
        newModel->invisibleRootItem()->setData(details);

363
        // fixup headers
364
        QStringList headers {i18n("Symbols")};
365
366
        newModel->setHorizontalHeaderLabels(headers);

367
368
369
        setModel(newModel);
    }

370
    void setModel(const std::shared_ptr<QStandardItemModel> &newModel)
371
372
373
    {
        Q_ASSERT(newModel);

374
        // update filter model, do this before the assignment below deletes the old model!
375
        m_filterModel.setSourceModel(newModel.get());
376
377

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

Christoph Cullmann's avatar
Christoph Cullmann committed
380
381
382
        // fixup sorting
        if (m_sortOn->isChecked()) {
            m_symbols->setSortingEnabled(true);
383
            m_symbols->sortByColumn(0, Qt::AscendingOrder);
Christoph Cullmann's avatar
Christoph Cullmann committed
384
        } else {
385
            m_symbols->sortByColumn(-1, Qt::AscendingOrder);
Christoph Cullmann's avatar
Christoph Cullmann committed
386
        }
387
388
389
390
391
392

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

393
394
395
        // recover detail info from model data
        bool details = newModel->invisibleRootItem()->data().toBool();

396
397
        // disable detail setting if no such info available
        // (as an indication there is nothing to show anyway)
398
399
        m_detailsOn->setEnabled(details);

400
401
402
403
        // current item tracking
        updateCurrentTreeItem();
    }

404
    void refresh(bool clear)
405
    {
406
        // cancel old request!
407
        m_handle.cancel();
408
409

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

            // check (valid) cache
            auto doc = view->document();
            auto revision = m_serverManager->revision(doc);
            auto it = m_models.begin();
            for (; it != m_models.end(); ++it) {
425
                if (it->document == doc) {
426
427
428
429
                    break;
                }
            }
            if (it != m_models.end()) {
430
                // move to most recently used head
431
                m_models.move(it - m_models.begin(), 0);
432
                auto &model = m_models.front();
433
                // re-use if possible
434
435
436
                // reloaded document recycles revision number, so avoid stale cache
                // (clear := view switch)
                if (revision == model.revision && model.model && (clear || revision > 0)) {
437
438
439
440
                    setModel(model.model);
                    return;
                }
                it->revision = revision;
441
            } else {
442
                m_models.insert(0, {doc, revision, nullptr});
443
444
445
                if (m_models.size() > MAX_MODELS) {
                    m_models.pop_back();
                }
446
            }
447

448
            m_handle = server->documentSymbols(view->document()->url(), this, utils::mem_fun(&self_type::onDocumentSymbols, this));
449
450

            return;
451
        }
452
453

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

457
    QStandardItem *getCurrentItem(QStandardItem *item, int line)
458
    {
459
460
        // first traverse the child items to have deepest match!
        // only do this if our stuff is expanded
461
        if (item == m_outline->invisibleRootItem() || m_symbols->isExpanded(m_filterModel.mapFromSource(m_outline->indexFromItem(item)))) {
462
463
464
465
466
            for (int i = 0; i < item->rowCount(); i++) {
                if (auto citem = getCurrentItem(item->child(i), line)) {
                    return citem;
                }
            }
467
468
        }

469
        // does the line match our item?
470
        return item->data(Qt::UserRole).value<KTextEditor::Range>().overlapsLine(line) ? item : nullptr;
471
472
473
474
    }

    void updateCurrentTreeItem()
    {
475
        KTextEditor::View *editView = m_mainWindow->activeView();
476
477
478
479
        if (!editView || !m_symbols) {
            return;
        }

480
481
482
        /**
         * get item if any
         */
483
        QStandardItem *item = getCurrentItem(m_outline->invisibleRootItem(), editView->cursorPositionVirtual().line());
484
485
        if (!item) {
            return;
486
487
        }

488
489
490
491
492
        /**
         * select it
         */
        QModelIndex index = m_filterModel.mapFromSource(m_outline->indexFromItem(item));
        m_symbols->scrollTo(index);
493
        m_symbols->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Clear | QItemSelectionModel::Select);
494
495
    }

496
    void goToSymbol(const QModelIndex &index)
497
    {
498
499
500
501
502
        KTextEditor::View *kv = m_mainWindow->activeView();
        const auto range = index.data(Qt::UserRole).value<KTextEditor::Range>();
        if (kv && range.isValid()) {
            kv->setCursorPosition(range.start());
        }
503
504
    }

505
506
507
508
509
510
private Q_SLOTS:
    /**
     * React on filter change
     * @param filterText new filter text
     */
    void filterTextChanged(const QString &filterText)
511
    {
512
        if (!m_symbols) {
513
            return;
514
        }
515

516
517
518
519
        /**
         * filter
         */
        m_filterModel.setFilterFixedString(filterText);
520

521
522
523
524
525
526
        /**
         * expand
         */
        if (!filterText.isEmpty()) {
            QTimer::singleShot(100, m_symbols, &QTreeView::expandAll);
        }
527
528
529
    }
};

530
QObject *LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer<LSPClientServerManager> manager)
531
{
Filip Gawin's avatar
Filip Gawin committed
532
    return new LSPClientSymbolViewImpl(plugin, mainWin, std::move(manager));
533
534
535
}

#include "lspclientsymbolview.moc"