lspclientpluginview.cpp 95.1 KB
Newer Older
1
/*
2
    SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
3

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

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

#include "lspclient_debug.h"

16
#include <KAcceleratorManager>
17
18
19
20
21
22
23
24
25
#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
26
#include <KTextEditor/Message>
27
#include <KTextEditor/MovingInterface>
28
#include <KTextEditor/View>
29
30
#include <KXMLGUIClient>

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

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

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

63
64
65
66
class KindEnum
{
public:
    enum _kind {
67
68
69
70
71
72
73
        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),
74
75
76
        Related
    };

77
78
    KindEnum(int v)
    {
79
        m_value = _kind(v);
80
    }
81

82
    KindEnum(LSPDocumentHighlightKind hl)
83
        : KindEnum(static_cast<_kind>(hl))
84
85
    {
    }
86

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

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

private:
    _kind m_value;
};

101
102
103
104
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;
Alexander Lohnau's avatar
Alexander Lohnau committed
105
106
static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagAll =
    KTextEditor::MarkInterface::MarkTypes(markTypeDiagError | markTypeDiagWarning | markTypeDiagOther);
107
108

}
109

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

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

140
KTextEditor::Document *findDocument(KTextEditor::MainWindow *mainWindow, const QUrl &url)
141
142
{
    auto views = mainWindow->views();
143
    for (const auto v : views) {
144
        auto doc = v->document();
145
        if (doc && doc->url() == url) {
146
            return doc;
147
        }
148
149
150
151
    }
    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
                }
185
                lastLine = text;
186
187
188
189
190
191
192
                return text;
            }
        }
        return QString();
    }
};

193
194
195
/**
 * @brief This is just a helper class that provides "underline" on Ctrl + click
 */
196
class CtrlHoverFeedback : public QObject
197
{
198
    Q_OBJECT
199
public:
Alexander Lohnau's avatar
Alexander Lohnau committed
200
    void highlight(KTextEditor::View *activeView)
201
    {
202
        // sanity checks
203
        if (!activeView) {
204
            return;
205
        }
206
207

        auto doc = activeView->document();
208
        if (!doc) {
209
            return;
210
        }
211
212

        // set the cursor
213
        if (w) {
214
            w->setCursor(Qt::PointingHandCursor);
215
        }
216

217
218
219
220
221
        // underline the hovered word
        auto mr = ranges[doc];
        if (mr) {
            mr->setRange(range);
        } else {
Alexander Lohnau's avatar
Alexander Lohnau committed
222
            auto miface = qobject_cast<KTextEditor::MovingInterface *>(doc);
223
            if (!miface) {
224
                return;
225
            }
226
227
            mr = miface->newMovingRange(range);
            ranges[doc] = mr;
228
            // clang-format off
Alexander Lohnau's avatar
Alexander Lohnau committed
229
            connect(doc,
230
                    SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)),
Alexander Lohnau's avatar
Alexander Lohnau committed
231
                    this,
232
                    SLOT(clear(KTextEditor::Document*)),
Alexander Lohnau's avatar
Alexander Lohnau committed
233
234
                    Qt::UniqueConnection);
            connect(doc,
235
                    SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)),
Alexander Lohnau's avatar
Alexander Lohnau committed
236
                    this,
237
                    SLOT(clear(KTextEditor::Document*)),
Alexander Lohnau's avatar
Alexander Lohnau committed
238
                    Qt::UniqueConnection);
239
            // clang-format on
240
241
242
243
244
245
        }

        static KTextEditor::Attribute::Ptr attr;
        if (!attr) {
            attr = new KTextEditor::Attribute;
            attr->setUnderlineStyle(QTextCharFormat::SingleUnderline);
246
        }
247
        mr->setAttribute(attr);
248
249
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
250
    void clear(KTextEditor::View *activeView)
