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

    SPDX-License-Identifier: MIT
5
*/
6
7
8
9
10
11
12
13
14
15
16
17
18

#include "lspclientcompletion.h"
#include "lspclientplugin.h"

#include "lspclient_debug.h"

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

#include <QIcon>
#include <QUrl>

19
#include <algorithm>
Filip Gawin's avatar
Filip Gawin committed
20
#include <utility>
21

22
23
24
25
26
// clang-format off
#define RETURN_CACHED_ICON(name) \
    { \
        static QIcon icon(QIcon::fromTheme(QStringLiteral(name))); \
        return icon; \
27
    }
28
// clang-format on
29

30
static QIcon kind_icon(LSPCompletionItemKind kind)
31
{
32
    switch (kind) {
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    case LSPCompletionItemKind::Method:
    case LSPCompletionItemKind::Function:
    case LSPCompletionItemKind::Constructor:
        RETURN_CACHED_ICON("code-function")
    case LSPCompletionItemKind::Variable:
        RETURN_CACHED_ICON("code-variable")
    case LSPCompletionItemKind::Class:
    case LSPCompletionItemKind::Interface:
    case LSPCompletionItemKind::Struct:
        RETURN_CACHED_ICON("code-class");
    case LSPCompletionItemKind::Module:
        RETURN_CACHED_ICON("code-block");
    case LSPCompletionItemKind::Field:
    case LSPCompletionItemKind::Property:
        // align with symbolview
        RETURN_CACHED_ICON("code-variable");
    case LSPCompletionItemKind::Enum:
    case LSPCompletionItemKind::EnumMember:
        RETURN_CACHED_ICON("enum");
    default:
        break;
54
55
56
57
    }
    return QIcon();
}

58
static KTextEditor::CodeCompletionModel::CompletionProperty kind_property(LSPCompletionItemKind kind)
59
60
61
62
{
    using CompletionProperty = KTextEditor::CodeCompletionModel::CompletionProperty;
    auto p = CompletionProperty::NoProperty;

63
    switch (kind) {
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
    case LSPCompletionItemKind::Method:
    case LSPCompletionItemKind::Function:
    case LSPCompletionItemKind::Constructor:
        p = CompletionProperty::Function;
        break;
    case LSPCompletionItemKind::Variable:
        p = CompletionProperty::Variable;
        break;
    case LSPCompletionItemKind::Class:
    case LSPCompletionItemKind::Interface:
        p = CompletionProperty::Class;
        break;
    case LSPCompletionItemKind::Struct:
        p = CompletionProperty::Class;
        break;
    case LSPCompletionItemKind::Module:
        p = CompletionProperty::Namespace;
        break;
    case LSPCompletionItemKind::Enum:
    case LSPCompletionItemKind::EnumMember:
        p = CompletionProperty::Enum;
        break;
    default:
        break;
88
89
90
91
    }
    return p;
}

92
struct LSPClientCompletionItem : public LSPCompletionItem {
93
94
95
    int argumentHintDepth = 0;
    QString prefix;
    QString postfix;
96
97
    int start = 0;
    int len = 0;
98

99
100
    LSPClientCompletionItem(const LSPCompletionItem &item)
        : LSPCompletionItem(item)
101
102
103
104
    {
        // transform for later display
        // sigh, remove (leading) whitespace (looking at clangd here)
        // could skip the [] if empty detail, but it is a handy watermark anyway ;-)
105
        label = QString(label.simplified() + QLatin1String(" [") + detail.simplified() + QStringLiteral("]"));
106
107
    }

108
    LSPClientCompletionItem(const LSPSignatureInformation &sig, int activeParameter, const QString &_sortText)
109
110
111
112
113
    {
        argumentHintDepth = 1;
        documentation = sig.documentation;
        label = sig.label;
        sortText = _sortText;
114

115
116
        // transform into prefix, name, suffix if active
        if (activeParameter >= 0 && activeParameter < sig.parameters.length()) {
117
            const auto &param = sig.parameters.at(activeParameter);
118
            if (param.start >= 0 && param.start < label.length() && param.end >= 0 && param.end < label.length() && param.start < param.end) {
119
120
                start = param.start;
                len = param.end - param.start;
121
122
123
124
125
126
127
128
                prefix = label.mid(0, param.start);
                postfix = label.mid(param.end);
                label = label.mid(param.start, param.end - param.start);
            }
        }
    }
};

129
static bool compare_match(const LSPCompletionItem &a, const LSPCompletionItem &b)
130
131
132
{
    return a.sortText < b.sortText;
}
133
134
135
136
137
138
139
140
141

class LSPClientCompletionImpl : public LSPClientCompletion
{
    Q_OBJECT

    typedef LSPClientCompletionImpl self_type;

