lspclientcompletion.cpp 11.7 KB
Newer Older
1
2
/*  SPDX-License-Identifier: MIT

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

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

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

#include "lspclient_debug.h"

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

#include <QIcon>
#include <QUrl>

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

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

31
static QIcon kind_icon(LSPCompletionItemKind kind)
32
{
33
    switch (kind) {
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    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;
55
56
57
58
    }
    return QIcon();
}

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

64
    switch (kind) {
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
    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;
89
90
91
92
    }
    return p;
}

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

98
99
    LSPClientCompletionItem(const LSPCompletionItem &item)
        : LSPCompletionItem(item)
100
101
102
103
    {
        // 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 ;-)
104
        label = QString(label.simplified() + QLatin1String(" [") + detail.simplified() + QStringLiteral("]"));
105
106
    }

107
    LSPClientCompletionItem(const LSPSignatureInformation &sig, int activeParameter, const QString &_sortText)
108
109
110
111
112
113
114
    {
        argumentHintDepth = 1;
        documentation = sig.documentation;
        label = sig.label;
        sortText = _sortText;
        // transform into prefix, name, suffix if active
        if (activeParameter >= 0 && activeParameter < sig.parameters.length()) {
115
            const auto &param = sig.parameters.at(activeParameter);
116
            if (param.start >= 0 && param.start < label.length() && param.end >= 0 && param.end < label.length() && param.start < param.end) {
117
118
119
120
121
122
123
124
                prefix = label.mid(0, param.start);
                postfix = label.mid(param.end);
                label = label.mid(param.start, param.end - param.start);
            }
        }
    }
};

125
static bool compare_match(const LSPCompletionItem &a, const LSPCompletionItem &b)
126
127
128
{
    return a.sortText < b.sortText;
}
129
130
131
132
133
134
135
136
137

class LSPClientCompletionImpl : public LSPClientCompletion
{
    Q_OBJECT

    typedef LSPClientCompletionImpl self_type;

    QSharedPointer<LSPClientServerManager> m_manager;
    QSharedPointer<LSPClientServer> m_server;
138
139
    bool m_selectedDocumentation = false;

140
141
142
    QVector<QChar> m_triggersCompletion;
    QVector<QChar> m_triggersSignature;
    bool m_triggerSignature = false;
143

144
145
    QList<LSPClientCompletionItem> m_matches;
    LSPClientServer::RequestHandle m_handle, m_handleSig;
146
147
148

public:
    LSPClientCompletionImpl(QSharedPointer<LSPClientServerManager> manager)
149
150
151
        : LSPClientCompletion(nullptr)
        , m_manager(std::move(manager))
        , m_server(nullptr)
152
153
154
155
    {
    }

    void setServer(QSharedPointer<LSPClientServer> server) override
156
157
158
    {
        m_server = server;
        if (m_server) {
159
            const auto &caps = m_server->capabilities();
160
161
162
163
164
165
166
            m_triggersCompletion = caps.completionProvider.triggerCharacters;
            m_triggersSignature = caps.signatureHelpProvider.triggerCharacters;
        } else {
            m_triggersCompletion.clear();
            m_triggersSignature.clear();
        }
    }
167

168
169
170
171
    void setSelectedDocumentation(bool s) override
    {
        m_selectedDocumentation = s;
    }
172

173
174
175
176
177
178
179
180
    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());

181
182
183
184
185
186
187
188
        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;
            }
189
        } else if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) {
190
191
192
193
            return kind_icon(match.kind);
        } else if (role == KTextEditor::CodeCompletionModel::CompletionRole) {
            return kind_property(match.kind);
        } else if (role == KTextEditor::CodeCompletionModel::ArgumentHintDepth) {
194
            return match.argumentHintDepth;
195
196
197
        } else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) {
            // (ab)use depth to indicate sort order
            return index.row();
198
199
        } else if (role == KTextEditor::CodeCompletionModel::IsExpandable) {
            return !match.documentation.value.isEmpty();
200
        } else if (role == KTextEditor::CodeCompletionModel::ExpandingWidget && !match.documentation.value.isEmpty()) {
201
202
203
            // 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
204
205
        } else if (role == KTextEditor::CodeCompletionModel::ItemSelected && !match.argumentHintDepth && !match.documentation.value.isEmpty()
                   && m_selectedDocumentation) {
206
            return match.documentation.value;
207
208
209
210
211
        }

        return QVariant();
    }

212
    bool shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position) override
213
    {
214
        qCInfo(LSPCLIENT) << "should start " << userInsertion << insertedText;
215

216
        if (!userInsertion || !m_server || insertedText.isEmpty()) {
217
218
219
            return false;
        }

220
        // covers most already ...
221
        bool complete = CodeCompletionModelControllerInterface::shouldStartCompletion(view, insertedText, userInsertion, position);
222
        QChar lastChar = insertedText.at(insertedText.count() - 1);
223

224
        m_triggerSignature = false;
225
        complete = complete || m_triggersCompletion.contains(lastChar);
226
227
228
229
        if (m_triggersSignature.contains(lastChar)) {
            complete = true;
            m_triggerSignature = true;
        }
230
231
232
233

        return complete;
    }

234
    void completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it) override
235
236
237
238
239
    {
        Q_UNUSED(it)

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

240
241
        // maybe use WaitForReset ??
        // but more complex and already looks good anyway
Christoph Cullmann's avatar
Christoph Cullmann committed
242
        auto handler = [this](const QList<LSPCompletionItem> & compl ) {
243
            beginResetModel();
Christoph Cullmann's avatar
Christoph Cullmann committed
244
245
            qCInfo(LSPCLIENT) << "adding completions " << compl .size();
            for (const auto &item : compl )
246
                m_matches.push_back(item);
247
            std::stable_sort(m_matches.begin(), m_matches.end(), compare_match);
248
249
250
251
            setRowCount(m_matches.size());
            endResetModel();
        };

252
        auto sigHandler = [this](const LSPSignatureHelp &sig) {
253
            beginResetModel();
254
255
            qCInfo(LSPCLIENT) << "adding signatures " << sig.signatures.size();
            int index = 0;
256
            for (const auto &item : sig.signatures) {
257
258
259
260
261
262
263
                int sortIndex = 10 + index;
                int active = -1;
                if (index == sig.activeSignature) {
                    sortIndex = 0;
                    active = sig.activeParameter;
                }
                // trick active first, others after that
264
                m_matches.push_back({item, active, QString(QStringLiteral("%1").arg(sortIndex, 3, 10))});
265
266
                ++index;
            }
267
            std::stable_sort(m_matches.begin(), m_matches.end(), compare_match);
268
269
270
271
272
273
274
275
276
277
278
279
280
            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));
281
            m_manager->update(document, false);
282
            if (!m_triggerSignature) {
283
                m_handle = m_server->documentCompletion(document->url(), {cursor.line(), cursor.column()}, this, handler);
284
            }
285
            m_handleSig = m_server->signatureHelp(document->url(), {cursor.line(), cursor.column()}, this, sigHandler);
286
287
288
289
290
        }
        setRowCount(m_matches.size());
        endResetModel();
    }

291
292
293
    /**
     * @brief return next char *after* the range
     */