251
    {
252
253
254
        if (activeView) {
            auto doc = activeView->document();
            if (doc) {
Alexander Lohnau's avatar
Alexander Lohnau committed
255
                auto &mr = ranges[doc];
256
                if (mr) {
257
                    mr->setRange(KTextEditor::Range::invalid());
258
                }
259
            }
260
261
262
        }
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
263
    void setRangeAndWidget(const KTextEditor::Range &r, QWidget *wid)
264
265
266
267
268
269
270
271
272
273
    {
        range = r;
        w = wid;
    }

    bool isValid() const
    {
        return w != nullptr;
    }

274
private:
Alexander Lohnau's avatar
Alexander Lohnau committed
275
    Q_SLOT void clear(KTextEditor::Document *doc)
276
    {
277
278
279
280
        if (doc) {
            auto it = ranges.find(doc);
            if (it != ranges.end()) {
                delete *it;
Waqar Ahmed's avatar
Waqar Ahmed committed
281
                ranges.erase(it);
282
283
            }
        }
284
285
    }

286
private:
Alexander Lohnau's avatar
Alexander Lohnau committed
287
288
    QWidget *w = nullptr;
    QHash<KTextEditor::Document *, KTextEditor::MovingRange *> ranges;
289
290
291
    KTextEditor::Range range;
};

292
class LSPClientActionView : public QObject
293
294
295
{
    Q_OBJECT

296
    typedef LSPClientActionView self_type;
297
298
299

    LSPClientPlugin *m_plugin;
    KTextEditor::MainWindow *m_mainWindow;
300
    KXMLGUIClient *m_client;
301
    QSharedPointer<LSPClientServerManager> m_serverManager;
302
    QScopedPointer<LSPClientViewTracker> m_viewTracker;
303
    QScopedPointer<LSPClientCompletion> m_completion;
304
    QScopedPointer<LSPClientHover> m_hover;
305
    QScopedPointer<KTextEditor::TextHintProvider> m_forwardHover;
306
307
    QScopedPointer<QObject> m_symbolView;

308
309
310
    QPointer<QAction> m_findDef;
    QPointer<QAction> m_findDecl;
    QPointer<QAction> m_findRef;
311
    QPointer<QAction> m_findImpl;
312
    QPointer<QAction> m_triggerHighlight;
313
    QPointer<QAction> m_triggerSymbolInfo;
314
    QPointer<QAction> m_triggerFormat;
315
    QPointer<QAction> m_triggerRename;
316
    QPointer<QAction> m_complDocOn;
317
    QPointer<QAction> m_refDeclaration;
318
    QPointer<QAction> m_autoHover;
319
    QPointer<QAction> m_onTypeFormatting;
320
    QPointer<QAction> m_incrementalSync;
321
322
323
    QPointer<QAction> m_diagnostics;
    QPointer<QAction> m_diagnosticsHighlight;
    QPointer<QAction> m_diagnosticsMark;
324
    QPointer<QAction> m_diagnosticsHover;
325
    QPointer<QAction> m_diagnosticsSwitch;
326
327
328
    QPointer<QAction> m_messages;
    QPointer<KSelectAction> m_messagesAutoSwitch;
    QPointer<QAction> m_messagesSwitch;
329
    QPointer<QAction> m_closeDynamic;
330
331
    QPointer<QAction> m_restartServer;
    QPointer<QAction> m_restartAll;
332
    QPointer<QAction> m_switchSourceHeader;
333

334
335
336
    // toolview
    QScopedPointer<QWidget> m_toolView;
    QPointer<QTabWidget> m_tabWidget;
337
    // applied search ranges
338
    typedef QMultiHash<KTextEditor::Document *, KTextEditor::MovingRange *> RangeCollection;
339
    RangeCollection m_ranges;
Christoph Cullmann's avatar
Christoph Cullmann committed
340
    QHash<KTextEditor::Document *, QHash<int, QVector<KTextEditor::MovingRange *>>> m_semanticHighlightRanges;
341
    // applied marks
342
    typedef QSet<KTextEditor::Document *> DocumentCollection;
343
    DocumentCollection m_marks;
344
345
346
347
    // 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;
348
349
350
    // 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
351
    QPointer<QTreeView> m_defTree;
352
    // ... and for declaration
353
    QPointer<QTreeView> m_declTree;
354

355
    // diagnostics tab
356
    QPointer<QTreeView> m_diagnosticsTree;
357
    // tree widget is either owned here or by tab
358
359
    QScopedPointer<QTreeView> m_diagnosticsTreeOwn;
    QScopedPointer<QStandardItemModel> m_diagnosticsModel;
360
361
    // diagnostics ranges
    RangeCollection m_diagnosticsRanges;
362
363
    // and marks
    DocumentCollection m_diagnosticsMarks;
364

365
366
367
368
369
370
    using MessagesWidget = QPlainTextEdit;
    // messages tab
    QPointer<MessagesWidget> m_messagesView;
    // widget is either owned here or by tab
    QScopedPointer<MessagesWidget> m_messagesViewOwn;

371
372
    // views on which completions have been registered
    QSet<KTextEditor::View *> m_completionViews;
373
374
375
376

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

377
378
379
380
381
    // outstanding request
    LSPClientServer::RequestHandle m_handle;
    // timeout on request
    bool m_req_timeout = false;

382
383
    // accept incoming applyEdit
    bool m_accept_edit = false;
384
385
    // characters to trigger format request
    QVector<QChar> m_onTypeFormattingTriggers;
386

387
    CtrlHoverFeedback m_ctrlHoverFeedback = {};
388

389
390
391
392
    KActionCollection *actionCollection() const
    {
        return m_client->actionCollection();
    }
393

394
395
396
397
    // inner class that forwards directly to method for convenience
    class ForwardingTextHintProvider : public KTextEditor::TextHintProvider
    {
        LSPClientActionView *m_parent;
Christoph Cullmann's avatar
Christoph Cullmann committed
398

399
400
401
402
403
404
405
406
407
408
409
410
411
    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);
        }
    };