    QSharedPointer<LSPClientServerManager> m_manager;
    QSharedPointer<LSPClientServer> m_server;
142
    bool m_selectedDocumentation = false;
143
    bool m_signatureHelp = true;
144
    bool m_complParens = true;
145

146
147
148
    QVector<QChar> m_triggersCompletion;
    QVector<QChar> m_triggersSignature;
    bool m_triggerSignature = false;
149

150
151
    QList<LSPClientCompletionItem> m_matches;
    LSPClientServer::RequestHandle m_handle, m_handleSig;
152
153

public:
154
    LSPClientCompletionImpl(QSharedPointer<LSPClientServerManager> manager)
155
156
157
        : LSPClientCompletion(nullptr)
        , m_manager(std::move(manager))
        , m_server(nullptr)
158
159
160
161
    {
    }

    void setServer(QSharedPointer<LSPClientServer> server) override
162
163
164
    {
        m_server = server;
        if (m_server) {
165
            const auto &caps = m_server->capabilities();
166
167
168
169
170
171
172
            m_triggersCompletion = caps.completionProvider.triggerCharacters;
            m_triggersSignature = caps.signatureHelpProvider.triggerCharacters;
        } else {
            m_triggersCompletion.clear();
            m_triggersSignature.clear();
        }
    }
173

174
175
176
177
    void setSelectedDocumentation(bool s) override
    {
        m_selectedDocumentation = s;
    }
178

179
180
181
182
183
    void setSignatureHelp(bool s) override
    {
        m_signatureHelp = s;
    }

184
185
186
187
188
    void setCompleteParens(bool s) override
    {
        m_complParens = s;
    }

189
190
191
192
193
194
195
196
    QVariant data(const QModelIndex &index, int role) const override
    {
        if (!index.isValid() || index.row() >= m_matches.size()) {
            return QVariant();
        }

        const auto &match = m_matches.at(index.row());

197
198
199
200
201
202
203
204
        if (role == Qt::DisplayRole) {
            if (index.column() == KTextEditor::CodeCompletionModel::Name) {
                return match.label;
            } else if (index.column() == KTextEditor::CodeCompletionModel::Prefix) {
                return match.prefix;
            } else if (index.column() == KTextEditor::CodeCompletionModel::Postfix) {
                return match.postfix;
            }
205
        } else if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) {
206
207
208
209
            return kind_icon(match.kind);
        } else if (role == KTextEditor::CodeCompletionModel::CompletionRole) {
            return kind_property(match.kind);
        } else if (role == KTextEditor::CodeCompletionModel::ArgumentHintDepth) {
210
            return match.argumentHintDepth;
211
212
213
        } else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) {
            // (ab)use depth to indicate sort order
            return index.row();
214
215
        } else if (role == KTextEditor::CodeCompletionModel::IsExpandable) {
            return !match.documentation.value.isEmpty();
216
        } else if (role == KTextEditor::CodeCompletionModel::ExpandingWidget && !match.documentation.value.isEmpty()) {
217
218
219
            // probably plaintext, but let's show markdown as-is for now
            // FIXME better presentation of markdown
            return match.documentation.value;
Alexander Lohnau's avatar
Alexander Lohnau committed
220
221
        } else if (role == KTextEditor::CodeCompletionModel::ItemSelected && !match.argumentHintDepth && !match.documentation.value.isEmpty()
                   && m_selectedDocumentation) {
222
            return match.documentation.value;
223
224
225
226
227
228
229
230
231
232
233
234
235
        } else if (role == KTextEditor::CodeCompletionModel::CustomHighlight && match.argumentHintDepth > 0) {
            if (index.column() != Name || match.len == 0)
                return {};
            QTextCharFormat boldFormat;
            boldFormat.setFontWeight(QFont::Bold);
            const QList<QVariant> highlighting{
                QVariant(0),
                QVariant(match.len),
                boldFormat,
            };
            return highlighting;
        } else if (role == CodeCompletionModel::HighlightingMethod && match.argumentHintDepth > 0) {
            return QVariant(HighlightMethod::CustomHighlighting);
236
237
238
239
240
        }

        return QVariant();
    }

241
    bool shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position) override
242
    {
243
        qCInfo(LSPCLIENT) << "should start " << userInsertion << insertedText;
244

245
        if (!userInsertion || !m_server || insertedText.isEmpty()) {
246
247
248
            return false;
        }

249
        // covers most already ...
250
        bool complete = CodeCompletionModelControllerInterface::shouldStartCompletion(view, insertedText, userInsertion, position);
251
        QChar lastChar = insertedText.at(insertedText.count() - 1);
252

253
        m_triggerSignature = false;
254
        complete = complete || m_triggersCompletion.contains(lastChar);
255
256
257
258
        if (m_triggersSignature.contains(lastChar)) {
            complete = true;
            m_triggerSignature = true;
        }
259
260
261
262

        return complete;
    }

263
    void completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it) override