Alexander Lohnau's avatar
Alexander Lohnau committed
294
    static QChar peekNextChar(KTextEditor::Document *doc, const KTextEditor::Range &range)
295
    {
296
        return doc->characterAt(KTextEditor::Cursor(range.end().line(), range.end().column()));
297
298
    }

299
    void executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const override
300
    {
301
302
303
        if (index.row() < m_matches.size()) {
            auto next = peekNextChar(view->document(), word);
            auto matching = m_matches.at(index.row()).insertText;
304
305
            // 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('>')))) {
306
307
308
309
                matching.chop(1);
            }
            view->document()->replaceText(word, matching);
        }
310
311
312
313
314
315
316
317
    }

    void aborted(KTextEditor::View *view) override
    {
        Q_UNUSED(view);
        beginResetModel();
        m_matches.clear();
        m_handle.cancel();
318
319
        m_handleSig.cancel();
        m_triggerSignature = false;
320
321
322
323
        endResetModel();
    }
};

324
LSPClientCompletion *LSPClientCompletion::new_(QSharedPointer<LSPClientServerManager> manager)
325
{
Filip Gawin's avatar
Filip Gawin committed
326
    return new LSPClientCompletionImpl(std::move(manager));
327
328
329
}

#include "lspclientcompletion.moc"