412
public:
413
414
415
416
417
418
    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))
419
        , m_completion(LSPClientCompletion::new_(m_serverManager, plugin))
420
        , m_hover(LSPClientHover::new_(m_serverManager))
421
        , m_forwardHover(new ForwardingTextHintProvider(this))
422
        , m_symbolView(LSPClientSymbolView::new_(plugin, mainWin, m_serverManager))
423
424
    {
        connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::updateState);
425
426
        connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &self_type::handleEsc);
        connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, this, &self_type::updateState);
427
        connect(m_serverManager.data(), &LSPClientServerManager::showMessage, this, &self_type::onShowMessage);
428

429
        m_findDef = actionCollection()->addAction(QStringLiteral("lspclient_find_definition"), this, &self_type::goToDefinition);
430
        m_findDef->setText(i18n("Go to Definition"));
431
        m_findDecl = actionCollection()->addAction(QStringLiteral("lspclient_find_declaration"), this, &self_type::goToDeclaration);
432
        m_findDecl->setText(i18n("Go to Declaration"));
433
        m_findRef = actionCollection()->addAction(QStringLiteral("lspclient_find_references"), this, &self_type::findReferences);
434
        m_findRef->setText(i18n("Find References"));
435
436
        m_findImpl = actionCollection()->addAction(QStringLiteral("lspclient_find_implementations"), this, &self_type::findImplementation);
        m_findImpl->setText(i18n("Find Implementations"));
437
        m_triggerHighlight = actionCollection()->addAction(QStringLiteral("lspclient_highlight"), this, &self_type::highlight);
438
        m_triggerHighlight->setText(i18n("Highlight"));
439
        m_triggerSymbolInfo = actionCollection()->addAction(QStringLiteral("lspclient_symbol_info"), this, &self_type::symbolInfo);
Andrzej Dabrowski's avatar
Andrzej Dabrowski committed
440
        m_triggerSymbolInfo->setText(i18n("Symbol Info"));
441
        m_triggerFormat = actionCollection()->addAction(QStringLiteral("lspclient_format"), this, &self_type::format);
442
        m_triggerFormat->setText(i18n("Format"));
443
        m_triggerRename = actionCollection()->addAction(QStringLiteral("lspclient_rename"), this, &self_type::rename);
444
        m_triggerRename->setText(i18n("Rename"));
445
446
        m_switchSourceHeader = actionCollection()->addAction(QStringLiteral("lspclient_clangd_switchheader"), this, &self_type::clangdSwitchSourceHeader);
        m_switchSourceHeader->setText(i18n("Switch Source Header"));
447

448
        // general options
449
        m_complDocOn = actionCollection()->addAction(QStringLiteral("lspclient_completion_doc"), this, &self_type::displayOptionChanged);
450
451
        m_complDocOn->setText(i18n("Show selected completion documentation"));
        m_complDocOn->setCheckable(true);