264
265
266
267
268
    {
        Q_UNUSED(it)

        qCInfo(LSPCLIENT) << "completion invoked" << m_server;

269
270
        // maybe use WaitForReset ??
        // but more complex and already looks good anyway
Christoph Cullmann's avatar
Christoph Cullmann committed
271
        auto handler = [this](const QList<LSPCompletionItem> & compl ) {
272
            beginResetModel();
Christoph Cullmann's avatar
Christoph Cullmann committed
273
            qCInfo(LSPCLIENT) << "adding completions " << compl .size();
274
            for (const auto &item : compl ) {
275
                m_matches.push_back(item);
276
            }
277
            std::stable_sort(m_matches.begin(), m_matches.end(), compare_match);
278
279
280
281
            setRowCount(m_matches.size());
            endResetModel();
        };

282
        auto sigHandler = [this](const LSPSignatureHelp &sig) {
283
            beginResetModel();
284
285
            qCInfo(LSPCLIENT) << "adding signatures " << sig.signatures.size();
            int index = 0;
286
            for (const auto &item : sig.signatures) {
287
288
289
290
291
292
293
                int sortIndex = 10 + index;
                int active = -1;
                if (index == sig.activeSignature) {
                    sortIndex = 0;
                    active = sig.activeParameter;
                }
                // trick active first, others after that
294
                m_matches.push_back({item, active, QString(QStringLiteral("%1").arg(sortIndex, 3, 10))});
295
296
                ++index;
            }
297
            std::stable_sort(m_matches.begin(), m_matches.end(), compare_match);
298
299
300
301
302
303
304
305
306
307
308
309
310
            setRowCount(m_matches.size());
            endResetModel();
        };

        beginResetModel();
        m_matches.clear();
        auto document = view->document();
        if (m_server && document) {
            // the default range is determined based on a reasonable identifier (word)
            // which is generally fine and nice, but let's pass actual cursor position
            // (which may be within this typical range)
            auto position = view->cursorPosition();
            auto cursor = qMax(range.start(), qMin(range.end(), position));
311
            m_manager->update(document, false);
312
            if (!m_triggerSignature) {
313
                m_handle = m_server->documentCompletion(document->url(), {cursor.line(), cursor.column()}, this, handler);
314
            }
315
            if (m_signatureHelp) {
316
317
                m_handleSig = m_server->signatureHelp(document->url(), {cursor.line(), cursor.column()}, this, sigHandler);
            }
318
319
320
321
322
        }
        setRowCount(m_matches.size());
        endResetModel();
    }

323
324
325
    /**
     * @brief return next char *after* the range
     */
Alexander Lohnau's avatar
Alexander Lohnau committed
326
    static QChar peekNextChar(KTextEditor::Document *doc, const KTextEditor::Range &range)
327
    {
328
        return doc->characterAt(KTextEditor::Cursor(range.end().line(), range.end().column()));
329
330
    }

331
332
333
334
335
    static bool isFunctionKind(LSPCompletionItemKind k)
    {
        return k == LSPCompletionItemKind::Function || k == LSPCompletionItemKind::Method;
    }

336
    void executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const override
337
    {
338
        if (index.row() < m_matches.size()) {
339
340
            QChar next = peekNextChar(view->document(), word);
            QString matching = m_matches.at(index.row()).insertText;
341
342
            // if there is already a '"' or >, remove it, this happens with #include "xx.h"
            if ((next == QLatin1Char('"') && matching.endsWith(QLatin1Char('"'))) || (next == QLatin1Char('>') && matching.endsWith(QLatin1Char('>')))) {
343
344
                matching.chop(1);
            }
345
346
347

            // This is a function
            const auto &m = m_matches.at(index.row());
348
            // add parentheses if function and guestimated meaningful for language in question
349
            // this covers at least the common cases such as clangd, python, etc
350
351
            // also no need to add one if the next char is already
            bool addParens = m_complParens && next != QLatin1Char('(') && isFunctionKind(m.kind) && m_triggersSignature.contains(QLatin1Char('('));
352
            if (addParens) {
353
354
355
                matching += QStringLiteral("()");
            }

356
            view->document()->replaceText(word, matching);
357

358
            if (addParens) {
359
360
361
                // place the cursor in between (|)
                view->setCursorPosition({view->cursorPosition().line(), view->cursorPosition().column() - 1});
            }
362
        }
363
364
365
366
367
368
369
370
    }

    void aborted(KTextEditor::View *view) override
    {
        Q_UNUSED(view);
        beginResetModel();
        m_matches.clear();
        m_handle.cancel();
371
372
        m_handleSig.cancel();
        m_triggerSignature = false;
373
374
375
376
        endResetModel();
    }
};

377
LSPClientCompletion *LSPClientCompletion::new_(QSharedPointer<LSPClientServerManager> manager)
378
{
379
    return new LSPClientCompletionImpl(std::move(manager));
380
381
382
}

#include "lspclientcompletion.moc"