lspclientpluginview.cpp 83.3 KB
Newer Older
1
2
/*  SPDX-License-Identifier: MIT

3
    SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
4

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

#include "lspclientpluginview.h"
#include "lspclientcompletion.h"
10
#include "lspclienthover.h"
11
12
13
#include "lspclientplugin.h"
#include "lspclientservermanager.h"
#include "lspclientsymbolview.h"
14
15
16

#include "lspclient_debug.h"

17
#include <KAcceleratorManager>
18
19
20
21
22
23
24
25
26
#include <KActionCollection>
#include <KActionMenu>
#include <KLocalizedString>
#include <KStandardAction>
#include <KXMLGUIFactory>

#include <KTextEditor/CodeCompletionInterface>
#include <KTextEditor/Document>
#include <KTextEditor/MainWindow>
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
27
#include <KTextEditor/Message>
28
#include <KTextEditor/MovingInterface>
29
#include <KTextEditor/View>
30
31
#include <KXMLGUIClient>

32
#include <ktexteditor/configinterface.h>
33
34
35
#include <ktexteditor/markinterface.h>
#include <ktexteditor/movinginterface.h>
#include <ktexteditor/movingrange.h>
36
#include <ktexteditor_version.h>
37

38
#include <QAction>
39
#include <QApplication>
40
#include <QDateTime>
41
42
#include <QFileInfo>
#include <QHBoxLayout>
43
#include <QHeaderView>
44
#include <QInputDialog>
45
46
#include <QJsonObject>
#include <QKeyEvent>
47
#include <QMenu>
48
#include <QPlainTextEdit>
49
#include <QSet>
50
#include <QStandardItem>
51
#include <QTextCodec>
52
53
#include <QTimer>
#include <QTreeView>
Filip Gawin's avatar
Filip Gawin committed
54
#include <utility>
55

56
57
namespace RangeData
{
58
enum {
59
60
61
    // preserve UserRole for generic use where needed
    FileUrlRole = Qt::UserRole + 1,
    RangeRole,
62
    KindRole,
63
64
};

65
66
67
68
class KindEnum
{
public:
    enum _kind {
69
70
71
72
73
74
75
        Text = static_cast<int>(LSPDocumentHighlightKind::Text),
        Read = static_cast<int>(LSPDocumentHighlightKind::Read),
        Write = static_cast<int>(LSPDocumentHighlightKind::Write),
        Error = 10 + static_cast<int>(LSPDiagnosticSeverity::Error),
        Warning = 10 + static_cast<int>(LSPDiagnosticSeverity::Warning),
        Information = 10 + static_cast<int>(LSPDiagnosticSeverity::Information),
        Hint = 10 + static_cast<int>(LSPDiagnosticSeverity::Hint),
76
77
78
        Related
    };

79
80
    KindEnum(int v)
    {
81
        m_value = _kind(v);
82
    }
83

84
    KindEnum(LSPDocumentHighlightKind hl)
85
        : KindEnum(static_cast<_kind>(hl))
86
87
    {
    }
88

89
    KindEnum(LSPDiagnosticSeverity sev)
90
        : KindEnum(_kind(10 + static_cast<int>(sev)))
91
92
    {
    }
93

94
95
96
97
    operator _kind()
    {
        return m_value;
    }
98
99
100
101
102

private:
    _kind m_value;
};

103
104
105
106
107
static constexpr KTextEditor::MarkInterface::MarkTypes markType = KTextEditor::MarkInterface::markType31;
static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagError = KTextEditor::MarkInterface::Error;
static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagWarning = KTextEditor::MarkInterface::Warning;
static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagOther = KTextEditor::MarkInterface::markType30;
static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagAll = KTextEditor::MarkInterface::MarkTypes(markTypeDiagError | markTypeDiagWarning | markTypeDiagOther);
108
109

}
110

111
static QIcon diagnosticsIcon(LSPDiagnosticSeverity severity)
112
{
113
    // clang-format off
114
#define RETURN_CACHED_ICON(name, fallbackname) \
115
    { \
116
117
        static QIcon icon(QIcon::fromTheme(QStringLiteral(name), \
                                           QIcon::fromTheme(QStringLiteral(fallbackname)))); \
118
        return icon; \
119
    }
120
    // clang-format on
121
    switch (severity) {
122
    case LSPDiagnosticSeverity::Error:
123
        RETURN_CACHED_ICON("data-error", "dialog-error")
124
    case LSPDiagnosticSeverity::Warning:
125
        RETURN_CACHED_ICON("data-warning", "dialog-warning")
126
127
    case LSPDiagnosticSeverity::Information:
    case LSPDiagnosticSeverity::Hint:
128
        RETURN_CACHED_ICON("data-information", "dialog-information")
129
130
    default:
        break;
131
132
133
134
    }
    return QIcon();
}

135
static QIcon codeActionIcon()
136
137
138
139
140
{
    static QIcon icon(QIcon::fromTheme(QStringLiteral("insert-text")));
    return icon;
}

141
KTextEditor::Document *findDocument(KTextEditor::MainWindow *mainWindow, const QUrl &url)
142
143
{
    auto views = mainWindow->views();
144
    for (const auto v : views) {
145
146
147
148
149
150
151
        auto doc = v->document();
        if (doc && doc->url() == url)
            return doc;
    }
    return nullptr;
}

152
153
154
155
156
// helper to read lines from unopened documents
// lightweight and does not require additional symbols
class FileLineReader
{
    QFile file;
157
158
    int lastLineNo = -1;
    QString lastLine;
159
160

public:
161
    FileLineReader(const QUrl &url)
162
        : file(url.toLocalFile())
163
164
165
    {
        file.open(QIODevice::ReadOnly);
    }
166

167
    // called with non-descending lineno
168
169
    QString line(int lineno)
    {
170
171
172
        if (lineno == lastLineNo) {
            return lastLine;
        }
173
174
        while (file.isOpen() && !file.atEnd()) {
            auto line = file.readLine();
175
            if (++lastLineNo == lineno) {
176
177
178
179
180
181
                QTextCodec::ConverterState state;
                QTextCodec *codec = QTextCodec::codecForName("UTF-8");
                QString text = codec->toUnicode(line.constData(), line.size(), &state);
                if (state.invalidChars > 0) {
                    text = QString::fromLatin1(line);
                }
182
                while (text.size() && text.at(text.size() - 1).isSpace())
183
                    text.chop(1);
184
                lastLine = text;
185
186
187
188
189
190
191
                return text;
            }
        }
        return QString();
    }
};

192
class LSPClientActionView : public QObject
193
194
195
{
    Q_OBJECT

196
    typedef LSPClientActionView self_type;
197
198
199

    LSPClientPlugin *m_plugin;
    KTextEditor::MainWindow *m_mainWindow;
200
    KXMLGUIClient *m_client;
201
    QSharedPointer<LSPClientServerManager> m_serverManager;
202
    QScopedPointer<LSPClientViewTracker> m_viewTracker;
203
    QScopedPointer<LSPClientCompletion> m_completion;
204
    QScopedPointer<LSPClientHover> m_hover;
205
    QScopedPointer<KTextEditor::TextHintProvider> m_forwardHover;
206
207
    QScopedPointer<QObject> m_symbolView;

208
209
210
    QPointer<QAction> m_findDef;
    QPointer<QAction> m_findDecl;
    QPointer<QAction> m_findRef;
211
    QPointer<QAction> m_triggerHighlight;
212
    QPointer<QAction> m_triggerSymbolInfo;
213
    QPointer<QAction> m_triggerFormat;
214
    QPointer<QAction> m_triggerRename;
215
    QPointer<QAction> m_complDocOn;
216
    QPointer<QAction> m_refDeclaration;
217
    QPointer<QAction> m_autoHover;
218
    QPointer<QAction> m_onTypeFormatting;
219
    QPointer<QAction> m_incrementalSync;
220
221
222
    QPointer<QAction> m_diagnostics;
    QPointer<QAction> m_diagnosticsHighlight;
    QPointer<QAction> m_diagnosticsMark;
223
    QPointer<QAction> m_diagnosticsHover;
224
    QPointer<QAction> m_diagnosticsSwitch;
225
226
227
    QPointer<QAction> m_messages;
    QPointer<KSelectAction> m_messagesAutoSwitch;
    QPointer<QAction> m_messagesSwitch;
228
    QPointer<QAction> m_closeDynamic;
229
230
    QPointer<QAction> m_restartServer;
    QPointer<QAction> m_restartAll;
231

232
233
234
    // toolview
    QScopedPointer<QWidget> m_toolView;
    QPointer<QTabWidget> m_tabWidget;
235
    // applied search ranges
236
    typedef QMultiHash<KTextEditor::Document *, KTextEditor::MovingRange *> RangeCollection;
237
    RangeCollection m_ranges;
Christoph Cullmann's avatar
Christoph Cullmann committed
238
    QHash<KTextEditor::Document *, QHash<int, QVector<KTextEditor::MovingRange *>>> m_semanticHighlightRanges;
239
    // applied marks
240
    typedef QSet<KTextEditor::Document *> DocumentCollection;
241
    DocumentCollection m_marks;
242
243
244
245
    // modelis either owned by tree added to tabwidget or owned here
    QScopedPointer<QStandardItemModel> m_ownedModel;
    // in either case, the model that directs applying marks/ranges
    QPointer<QStandardItemModel> m_markModel;
246
247
248
    // goto definition and declaration jump list is more a menu than a
    // search result, so let's not keep adding new tabs for those
    // previous tree for definition result
249
    QPointer<QTreeView> m_defTree;
250
    // ... and for declaration
251
    QPointer<QTreeView> m_declTree;
252

253
    // diagnostics tab
254
    QPointer<QTreeView> m_diagnosticsTree;
255
    // tree widget is either owned here or by tab
256
257
    QScopedPointer<QTreeView> m_diagnosticsTreeOwn;
    QScopedPointer<QStandardItemModel> m_diagnosticsModel;
258
259
    // diagnostics ranges
    RangeCollection m_diagnosticsRanges;
260
261
    // and marks
    DocumentCollection m_diagnosticsMarks;
262

263
264
265
266
267
268
    using MessagesWidget = QPlainTextEdit;
    // messages tab
    QPointer<MessagesWidget> m_messagesView;
    // widget is either owned here or by tab
    QScopedPointer<MessagesWidget> m_messagesViewOwn;

269
270
    // views on which completions have been registered
    QSet<KTextEditor::View *> m_completionViews;
271
272
273
274

    // views on which hovers have been registered
    QSet<KTextEditor::View *> m_hoverViews;

275
276
277
278
279
    // outstanding request
    LSPClientServer::RequestHandle m_handle;
    // timeout on request
    bool m_req_timeout = false;

280
281
    // accept incoming applyEdit
    bool m_accept_edit = false;
282
283
    // characters to trigger format request
    QVector<QChar> m_onTypeFormattingTriggers;
284

285
286
287
288
    KActionCollection *actionCollection() const
    {
        return m_client->actionCollection();
    }
289

290
291
292
293
    // inner class that forwards directly to method for convenience
    class ForwardingTextHintProvider : public KTextEditor::TextHintProvider
    {
        LSPClientActionView *m_parent;
Christoph Cullmann's avatar
Christoph Cullmann committed
294

295
296
297
298
299
300
301
302
303
304
305
306
307
    public:
        ForwardingTextHintProvider(LSPClientActionView *parent)
            : m_parent(parent)
        {
            Q_ASSERT(m_parent);
        }

        virtual QString textHint(KTextEditor::View *view, const KTextEditor::Cursor &position) override
        {
            return m_parent->onTextHint(view, position);
        }
    };

308
public:
309
310
311
312
313
314
315
316
    LSPClientActionView(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, KXMLGUIClient *client, QSharedPointer<LSPClientServerManager> serverManager)
        : QObject(mainWin)
        , m_plugin(plugin)
        , m_mainWindow(mainWin)
        , m_client(client)
        , m_serverManager(std::move(serverManager))
        , m_completion(LSPClientCompletion::new_(m_serverManager))
        , m_hover(LSPClientHover::new_(m_serverManager))
317
        , m_forwardHover(new ForwardingTextHintProvider(this))
318
        , m_symbolView(LSPClientSymbolView::new_(plugin, mainWin, m_serverManager))
319
320
    {
        connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::updateState);
321
322
        connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &self_type::handleEsc);
        connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, this, &self_type::updateState);
323
        connect(m_serverManager.data(), &LSPClientServerManager::showMessage, this, &self_type::onShowMessage);
324

325
        m_findDef = actionCollection()->addAction(QStringLiteral("lspclient_find_definition"), this, &self_type::goToDefinition);
326
        m_findDef->setText(i18n("Go to Definition"));
327
        m_findDecl = actionCollection()->addAction(QStringLiteral("lspclient_find_declaration"), this, &self_type::goToDeclaration);
328
        m_findDecl->setText(i18n("Go to Declaration"));
329
        m_findRef = actionCollection()->addAction(QStringLiteral("lspclient_find_references"), this, &self_type::findReferences);
330
        m_findRef->setText(i18n("Find References"));
331
        m_triggerHighlight = actionCollection()->addAction(QStringLiteral("lspclient_highlight"), this, &self_type::highlight);
332
        m_triggerHighlight->setText(i18n("Highlight"));
333
        m_triggerSymbolInfo = actionCollection()->addAction(QStringLiteral("lspclient_symbol_info"), this, &self_type::symbolInfo);
Andrzej Dabrowski's avatar
Andrzej Dabrowski committed
334
        m_triggerSymbolInfo->setText(i18n("Symbol Info"));
335
        m_triggerFormat = actionCollection()->addAction(QStringLiteral("lspclient_format"), this, &self_type::format);
336
        m_triggerFormat->setText(i18n("Format"));
337
        m_triggerRename = actionCollection()->addAction(QStringLiteral("lspclient_rename"), this, &self_type::rename);
338
        m_triggerRename->setText(i18n("Rename"));
339

340
        // general options
341
        m_complDocOn = actionCollection()->addAction(QStringLiteral("lspclient_completion_doc"), this, &self_type::displayOptionChanged);
342
343
        m_complDocOn->setText(i18n("Show selected completion documentation"));
        m_complDocOn->setCheckable(true);
344
        m_refDeclaration = actionCollection()->addAction(QStringLiteral("lspclient_references_declaration"), this, &self_type::displayOptionChanged);
345
346
        m_refDeclaration->setText(i18n("Include declaration in references"));
        m_refDeclaration->setCheckable(true);
347
        m_autoHover = actionCollection()->addAction(QStringLiteral("lspclient_auto_hover"), this, &self_type::displayOptionChanged);
348
349
        m_autoHover->setText(i18n("Show hover information"));
        m_autoHover->setCheckable(true);
350
        m_onTypeFormatting = actionCollection()->addAction(QStringLiteral("lspclient_type_formatting"), this, &self_type::displayOptionChanged);
351
352
        m_onTypeFormatting->setText(i18n("Format on typing"));
        m_onTypeFormatting->setCheckable(true);
353
        m_incrementalSync = actionCollection()->addAction(QStringLiteral("lspclient_incremental_sync"), this, &self_type::displayOptionChanged);
354
355
        m_incrementalSync->setText(i18n("Incremental document synchronization"));
        m_incrementalSync->setCheckable(true);
356

357
        // diagnostics
358
        m_diagnostics = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics"), this, &self_type::displayOptionChanged);
359
360
        m_diagnostics->setText(i18n("Show diagnostics notifications"));
        m_diagnostics->setCheckable(true);
361
        m_diagnosticsHighlight = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_highlight"), this, &self_type::displayOptionChanged);
362
363
        m_diagnosticsHighlight->setText(i18n("Show diagnostics highlights"));
        m_diagnosticsHighlight->setCheckable(true);
364
        m_diagnosticsMark = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_mark"), this, &self_type::displayOptionChanged);
365
366
        m_diagnosticsMark->setText(i18n("Show diagnostics marks"));
        m_diagnosticsMark->setCheckable(true);
367
368
369
        m_diagnosticsHover = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_hover"), this, &self_type::displayOptionChanged);
        m_diagnosticsHover->setText(i18n("Show diagnostics on hover"));
        m_diagnosticsHover->setCheckable(true);
370
        m_diagnosticsSwitch = actionCollection()->addAction(QStringLiteral("lspclient_diagnostic_switch"), this, &self_type::switchToDiagnostics);
371
        m_diagnosticsSwitch->setText(i18n("Switch to diagnostics tab"));
372

373
374
375
376
377
378
379
380
381
382
383
        // messages
        m_messages = actionCollection()->addAction(QStringLiteral("lspclient_messages"), this, &self_type::displayOptionChanged);
        m_messages->setText(i18n("Show messages"));
        m_messages->setCheckable(true);
        m_messagesAutoSwitch = new KSelectAction(i18n("Switch to messages tab upon message level"), this);
        actionCollection()->addAction(QStringLiteral("lspclient_messages_auto_switch"), m_messagesAutoSwitch);
        const QStringList list {i18nc("@info", "Never"), i18nc("@info", "Error"), i18nc("@info", "Warning"), i18nc("@info", "Information"), i18nc("@info", "Log")};
        m_messagesAutoSwitch->setItems(list);
        m_messagesSwitch = actionCollection()->addAction(QStringLiteral("lspclient_messages_switch"), this, &self_type::switchToMessages);
        m_messagesSwitch->setText(i18n("Switch to messages tab"));

384
385
386
        // server control and misc actions
        m_closeDynamic = actionCollection()->addAction(QStringLiteral("lspclient_close_dynamic"), this, &self_type::closeDynamic);
        m_closeDynamic->setText(i18n("Close all dynamic reference tabs"));
387
        m_restartServer = actionCollection()->addAction(QStringLiteral("lspclient_restart_server"), this, &self_type::restartCurrent);
388
        m_restartServer->setText(i18n("Restart LSP Server"));
389
        m_restartAll = actionCollection()->addAction(QStringLiteral("lspclient_restart_all"), this, &self_type::restartAll);
390
391
        m_restartAll->setText(i18n("Restart All LSP Servers"));

392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
        // more options
        auto moreOptions = new KActionMenu(i18n("More options"), this);
        actionCollection()->addAction(QStringLiteral("lspclient_more_options"), moreOptions);
        moreOptions->addAction(m_complDocOn);
        moreOptions->addAction(m_refDeclaration);
        moreOptions->addAction(m_autoHover);
        moreOptions->addAction(m_onTypeFormatting);
        moreOptions->addAction(m_incrementalSync);
        moreOptions->addSeparator();
        moreOptions->addAction(m_diagnostics);
        moreOptions->addAction(m_diagnosticsHighlight);
        moreOptions->addAction(m_diagnosticsMark);
        moreOptions->addAction(m_diagnosticsHover);
        moreOptions->addSeparator();
        moreOptions->addAction(m_messages);
        moreOptions->addAction(m_messagesAutoSwitch);
408

409
410
411
        // popup menu
        auto menu = new KActionMenu(i18n("LSP Client"), this);
        actionCollection()->addAction(QStringLiteral("popup_lspclient"), menu);
412
413
414
        menu->addAction(m_findDef);
        menu->addAction(m_findDecl);
        menu->addAction(m_findRef);
415
        menu->addAction(m_triggerHighlight);
416
        menu->addAction(m_triggerSymbolInfo);
417
        menu->addAction(m_triggerFormat);
418
        menu->addAction(m_triggerRename);
419
420
        menu->addSeparator();
        menu->addAction(m_complDocOn);
421
        menu->addAction(m_refDeclaration);
422
        menu->addAction(m_autoHover);
423
        menu->addAction(m_onTypeFormatting);
424
        menu->addAction(m_incrementalSync);
425
        menu->addSeparator();
426
427
428
        menu->addAction(m_diagnostics);
        menu->addAction(m_diagnosticsHighlight);
        menu->addAction(m_diagnosticsMark);
429
        menu->addAction(m_diagnosticsHover);
430
        menu->addAction(m_diagnosticsSwitch);
431
        menu->addSeparator();
432
433
434
435
        menu->addAction(m_messages);
        menu->addAction(m_messagesAutoSwitch);
        menu->addAction(m_messagesSwitch);
        menu->addSeparator();
436
        menu->addAction(m_closeDynamic);
437
438
        menu->addAction(m_restartServer);
        menu->addAction(m_restartAll);
439
440
441

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

443
        // toolview
444
        m_toolView.reset(mainWin->createToolView(plugin, QStringLiteral("kate_lspclient"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")), i18n("LSP Client")));
445
        m_tabWidget = new QTabWidget(m_toolView.data());
446
        m_toolView->layout()->addWidget(m_tabWidget);
447
        m_tabWidget->setFocusPolicy(Qt::NoFocus);
448
449
450
        m_tabWidget->setTabsClosable(true);
        KAcceleratorManager::setNoAccel(m_tabWidget);
        connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &self_type::tabCloseRequested);
451
        connect(m_tabWidget, &QTabWidget::currentChanged, this, &self_type::tabChanged);
452

453
        // diagnostics tab
454
        m_diagnosticsTree = new QTreeView();
455
        m_diagnosticsTree->setAlternatingRowColors(true);
456
        m_diagnosticsTreeOwn.reset(m_diagnosticsTree);
457
        m_diagnosticsModel.reset(new QStandardItemModel());
458
        m_diagnosticsModel->setColumnCount(1);
459
        m_diagnosticsTree->setModel(m_diagnosticsModel.data());
460
        configureTreeView(m_diagnosticsTree);
461
462
        connect(m_diagnosticsTree, &QTreeView::clicked, this, &self_type::goToItemLocation);
        connect(m_diagnosticsTree, &QTreeView::doubleClicked, this, &self_type::triggerCodeAction);
463

464
465
466
467
468
469
        // messages tab
        m_messagesView = new QPlainTextEdit();
        m_messagesView->setMaximumBlockCount(100);
        m_messagesView->setReadOnly(true);
        m_messagesViewOwn.reset(m_messagesView);

470
471
        // track position in view to sync diagnostics list
        m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 0, 500));
472
        connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState);
473

474
        configUpdated();
475
476
477
        updateState();
    }

Filip Gawin's avatar
Filip Gawin committed
478
    ~LSPClientActionView() override
479
    {
480
481
        // unregister all code-completion providers, else we might crash
        for (auto view : qAsConst(m_completionViews)) {
482
            qobject_cast<KTextEditor::CodeCompletionInterface *>(view)->unregisterCompletionModel(m_completion.data());
483
484
485
486
        }

        // unregister all text-hint providers, else we might crash
        for (auto view : qAsConst(m_hoverViews)) {
487
            qobject_cast<KTextEditor::TextHintInterface *>(view)->unregisterTextHintProvider(m_forwardHover.data());
488
489
        }

490
491
        clearAllLocationMarks();
        clearAllDiagnosticsMarks();
492
493
    }

494
495
496
497
498
499
500
    void configureTreeView(QTreeView *treeView)
    {
        treeView->setHeaderHidden(true);
        treeView->setFocusPolicy(Qt::NoFocus);
        treeView->setLayoutDirection(Qt::LeftToRight);
        treeView->setSortingEnabled(false);
        treeView->setEditTriggers(QAbstractItemView::NoEditTriggers);
501
502
503
504
505
506
507
508

        // context menu
        treeView->setContextMenuPolicy(Qt::CustomContextMenu);
        auto menu = new QMenu(treeView);
        menu->addAction(i18n("Expand All"), treeView, &QTreeView::expandAll);
        menu->addAction(i18n("Collapse All"), treeView, &QTreeView::collapseAll);
        auto h = [menu](const QPoint &) { menu->popup(QCursor::pos()); };
        connect(treeView, &QTreeView::customContextMenuRequested, h);
509
510
    }

511
512
    void displayOptionChanged()
    {
513
514
        m_diagnosticsHighlight->setEnabled(m_diagnostics->isChecked());
        m_diagnosticsMark->setEnabled(m_diagnostics->isChecked());
515
        m_diagnosticsHover->setEnabled(m_diagnostics->isChecked());
516
517
518
519
520
521
522
523
524
525
526
527
528
        // messages tab should go first
        int messagesIndex = m_tabWidget->indexOf(m_messagesView);
        if (m_messages->isChecked() && m_messagesViewOwn) {
            m_tabWidget->insertTab(0, m_messagesView, i18nc("@title:tab", "Messages"));
            messagesIndex = 0;
            m_messagesViewOwn.take();
        } else if (!m_messages->isChecked() && !m_messagesViewOwn) {
            m_messagesViewOwn.reset(m_messagesView);
            m_tabWidget->removeTab(messagesIndex);
            messagesIndex = -1;
        }
        // diagnstics tab next
        int diagnosticsIndex = m_tabWidget->indexOf(m_diagnosticsTree);
529
530
531
        // setTabEnabled may still show it ... so let's be more forceful
        if (m_diagnostics->isChecked() && m_diagnosticsTreeOwn) {
            m_diagnosticsTreeOwn.take();
532
            m_tabWidget->insertTab(messagesIndex + 1, m_diagnosticsTree, i18nc("@title:tab", "Diagnostics"));
533
534
        } else if (!m_diagnostics->isChecked() && !m_diagnosticsTreeOwn) {
            m_diagnosticsTreeOwn.reset(m_diagnosticsTree);
535
            m_tabWidget->removeTab(diagnosticsIndex);
536
        }
537
        m_diagnosticsSwitch->setEnabled(m_diagnostics->isChecked());
538
        m_serverManager->setIncrementalSync(m_incrementalSync->isChecked());
539
        updateState();
540
541
542
543
544
545
    }

    void configUpdated()
    {
        if (m_complDocOn)
            m_complDocOn->setChecked(m_plugin->m_complDoc);
546
547
        if (m_refDeclaration)
            m_refDeclaration->setChecked(m_plugin->m_refDeclaration);
548
549
        if (m_autoHover)
            m_autoHover->setChecked(m_plugin->m_autoHover);
550
551
        if (m_onTypeFormatting)
            m_onTypeFormatting->setChecked(m_plugin->m_onTypeFormatting);
552
553
        if (m_incrementalSync)
            m_incrementalSync->setChecked(m_plugin->m_incrementalSync);
554
555
556
557
558
559
        if (m_diagnostics)
            m_diagnostics->setChecked(m_plugin->m_diagnostics);
        if (m_diagnosticsHighlight)
            m_diagnosticsHighlight->setChecked(m_plugin->m_diagnosticsHighlight);
        if (m_diagnosticsMark)
            m_diagnosticsMark->setChecked(m_plugin->m_diagnosticsMark);
560
561
        if (m_diagnosticsHover)
            m_diagnosticsHover->setChecked(m_plugin->m_diagnosticsHover);
562
563
564
565
        if (m_messages)
            m_messages->setChecked(m_plugin->m_messages);
        if (m_messagesAutoSwitch)
            m_messagesAutoSwitch->setCurrentItem(m_plugin->m_messagesAutoSwitch);
566
567
568
        displayOptionChanged();
    }

569
570
571
572
573
    void restartCurrent()
    {
        KTextEditor::View *activeView = m_mainWindow->activeView();
        auto server = m_serverManager->findServer(activeView);
        if (server)
574
            m_serverManager->restart(server.data());
575
576
    }

577
578
579
580
    void restartAll()
    {
        m_serverManager->restart(nullptr);
    }
581

582
    static void clearMarks(KTextEditor::Document *doc, RangeCollection &ranges, DocumentCollection &docs, uint markType)
583
    {
584
        KTextEditor::MarkInterface *iface = docs.contains(doc) ? qobject_cast<KTextEditor::MarkInterface *>(doc) : nullptr;
585
        if (iface) {
586
587
            const QHash<int, KTextEditor::Mark *> marks = iface->marks();
            QHashIterator<int, KTextEditor::Mark *> i(marks);
588
589
            while (i.hasNext()) {
                i.next();
590
591
                if (i.value()->type & markType) {
                    iface->removeMark(i.value()->line, markType);
592
593
                }
            }
594
            docs.remove(doc);
595
596
        }

597
        for (auto it = ranges.find(doc); it != ranges.end() && it.key() == doc;) {
598
            delete it.value();
599
            it = ranges.erase(it);
600
601
602
        }
    }

603
    static void clearMarks(RangeCollection &ranges, DocumentCollection &docs, uint markType)
604
    {
605
        while (!ranges.empty()) {
606
            clearMarks(ranges.begin().key(), ranges, docs, markType);
607
        }
608
609
610
611
    }

    Q_SLOT void clearAllMarks(KTextEditor::Document *doc)
    {
612
613
        clearMarks(doc, m_ranges, m_marks, RangeData::markType);
        clearMarks(doc, m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll);
614
615
616
617
    }

    void clearAllLocationMarks()
    {
618
        clearMarks(m_ranges, m_marks, RangeData::markType);
619
        // no longer add any again
620
621
        m_ownedModel.reset();
        m_markModel.clear();
622
623
    }

624
625
    void clearAllDiagnosticsMarks()
    {
626
        clearMarks(m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll);
627
628
    }

629
    void addMarks(KTextEditor::Document *doc, QStandardItem *item, RangeCollection *ranges, DocumentCollection *docs)
630
    {
631
        Q_ASSERT(item);
632
        KTextEditor::MovingInterface *miface = qobject_cast<KTextEditor::MovingInterface *>(doc);
633
        Q_ASSERT(miface);
Christoph Cullmann's avatar
Christoph Cullmann committed
634
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 69, 0)
635
636
        KTextEditor::MarkInterfaceV2 *iface = qobject_cast<KTextEditor::MarkInterfaceV2 *>(doc);
#else
637
        KTextEditor::MarkInterface *iface = qobject_cast<KTextEditor::MarkInterface *>(doc);
638
#endif
639
        Q_ASSERT(iface);
640
        KTextEditor::View *activeView = m_mainWindow->activeView();
641
        KTextEditor::ConfigInterface *ciface = qobject_cast<KTextEditor::ConfigInterface *>(activeView);
642

643
        auto url = item->data(RangeData::FileUrlRole).toUrl();
644
645
646
        // document url could end up empty while in intermediate reload state
        // (and then it might match a parent item with no RangeData at all)
        if (url != doc->url() || url.isEmpty())
647
648
            return;

649
        KTextEditor::Range range = item->data(RangeData::RangeRole).value<LSPRange>();
650
651
        if (!range.isValid() || range.isEmpty())
            return;
652
        auto line = range.start().line();
653
        RangeData::KindEnum kind = RangeData::KindEnum(item->data(RangeData::KindRole).toInt());
654
655
656

        KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute());

657
        bool enabled = m_diagnostics && m_diagnostics->isChecked() && m_diagnosticsHighlight && m_diagnosticsHighlight->isChecked();
658
        KTextEditor::MarkInterface::MarkTypes markType = RangeData::markType;
659
        switch (kind) {
660
661
662
663
664
        case RangeData::KindEnum::Text: {
            // well, it's a bit like searching for something, so re-use that color
            QColor rangeColor = Qt::yellow;
            if (ciface) {
                rangeColor = ciface->configValue(QStringLiteral("search-highlight-color")).value<QColor>();
665
            }
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
            attr->setBackground(rangeColor);
            enabled = true;
            break;
        }
        // FIXME are there any symbolic/configurable ways to pick these colors?
        case RangeData::KindEnum::Read:
            attr->setBackground(Qt::green);
            enabled = true;
            break;
        case RangeData::KindEnum::Write:
            attr->setBackground(Qt::red);
            enabled = true;
            break;
        // use underlining for diagnostics to avoid lots of fancy flickering
        case RangeData::KindEnum::Error:
            markType = RangeData::markTypeDiagError;
            attr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
            attr->setUnderlineColor(Qt::red);
            break;
        case RangeData::KindEnum::Warning:
            markType = RangeData::markTypeDiagWarning;
            attr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
            attr->setUnderlineColor(QColor(255, 128, 0));
            break;
        case RangeData::KindEnum::Information:
        case RangeData::KindEnum::Hint:
        case RangeData::KindEnum::Related:
            markType = RangeData::markTypeDiagOther;
            attr->setUnderlineStyle(QTextCharFormat::DashUnderline);
            attr->setUnderlineColor(Qt::blue);
            break;
697
        }
698
        if (activeView) {
699
            attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color());
700
701
702
        }

        // highlight the range
703
        if (enabled && ranges) {
704
            KTextEditor::MovingRange *mr = miface->newMovingRange(range);
705
706
707
            mr->setAttribute(attr);
            mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection
            mr->setAttributeOnlyForViews(true);
708
            ranges->insert(doc, mr);
709
        }
710
711

        // add match mark for range
712
713
        const int ps = 32;
        bool handleClick = true;
714
        enabled = m_diagnostics && m_diagnostics->isChecked() && m_diagnosticsMark && m_diagnosticsMark->isChecked();
715
        switch (markType) {
716
717
        case RangeData::markType:
            iface->setMarkDescription(markType, i18n("RangeHighLight"));
Christoph Cullmann's avatar
Christoph Cullmann committed
718
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 69, 0)
719
720
            iface->setMarkIcon(markType, QIcon());
#else
721
            iface->setMarkPixmap(markType, QIcon().pixmap(0, 0));
722
#endif
723
724
725
726
727
            handleClick = false;
            enabled = true;
            break;
        case RangeData::markTypeDiagError:
            iface->setMarkDescription(markType, i18n("Error"));
Christoph Cullmann's avatar
Christoph Cullmann committed
728
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 69, 0)
729
730
            iface->setMarkIcon(markType, diagnosticsIcon(LSPDiagnosticSeverity::Error));
#else
731
            iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Error).pixmap(ps, ps));
732
#endif
733
734
735
            break;
        case RangeData::markTypeDiagWarning:
            iface->setMarkDescription(markType, i18n("Warning"));
Christoph Cullmann's avatar
Christoph Cullmann committed
736
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 69, 0)
737
738
            iface->setMarkIcon(markType, diagnosticsIcon(LSPDiagnosticSeverity::Warning));
#else
739
            iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Warning).pixmap(ps, ps));
740
#endif
741
742
743
            break;
        case RangeData::markTypeDiagOther:
            iface->setMarkDescription(markType, i18n("Information"));
Christoph Cullmann's avatar
Christoph Cullmann committed
744
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 69, 0)
745
746
            iface->setMarkIcon(markType, diagnosticsIcon(LSPDiagnosticSeverity::Information));
#else
747
            iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Information).pixmap(ps, ps));
748
#endif
749
750
751
752
            break;
        default:
            Q_ASSERT(false);
            break;
753
        }
754
        if (enabled && docs) {
755
            iface->addMark(line, markType);
756
757
            docs->insert(doc);
        }
758
759

        // ensure runtime match
760
761
        connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearAllMarks(KTextEditor::Document *)), Qt::UniqueConnection);
        connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearAllMarks(KTextEditor::Document *)), Qt::UniqueConnection);
762
763
        // reload might save/restore marks before/after above signals, so let's clear before that
        connect(doc, &KTextEditor::Document::aboutToReload, this, &self_type::clearAllMarks, Qt::UniqueConnection);
764
765

        if (handleClick) {
766
            connect(doc, SIGNAL(markClicked(KTextEditor::Document *, KTextEditor::Mark, bool &)), this, SLOT(onMarkClicked(KTextEditor::Document *, KTextEditor::Mark, bool &)), Qt::UniqueConnection);
767
        }
768
769
    }

770
    void addMarksRec(KTextEditor::Document *doc, QStandardItem *item, RangeCollection *ranges, DocumentCollection *docs)
771
772
    {
        Q_ASSERT(item);
773
        addMarks(doc, item, ranges, docs);
774
        for (int i = 0; i < item->rowCount(); ++i) {
775
            addMarksRec(doc, item->child(i), ranges, docs);
776
777
778
        }
    }

779
    void addMarks(KTextEditor::Document *doc, QStandardItemModel *treeModel, RangeCollection &ranges, DocumentCollection &docs)
780
781
    {
        // check if already added
782
783
784
785
        auto oranges = ranges.contains(doc) ? nullptr : &ranges;
        auto odocs = docs.contains(doc) ? nullptr : &docs;

        if (!oranges && !odocs)
786
787
            return;

788
        Q_ASSERT(treeModel);
789
        addMarksRec(doc, treeModel->invisibleRootItem(), oranges, odocs);
790
791
    }

792
    void goToDocumentLocation(const QUrl &uri, int line, int column)
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
    {
        KTextEditor::View *activeView = m_mainWindow->activeView();
        if (!activeView || uri.isEmpty() || line < 0 || column < 0)
            return;

        KTextEditor::Document *document = activeView->document();
        KTextEditor::Cursor cdef(line, column);

        if (document && uri == document->url()) {
            activeView->setCursorPosition(cdef);
        } else {
            KTextEditor::View *view = m_mainWindow->openUrl(uri);
            if (view) {
                view->setCursorPosition(cdef);
            }
        }
    }

811
    void goToItemLocation(const QModelIndex &index)
812
    {
813
        auto url = index.data(RangeData::FileUrlRole).toUrl();
814
815
        auto start = index.data(RangeData::RangeRole).value<LSPRange>().start();
        goToDocumentLocation(url, start.line(), start.column());
816
817
    }

818
819
    // custom item subclass that captures additional attributes;
    // a bit more convenient than the variant/role way
820
    struct DiagnosticItem : public QStandardItem {
821
822
823
824
        LSPDiagnostic m_diagnostic;
        LSPCodeAction m_codeAction;
        QSharedPointer<LSPClientRevisionSnapshot> m_snapshot;

825
826
827
828
        DiagnosticItem(const LSPDiagnostic &d)
            : m_diagnostic(d)
        {
        }
829

830
        DiagnosticItem(const LSPCodeAction &c, QSharedPointer<LSPClientRevisionSnapshot> s)
831
832
            : m_codeAction(c)
            , m_snapshot(std::move(s))
833
834
835
        {
            m_diagnostic.range = LSPRange::invalid();
        }
836

837
838
839
840
        bool isCodeAction()
        {
            return !m_diagnostic.range.isValid() && m_codeAction.title.size();
        }
841
842
    };

843
844
845
846
    // double click on:
    // diagnostic item -> request and add actions (below item)
    // code action -> perform action (literal edit and/or execute command)
    // (execution of command may lead to an applyEdit request from server)
847
    void triggerCodeAction(const QModelIndex &index)
848
849
850
851
    {
        KTextEditor::View *activeView = m_mainWindow->activeView();
        QPointer<KTextEditor::Document> document = activeView->document();
        auto server = m_serverManager->findServer(activeView);
852
        auto it = dynamic_cast<DiagnosticItem *>(m_diagnosticsModel->itemFromIndex(index));
853
854
855
856
        if (!server || !document || !it)
            return;

        // click on an action ?
857
        if (it->isCodeAction()) {
858
            auto &action = it->m_codeAction;
859
            // apply edit before command
860
            applyWorkspaceEdit(action.edit, it->m_snapshot.data());
861
            auto &command = action.command;
862
863
864
865
866
867
868
            if (command.command.size()) {
                // accept edit requests that may be sent to execute command
                m_accept_edit = true;
                // but only for a short time
                QTimer::singleShot(2000, this, [this] { m_accept_edit = false; });
                server->executeCommand(command.command, command.arguments);
            }
869
870
871
872
            // diagnostics are likely updated soon, but might be clicked again in meantime
            // so clear once executed, so not executed again
            action.edit.changes.clear();
            action.command.command.clear();
873
874
875
            return;
        }

876
877
878
879
880
        // only engage action if
        // * active document matches diagnostic document
        // * if really clicked a diagnostic item
        //   (which is the case as it != nullptr and not a code action)
        // * if no code action invoked and added already
Mark Nauwelaerts's avatar