452
        m_refDeclaration = actionCollection()->addAction(QStringLiteral("lspclient_references_declaration"), this, &self_type::displayOptionChanged);
453
454
        m_refDeclaration->setText(i18n("Include declaration in references"));
        m_refDeclaration->setCheckable(true);
455
        m_autoHover = actionCollection()->addAction(QStringLiteral("lspclient_auto_hover"), this, &self_type::displayOptionChanged);
456
457
        m_autoHover->setText(i18n("Show hover information"));
        m_autoHover->setCheckable(true);
458
        m_onTypeFormatting = actionCollection()->addAction(QStringLiteral("lspclient_type_formatting"), this, &self_type::displayOptionChanged);
459
460
        m_onTypeFormatting->setText(i18n("Format on typing"));
        m_onTypeFormatting->setCheckable(true);
461
        m_incrementalSync = actionCollection()->addAction(QStringLiteral("lspclient_incremental_sync"), this, &self_type::displayOptionChanged);
462
463
        m_incrementalSync->setText(i18n("Incremental document synchronization"));
        m_incrementalSync->setCheckable(true);
464

465
        // diagnostics
466
        m_diagnostics = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics"), this, &self_type::displayOptionChanged);
467
468
        m_diagnostics->setText(i18n("Show diagnostics notifications"));
        m_diagnostics->setCheckable(true);
469
        m_diagnosticsHighlight = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_highlight"), this, &self_type::displayOptionChanged);
470
471
        m_diagnosticsHighlight->setText(i18n("Show diagnostics highlights"));
        m_diagnosticsHighlight->setCheckable(true);
472
        m_diagnosticsMark = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_mark"), this, &self_type::displayOptionChanged);
473
474
        m_diagnosticsMark->setText(i18n("Show diagnostics marks"));
        m_diagnosticsMark->setCheckable(true);
475
476
477
        m_diagnosticsHover = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_hover"), this, &self_type::displayOptionChanged);
        m_diagnosticsHover->setText(i18n("Show diagnostics on hover"));
        m_diagnosticsHover->setCheckable(true);
478
        m_diagnosticsSwitch = actionCollection()->addAction(QStringLiteral("lspclient_diagnostic_switch"), this, &self_type::switchToDiagnostics);
479
        m_diagnosticsSwitch->setText(i18n("Switch to diagnostics tab"));
480

481
482
483
484
485
486
        // 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);
Alexander Lohnau's avatar
Alexander Lohnau committed
487
488
489
490
491
        const QStringList list{i18nc("@info", "Never"),
                               i18nc("@info", "Error"),
                               i18nc("@info", "Warning"),
                               i18nc("@info", "Information"),
                               i18nc("@info", "Log")};
492
493
494
495
        m_messagesAutoSwitch->setItems(list);
        m_messagesSwitch = actionCollection()->addAction(QStringLiteral("lspclient_messages_switch"), this, &self_type::switchToMessages);
        m_messagesSwitch->setText(i18n("Switch to messages tab"));

496
497
498
        // 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"));
499
        m_restartServer = actionCollection()->addAction(QStringLiteral("lspclient_restart_server"), this, &self_type::restartCurrent);
500
        m_restartServer->setText(i18n("Restart LSP Server"));
501
        m_restartAll = actionCollection()->addAction(QStringLiteral("lspclient_restart_all"), this, &self_type::restartAll);
502
503
        m_restartAll->setText(i18n("Restart All LSP Servers"));

504
505
506
        // popup menu
        auto menu = new KActionMenu(i18n("LSP Client"), this);
        actionCollection()->addAction(QStringLiteral("popup_lspclient"), menu);
507
508
509
        menu->addAction(m_findDef);
        menu->addAction(m_findDecl);
        menu->addAction(m_findRef);
510
        menu->addAction(m_findImpl);
511
        menu->addAction(m_switchSourceHeader);
512
        menu->addAction(m_triggerHighlight);
513
        menu->addAction(m_triggerSymbolInfo);
514
        menu->addAction(m_triggerFormat);
515
        menu->addAction(m_triggerRename);
516
        menu->addSeparator();
517
        menu->addAction(m_diagnosticsSwitch);
518
        menu->addAction(m_messagesSwitch);
519
        menu->addAction(m_closeDynamic);
520
        menu->addSeparator();
521
522
        menu->addAction(m_restartServer);
        menu->addAction(m_restartAll);
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
        menu->addSeparator();
        // more options
        auto moreOptions = new KActionMenu(i18n("More options"), this);
        menu->addAction(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);
540
541
542

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

544
        // toolview
Alexander Lohnau's avatar
Alexander Lohnau committed
545
546
547
548
549
        m_toolView.reset(mainWin->createToolView(plugin,
                                                 QStringLiteral("kate_lspclient"),
                                                 KTextEditor::MainWindow::Bottom,
                                                 QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")),
                                                 i18n("LSP Client")));
550
        m_tabWidget = new QTabWidget(m_toolView.data());
551
        m_toolView->layout()->addWidget(m_tabWidget);
552
        m_tabWidget->setFocusPolicy(Qt::NoFocus);
553
554
555
        m_tabWidget->setTabsClosable(true);
        KAcceleratorManager::setNoAccel(m_tabWidget);
        connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &self_type::tabCloseRequested);
556
        connect(m_tabWidget, &QTabWidget::currentChanged, this, &self_type::tabChanged);
557

558
        // diagnostics tab
559
        m_diagnosticsTree = new QTreeView();
560
        m_diagnosticsTree->setAlternatingRowColors(true);
561
        m_diagnosticsTreeOwn.reset(m_diagnosticsTree);
562
        m_diagnosticsModel.reset(new QStandardItemModel());
563
        m_diagnosticsModel->setColumnCount(1);
564
        m_diagnosticsTree->setModel(m_diagnosticsModel.data());
565
        configureTreeView(m_diagnosticsTree);
566
567
        connect(m_diagnosticsTree, &QTreeView::clicked, this, &self_type::goToItemLocation);
        connect(m_diagnosticsTree, &QTreeView::doubleClicked, this, &self_type::triggerCodeAction);
568

569
570
571
572
573
574
        // messages tab
        m_messagesView = new QPlainTextEdit();
        m_messagesView->setMaximumBlockCount(100);
        m_messagesView->setReadOnly(true);
        m_messagesViewOwn.reset(m_messagesView);

575
576
        // track position in view to sync diagnostics list
        m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 0, 500));
577
        connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState);
578

579
580
581
582
        connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, &LSPClientActionView::onViewCreated);

        connect(this, &LSPClientActionView::ctrlClickDefRecieved, this, &LSPClientActionView::onCtrlMouseMove);

583
        configUpdated();
584
585
586
        updateState();
    }

587
588
589
590
591
592
    void onViewCreated(KTextEditor::View *view)
    {
        if (view) {
            view->installEventFilter(this);
            auto childs = view->children();
            for (auto c : childs) {
593
                if (c) {
594
                    c->installEventFilter(this);
595
                }
596
597
598
599
            }
        }
    }

600
    // This is taken from KDevelop :)
Alexander Lohnau's avatar
Alexander Lohnau committed
601
    KTextEditor::View *viewFromWidget(QWidget *widget)
602
    {
603
        if (!widget) {
604
            return nullptr;
605
        }
Alexander Lohnau's avatar
Alexander Lohnau committed
606
        auto *view = qobject_cast<KTextEditor::View *>(widget);
607
        if (view) {
608
            return view;
609
        } else {
610
            return viewFromWidget(widget->parentWidget());
611
        }
612
613
    }

614
615
616
617
618
619
    /**
     * @brief normal word range queried from doc->wordRangeAt() will not include
     * full header from a #include line. For example in line "#include <abc/some>"
     * we get either abc or some. But for Ctrl + click we need to highlight it as
     * one thing, so this function expands the range from wordRangeAt() to do that.
     */
Alexander Lohnau's avatar
Alexander Lohnau committed
620
    static void expandToFullHeaderRange(KTextEditor::Range &range, QStringView lineText)
621
622
623
624
625
    {
        auto expandRangeTo = [lineText, &range](QChar c, int startPos) {
            int end = lineText.indexOf(c, startPos);
            if (end > -1) {
                auto startC = range.start();
626
                startC.setColumn(startPos);
627
628
629
630
631
632
633
634
635
                auto endC = range.end();
                endC.setColumn(end);
                range.setStart(startC);
                range.setEnd(endC);
            }
        };

        int angleBracketPos = lineText.indexOf(QLatin1Char('<'), 7);
        if (angleBracketPos > -1) {
636
            expandRangeTo(QLatin1Char('>'), angleBracketPos + 1);
637
638
639
        } else {
            int startPos = lineText.indexOf(QLatin1Char('"'), 7);
            if (startPos > -1) {
640
                expandRangeTo(QLatin1Char('"'), startPos + 1);
641
642
643
644
            }
        }
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
645
646
647
    bool eventFilter(QObject *obj, QEvent *event) override
    {
        auto mouseEvent = dynamic_cast<QMouseEvent *>(event);
648

649
        // we are only concerned with mouse events for now :)
650
        if (!mouseEvent) {
651
            return false;
652
        }
653
654

        // common stuff that we need for both events
Alexander Lohnau's avatar
Alexander Lohnau committed
655
        auto wid = qobject_cast<QWidget *>(obj);
656
        auto v = viewFromWidget(wid);
657
        if (!v) {
658
            return false;
659
        }
660
661

        const auto coords = wid->mapTo(v, mouseEvent->pos());
Alexander Lohnau's avatar
Alexander Lohnau committed
662
        const auto cur = v->coordinatesToCursor(coords);
663
        // there isn't much we can do now, just bail out
664
        if (!cur.isValid()) {
665
            return false;
666
        }
667

668
        auto doc = v->document();
669
        if (!doc) {
670
            return false;
671
        }
672
673
674

        const auto word = doc->wordAt(cur);

675
676
677
678
679
680
681
682
        // The user pressed Ctrl + Click
        if (event->type() == QEvent::MouseButtonPress) {
            if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() == Qt::ControlModifier) {
                // must set cursor else we will be jumping somewhere else!!
                v->setCursorPosition(cur);
                if (!word.isEmpty()) {
                    m_ctrlHoverFeedback.clear(m_mainWindow->activeView());
                    goToDefinition();
683
684
                }
            }
685
686
687
688
        }
        // The user is hovering with Ctrl pressed
        else if (event->type() == QEvent::MouseMove) {
            if (mouseEvent->modifiers() == Qt::ControlModifier) {
689
                auto range = doc->wordRangeAt(cur);
690
                if (!word.isEmpty() && range.isValid()) {
691
692
693
694
695
696
697
                    // check if we are in #include
                    // and expand the word range
                    auto lineText = doc->line(range.start().line());
                    if (lineText.startsWith(QLatin1String("#include")) && range.start().column() > 7) {
                        expandToFullHeaderRange(range, lineText);
                    }

698
699
700
701
702
                    m_ctrlHoverFeedback.setRangeAndWidget(range, wid);
                    // this will not go anywhere actually, but just signal whether we have a definition
                    // Also, please rethink very hard if you are going to reuse this method. It's made
                    // only for Ctrl+Hover
                    processCtrlMouseHover(cur);
703
                } else {
704
                    // if there is no word, unset the cursor and remove the highlight
705
                    if (wid) {
706
                        wid->unsetCursor();
707
                    }
708
                    m_ctrlHoverFeedback.clear(m_mainWindow->activeView());
709
                }
710
711
712
            } else {
                // simple mouse move, make sure to unset the cursor
                // and remove the highlight
713
                if (wid) {
714
                    wid->unsetCursor();
715
                }
716
                m_ctrlHoverFeedback.clear(m_mainWindow->activeView());
717
718
719
720
721
722
            }
        }

        return false;
    }

Filip Gawin's avatar
Filip Gawin committed
723
    ~LSPClientActionView() override
724
    {
725
726
        // unregister all code-completion providers, else we might crash
        for (auto view : qAsConst(m_completionViews)) {
727
            qobject_cast<KTextEditor::CodeCompletionInterface *>(view)->unregisterCompletionModel(m_completion.data());
728
729
730
731
        }

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

735
736
        clearAllLocationMarks();
        clearAllDiagnosticsMarks();
737
738
    }

739
740
741
742
743
744
745
    void configureTreeView(QTreeView *treeView)
    {
        treeView->setHeaderHidden(true);
        treeView->setFocusPolicy(Qt::NoFocus);
        treeView->setLayoutDirection(Qt::LeftToRight);
        treeView->setSortingEnabled(false);
        treeView->setEditTriggers(QAbstractItemView::NoEditTriggers);
746
747
748
749
750
751

        // 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);
Alexander Lohnau's avatar
Alexander Lohnau committed
752
753
754
        auto h = [menu](const QPoint &) {
            menu->popup(QCursor::pos());
        };
755
        connect(treeView, &QTreeView::customContextMenuRequested, h);
756
757
    }

758
759
    void displayOptionChanged()
    {
760
761
        m_diagnosticsHighlight->setEnabled(m_diagnostics->isChecked());
        m_diagnosticsMark->setEnabled(m_diagnostics->isChecked());
762
        m_diagnosticsHover->setEnabled(m_diagnostics->isChecked());
763
764
765
766
767
768
769
770
771
772
773
774
775
        // 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);
776
777
778
        // setTabEnabled may still show it ... so let's be more forceful
        if (m_diagnostics->isChecked() && m_diagnosticsTreeOwn) {
            m_diagnosticsTreeOwn.take();
779
            m_tabWidget->insertTab(messagesIndex + 1, m_diagnosticsTree, i18nc("@title:tab", "Diagnostics"));
780
781
        } else if (!m_diagnostics->isChecked() && !m_diagnosticsTreeOwn) {
            m_diagnosticsTreeOwn.reset(m_diagnosticsTree);
782
            m_tabWidget->removeTab(diagnosticsIndex);
783
        }
784
        m_diagnosticsSwitch->setEnabled(m_diagnostics->isChecked());
785
        m_serverManager->setIncrementalSync(m_incrementalSync->isChecked());
786
        updateState();
787
788
789
790
    }

    void configUpdated()
    {
791
        if (m_complDocOn) {
792
            m_complDocOn->setChecked(m_plugin->m_complDoc);
793
794
        }
        if (m_refDeclaration) {
795
            m_refDeclaration->setChecked(m_plugin->m_refDeclaration);
796
797
        }
        if (m_autoHover) {
798
            m_autoHover->setChecked(m_plugin->m_autoHover);
799
800
        }
        if (m_onTypeFormatting) {
801
            m_onTypeFormatting->setChecked(m_plugin->m_onTypeFormatting);
802
803
        }
        if (m_incrementalSync) {
804
            m_incrementalSync->setChecked(m_plugin->m_incrementalSync);
805
806
        }
        if (m_diagnostics) {
807
            m_diagnostics->setChecked(m_plugin->m_diagnostics);
808
809
        }
        if (m_diagnosticsHighlight) {
810
            m_diagnosticsHighlight->setChecked(m_plugin->m_diagnosticsHighlight);
811
812
        }
        if (m_diagnosticsMark) {
813
            m_diagnosticsMark->setChecked(m_plugin->m_diagnosticsMark);
814
815
        }
        if (m_diagnosticsHover) {
816
            m_diagnosticsHover->setChecked(m_plugin->m_diagnosticsHover);
817
818
        }
        if (m_messages) {
819
            m_messages->setChecked(m_plugin->m_messages);
820
821
        }
        if (m_messagesAutoSwitch) {
822
            m_messagesAutoSwitch->setCurrentItem(m_plugin->m_messagesAutoSwitch);
823
        }
824
825
826
        displayOptionChanged();
    }

827
828
829
830
    void restartCurrent()
    {
        KTextEditor::View *activeView = m_mainWindow->activeView();
        auto server = m_serverManager->findServer(activeView);
831
        if (server) {
832
            m_serverManager->restart(server.data());
833
        }
834
835
    }

836
837
838
839
    void restartAll()
    {
        m_serverManager->restart(nullptr);
    }
840

841
    static void clearMarks(KTextEditor::Document *doc, RangeCollection &ranges, DocumentCollection &docs, uint markType)
842
    {
843
        KTextEditor::MarkInterface *iface = docs.contains(doc) ? qobject_cast<KTextEditor::MarkInterface *>(doc) : nullptr;
844
        if (iface) {
845
846
            const QHash<int, KTextEditor::Mark *> marks = iface->marks();
            QHashIterator<int, KTextEditor::Mark *> i(marks);
847
848
            while (i.hasNext()) {
                i.next();
849
850
                if (i.value()->type & markType) {
                    iface->removeMark(i.value()->line, markType);