File indexing completed on 2024-04-28 05:48:56

0001 /*
0002     SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include "lspclientcompletion.h"
0008 #include "lspclientplugin.h"
0009 #include "lspclientprotocol.h"
0010 #include "lspclientutils.h"
0011 
0012 #include "lspclient_debug.h"
0013 
0014 #include <KTextEditor/Cursor>
0015 #include <KTextEditor/Document>
0016 #include <KTextEditor/Editor>
0017 #include <KTextEditor/View>
0018 
0019 #include <QIcon>
0020 
0021 #include <algorithm>
0022 #include <utility>
0023 
0024 #include <drawing_utils.h>
0025 
0026 static KTextEditor::CodeCompletionModel::CompletionProperty kind_property(LSPCompletionItemKind kind)
0027 {
0028     using CompletionProperty = KTextEditor::CodeCompletionModel::CompletionProperty;
0029     auto p = CompletionProperty::NoProperty;
0030 
0031     switch (kind) {
0032     case LSPCompletionItemKind::Method:
0033     case LSPCompletionItemKind::Function:
0034     case LSPCompletionItemKind::Constructor:
0035         p = CompletionProperty::Function;
0036         break;
0037     case LSPCompletionItemKind::Variable:
0038         p = CompletionProperty::Variable;
0039         break;
0040     case LSPCompletionItemKind::Class:
0041     case LSPCompletionItemKind::Interface:
0042         p = CompletionProperty::Class;
0043         break;
0044     case LSPCompletionItemKind::Struct:
0045         p = CompletionProperty::Class;
0046         break;
0047     case LSPCompletionItemKind::Module:
0048         p = CompletionProperty::Namespace;
0049         break;
0050     case LSPCompletionItemKind::Enum:
0051     case LSPCompletionItemKind::EnumMember:
0052         p = CompletionProperty::Enum;
0053         break;
0054     default:
0055         break;
0056     }
0057     return p;
0058 }
0059 
0060 struct LSPClientCompletionItem : public LSPCompletionItem {
0061     int argumentHintDepth = 0;
0062     QString prefix;
0063     QString postfix;
0064     int start = 0;
0065     int len = 0;
0066 
0067     LSPClientCompletionItem(const LSPCompletionItem &item)
0068         : LSPCompletionItem(item)
0069     {
0070         // transform for later display
0071         // sigh, remove (leading) whitespace (looking at clangd here)
0072         // could skip the [] if empty detail, but it is a handy watermark anyway ;-)
0073         label = QString(label.simplified() + QLatin1String(" [") + detail.simplified() + QStringLiteral("]"));
0074     }
0075 
0076     LSPClientCompletionItem(const LSPSignatureInformation &sig, int activeParameter, const QString &_sortText)
0077     {
0078         argumentHintDepth = 1;
0079         documentation = sig.documentation;
0080         label = sig.label;
0081         sortText = _sortText;
0082 
0083         // transform into prefix, name, suffix if active
0084         if (activeParameter >= 0 && activeParameter < sig.parameters.length()) {
0085             const auto &param = sig.parameters.at(activeParameter);
0086             if (param.start >= 0 && param.start < label.length() && param.end >= 0 && param.end < label.length() && param.start < param.end) {
0087                 start = param.start;
0088                 len = param.end - param.start;
0089                 prefix = label.mid(0, param.start);
0090                 postfix = label.mid(param.end);
0091                 label = label.mid(param.start, param.end - param.start);
0092             }
0093         }
0094     }
0095 };
0096 
0097 /**
0098  * Helper class that caches the completion icons
0099  */
0100 class CompletionIcons : public QObject
0101 {
0102     Q_OBJECT
0103 
0104 public:
0105     CompletionIcons()
0106         : QObject(KTextEditor::Editor::instance())
0107         , classIcon(QIcon::fromTheme(QStringLiteral("code-class")))
0108         , blockIcon(QIcon::fromTheme(QStringLiteral("code-block")))
0109         , funcIcon(QIcon::fromTheme(QStringLiteral("code-function")))
0110         , varIcon(QIcon::fromTheme(QStringLiteral("code-variable")))
0111         , enumIcon(QIcon::fromTheme(QStringLiteral("enum")))
0112     {
0113         auto e = KTextEditor::Editor::instance();
0114         QObject::connect(e, &KTextEditor::Editor::configChanged, this, [this](KTextEditor::Editor *e) {
0115             colorIcons(e);
0116         });
0117         colorIcons(e);
0118     }
0119 
0120     QIcon iconForKind(LSPCompletionItemKind kind) const
0121     {
0122         switch (kind) {
0123         case LSPCompletionItemKind::Method:
0124         case LSPCompletionItemKind::Function:
0125         case LSPCompletionItemKind::Constructor:
0126             return funcIcon;
0127         case LSPCompletionItemKind::Variable:
0128             return varIcon;
0129         case LSPCompletionItemKind::Class:
0130         case LSPCompletionItemKind::Interface:
0131         case LSPCompletionItemKind::Struct:
0132             return classIcon;
0133         case LSPCompletionItemKind::Module:
0134             return blockIcon;
0135         case LSPCompletionItemKind::Field:
0136         case LSPCompletionItemKind::Property:
0137             // align with symbolview
0138             return varIcon;
0139         case LSPCompletionItemKind::Enum:
0140         case LSPCompletionItemKind::EnumMember:
0141             return enumIcon;
0142         default:
0143             break;
0144         }
0145         return QIcon();
0146     }
0147 
0148 private:
0149     void colorIcons(KTextEditor::Editor *e)
0150     {
0151         using KSyntaxHighlighting::Theme;
0152         auto theme = e->theme();
0153         auto varColor = QColor::fromRgba(theme.textColor(Theme::Variable));
0154         varIcon = Utils::colorIcon(varIcon, varColor);
0155 
0156         auto typeColor = QColor::fromRgba(theme.textColor(Theme::DataType));
0157         classIcon = Utils::colorIcon(classIcon, typeColor);
0158 
0159         auto enColor = QColor::fromRgba(theme.textColor(Theme::Constant));
0160         enumIcon = Utils::colorIcon(enumIcon, enColor);
0161 
0162         auto funcColor = QColor::fromRgba(theme.textColor(Theme::Function));
0163         funcIcon = Utils::colorIcon(funcIcon, funcColor);
0164 
0165         auto blockColor = QColor::fromRgba(theme.textColor(Theme::Import));
0166         blockIcon = Utils::colorIcon(blockIcon, blockColor);
0167     }
0168 
0169 private:
0170     QIcon classIcon;
0171     QIcon blockIcon;
0172     QIcon funcIcon;
0173     QIcon varIcon;
0174     QIcon enumIcon;
0175 };
0176 
0177 static bool compare_match(const LSPCompletionItem &a, const LSPCompletionItem &b)
0178 {
0179     return a.sortText < b.sortText;
0180 }
0181 
0182 class LSPClientCompletionImpl : public LSPClientCompletion
0183 {
0184     Q_OBJECT
0185 
0186     typedef LSPClientCompletionImpl self_type;
0187 
0188     std::shared_ptr<LSPClientServerManager> m_manager;
0189     std::shared_ptr<LSPClientServer> m_server;
0190     bool m_selectedDocumentation = false;
0191     bool m_signatureHelp = true;
0192     bool m_complParens = true;
0193     bool m_autoImport = true;
0194 
0195     QList<QChar> m_triggersCompletion;
0196     QList<QChar> m_triggersSignature;
0197     bool m_triggerSignature = false;
0198     bool m_triggerCompletion = false;
0199 
0200     QList<LSPClientCompletionItem> m_matches;
0201     LSPClientServer::RequestHandle m_handle, m_handleSig;
0202 
0203 public:
0204     LSPClientCompletionImpl(std::shared_ptr<LSPClientServerManager> manager)
0205         : LSPClientCompletion(nullptr)
0206         , m_manager(std::move(manager))
0207         , m_server(nullptr)
0208     {
0209     }
0210 
0211     void setServer(std::shared_ptr<LSPClientServer> server) override
0212     {
0213         m_server = server;
0214         if (m_server) {
0215             const auto &caps = m_server->capabilities();
0216             m_triggersCompletion = caps.completionProvider.triggerCharacters;
0217             m_triggersSignature = caps.signatureHelpProvider.triggerCharacters;
0218         } else {
0219             m_triggersCompletion.clear();
0220             m_triggersSignature.clear();
0221         }
0222     }
0223 
0224     void setSelectedDocumentation(bool s) override
0225     {
0226         m_selectedDocumentation = s;
0227     }
0228 
0229     void setSignatureHelp(bool s) override
0230     {
0231         m_signatureHelp = s;
0232     }
0233 
0234     void setCompleteParens(bool s) override
0235     {
0236         m_complParens = s;
0237     }
0238 
0239     void setAutoImport(bool s) override
0240     {
0241         m_autoImport = s;
0242     }
0243 
0244     QVariant data(const QModelIndex &index, int role) const override
0245     {
0246         if (!index.isValid() || index.row() >= m_matches.size()) {
0247             return QVariant();
0248         }
0249 
0250         const auto &match = m_matches.at(index.row());
0251         static CompletionIcons *icons = new CompletionIcons;
0252 
0253         if (role == Qt::DisplayRole) {
0254             if (index.column() == KTextEditor::CodeCompletionModel::Name) {
0255                 return match.label;
0256             } else if (index.column() == KTextEditor::CodeCompletionModel::Prefix) {
0257                 return match.prefix;
0258             } else if (index.column() == KTextEditor::CodeCompletionModel::Postfix) {
0259                 return match.postfix;
0260             }
0261         } else if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) {
0262             return icons->iconForKind(match.kind);
0263         } else if (role == KTextEditor::CodeCompletionModel::CompletionRole) {
0264             return kind_property(match.kind);
0265         } else if (role == KTextEditor::CodeCompletionModel::ArgumentHintDepth) {
0266             return match.argumentHintDepth;
0267         } else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) {
0268             // (ab)use depth to indicate sort order
0269             return index.row();
0270         } else if (role == KTextEditor::CodeCompletionModel::IsExpandable) {
0271             return !match.documentation.value.isEmpty();
0272         } else if (role == KTextEditor::CodeCompletionModel::ExpandingWidget && !match.documentation.value.isEmpty()) {
0273             // probably plaintext, but let's show markdown as-is for now
0274             // FIXME better presentation of markdown
0275             return match.documentation.value;
0276         } else if (role == KTextEditor::CodeCompletionModel::ItemSelected && !match.argumentHintDepth && !match.documentation.value.isEmpty()
0277                    && m_selectedDocumentation) {
0278             return match.documentation.value;
0279         } else if (role == KTextEditor::CodeCompletionModel::CustomHighlight && match.argumentHintDepth > 0) {
0280             if (index.column() != Name || match.len == 0)
0281                 return {};
0282             QTextCharFormat boldFormat;
0283             boldFormat.setFontWeight(QFont::Bold);
0284             const QVariantList highlighting{
0285                 QVariant(0),
0286                 QVariant(match.len),
0287                 boldFormat,
0288             };
0289             return highlighting;
0290         } else if (role == CodeCompletionModel::HighlightingMethod && match.argumentHintDepth > 0) {
0291             return QVariant(HighlightMethod::CustomHighlighting);
0292         }
0293 
0294         return QVariant();
0295     }
0296 
0297     bool shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position) override
0298     {
0299         qCInfo(LSPCLIENT) << "should start " << userInsertion << insertedText;
0300 
0301         if (!userInsertion || !m_server || insertedText.isEmpty()) {
0302             if (!insertedText.isEmpty() && m_triggersSignature.contains(insertedText.back())) {
0303                 m_triggerSignature = true;
0304                 return true;
0305             }
0306             return false;
0307         }
0308 
0309         // covers most already ...
0310         bool complete = CodeCompletionModelControllerInterface::shouldStartCompletion(view, insertedText, userInsertion, position);
0311         QChar lastChar = insertedText.at(insertedText.count() - 1);
0312 
0313         m_triggerSignature = false;
0314         complete = complete || m_triggersCompletion.contains(lastChar);
0315         m_triggerCompletion = complete;
0316         if (m_triggersSignature.contains(lastChar)) {
0317             complete = true;
0318             m_triggerSignature = true;
0319         }
0320         return complete;
0321     }
0322 
0323     void completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it) override
0324     {
0325         Q_UNUSED(it)
0326 
0327         qCInfo(LSPCLIENT) << "completion invoked" << m_server.get();
0328 
0329         const bool userInvocation = it == UserInvocation;
0330         if (userInvocation && range.isEmpty() && m_signatureHelp) {
0331             // If this is a user invocation (ctrl-space), check the last non-space char for sig help trigger
0332             QChar c;
0333             int i = range.start().column() - 1;
0334             int ln = range.start().line();
0335             for (; i >= 0; --i) {
0336                 c = view->document()->characterAt(KTextEditor::Cursor(ln, i));
0337                 if (!c.isSpace()) {
0338                     break;
0339                 }
0340             }
0341             m_triggerSignature = m_triggersSignature.contains(c);
0342         }
0343 
0344         // maybe use WaitForReset ??
0345         // but more complex and already looks good anyway
0346         auto handler = [this](const QList<LSPCompletionItem> &completion) {
0347             beginResetModel();
0348             qCInfo(LSPCLIENT) << "adding completions " << completion.size();
0349             // purge all existing completion items
0350             m_matches.erase(std::remove_if(m_matches.begin(),
0351                                            m_matches.end(),
0352                                            [](const LSPClientCompletionItem &ci) {
0353                                                return ci.argumentHintDepth == 0;
0354                                            }),
0355                             m_matches.end());
0356             for (const auto &item : completion) {
0357                 m_matches.push_back(item);
0358             }
0359             std::stable_sort(m_matches.begin(), m_matches.end(), compare_match);
0360             setRowCount(m_matches.size());
0361             endResetModel();
0362         };
0363 
0364         auto sigHandler = [this](const LSPSignatureHelp &sig) {
0365             beginResetModel();
0366             qCInfo(LSPCLIENT) << "adding signatures " << sig.signatures.size();
0367             int index = 0;
0368             m_matches.erase(std::remove_if(m_matches.begin(),
0369                                            m_matches.end(),
0370                                            [](const LSPClientCompletionItem &ci) {
0371                                                return ci.argumentHintDepth == 1;
0372                                            }),
0373                             m_matches.end());
0374             for (const auto &item : sig.signatures) {
0375                 int sortIndex = 10 + index;
0376                 int active = -1;
0377                 if (index == sig.activeSignature) {
0378                     sortIndex = 0;
0379                     active = sig.activeParameter;
0380                 }
0381                 // trick active first, others after that
0382                 m_matches.push_back({item, active, QString(QStringLiteral("%1").arg(sortIndex, 3, 10))});
0383                 ++index;
0384             }
0385             std::stable_sort(m_matches.begin(), m_matches.end(), compare_match);
0386             setRowCount(m_matches.size());
0387             endResetModel();
0388         };
0389 
0390         beginResetModel();
0391         m_matches.clear();
0392         auto document = view->document();
0393         if (m_server && document) {
0394             // the default range is determined based on a reasonable identifier (word)
0395             // which is generally fine and nice, but let's pass actual cursor position
0396             // (which may be within this typical range)
0397             auto position = view->cursorPosition();
0398             auto cursor = qMax(range.start(), qMin(range.end(), position));
0399             m_manager->update(document, false);
0400 
0401             if (m_triggerCompletion || userInvocation) {
0402                 m_handle = m_server->documentCompletion(document->url(), {cursor.line(), cursor.column()}, this, handler);
0403             }
0404 
0405             if (m_signatureHelp && m_triggerSignature) {
0406                 m_handleSig = m_server->signatureHelp(document->url(), {cursor.line(), cursor.column()}, this, sigHandler);
0407             }
0408         }
0409         setRowCount(m_matches.size());
0410         endResetModel();
0411     }
0412 
0413     /**
0414      * @brief return next char *after* the range
0415      */
0416     static QChar peekNextChar(KTextEditor::Document *doc, const KTextEditor::Range &range)
0417     {
0418         return doc->characterAt(KTextEditor::Cursor(range.end().line(), range.end().column()));
0419     }
0420 
0421     // parses lsp snippets
0422     // returns the column where cursor should be after completion and the text to insert
0423     std::pair<int, QString> stripSnippetMarkers(const QString &snip) const
0424     {
0425 #define C(c) QLatin1Char(c)
0426         QString ret;
0427         ret.reserve(snip.size());
0428         int bracket = 0;
0429         int lastSnippetMarkerPos = -1;
0430         for (auto i = snip.begin(), end = snip.end(); i != end; ++i) {
0431             const bool prevSlash = i > snip.begin() && *(i - 1) == C('\\');
0432             if (!prevSlash && *i == C('$') && i + 1 != end && *(i + 1) == C('{')) {
0433                 if (i + 2 != end && (i + 2)->isDigit()) {
0434                     // its ${1:
0435                     auto j = i + 2;
0436                     // eat through digits
0437                     while (j->isDigit()) {
0438                         ++j;
0439                     }
0440                     if (*j == C(':')) {
0441                         bracket++;
0442                         // skip forward
0443                         i = j;
0444                     }
0445                 } else {
0446                     // simple "${"
0447                     ++i;
0448                     bracket++;
0449                 }
0450             } else if (!prevSlash && *i == C('$') && i + 1 != end && (i + 1)->isDigit()) { // $0, $1 => we dont support multiple cursor pos
0451                 ++i;
0452                 // eat through the digits
0453                 while (i->isDigit()) {
0454                     ++i;
0455                 }
0456                 --i; // one step back to the last valid char
0457             } else if (bracket > 0 && *i == C('}')) {
0458                 bracket--;
0459                 if (bracket == 0 && lastSnippetMarkerPos == -1) {
0460                     lastSnippetMarkerPos = ret.size();
0461                 }
0462             } else if (bracket == 0) { // if we are in "real text", add it
0463                 ret += *i;
0464             }
0465         }
0466 
0467 #undef C
0468         return {lastSnippetMarkerPos, ret};
0469     }
0470 
0471     void executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const override
0472     {
0473         if (index.row() >= m_matches.size()) {
0474             return;
0475         }
0476 
0477         QChar next = peekNextChar(view->document(), word);
0478         const auto item = m_matches.at(index.row());
0479         QString matching = m_matches.at(index.row()).insertText;
0480         // if there is already a '"' or >, remove it, this happens with #include "xx.h"
0481         if ((next == QLatin1Char('"') && matching.endsWith(QLatin1Char('"'))) || (next == QLatin1Char('>') && matching.endsWith(QLatin1Char('>')))) {
0482             matching.chop(1);
0483         }
0484 
0485         // If the server sent a CompletionItem.textEdit.range and that range's start
0486         // is different than what we have, perfer the server. This leads to better
0487         // completion because the server might be supplying items for a bigger range than
0488         // just the current word under cursor.
0489         const auto textEditRange = item.textEdit.range;
0490         auto rangeToReplace = word;
0491         if (textEditRange.isValid()
0492             && textEditRange.start() < word.start()
0493             // only do this if the text to insert is the same as TextEdit.newText
0494             && m_matches.at(index.row()).insertText == m_matches.at(index.row()).textEdit.newText) {
0495             rangeToReplace.setStart(textEditRange.start());
0496         }
0497 
0498         // NOTE: view->setCursorPosition() will invalidate the matches, so we save the
0499         // additionalTextEdits before setting cursor-possition
0500         const auto additionalTextEdits = m_matches.at(index.row()).additionalTextEdits;
0501         if (m_complParens) {
0502             const auto [col, textToInsert] = stripSnippetMarkers(matching);
0503             qCInfo(LSPCLIENT) << "original text: " << matching << ", snippet markers removed; " << textToInsert;
0504             view->document()->replaceText(rangeToReplace, textToInsert);
0505             // if the text is same, don't do any work
0506             if (col >= 0 && textToInsert != matching) {
0507                 KTextEditor::Cursor p{rangeToReplace.start()};
0508                 int column = p.column();
0509                 int count = 0;
0510                 // can be multiline text
0511                 for (auto c : textToInsert) {
0512                     if (count == col) {
0513                         break;
0514                     }
0515                     if (c == QLatin1Char('\n')) {
0516                         p.setLine(p.line() + 1);
0517                         // line changed reset column
0518                         column = 0;
0519                         count++;
0520                         continue;
0521                     }
0522                     count++;
0523                     column++;
0524                 }
0525                 p.setColumn(column);
0526                 view->setCursorPosition(p);
0527             }
0528         } else {
0529             view->document()->replaceText(rangeToReplace, matching);
0530         }
0531 
0532         if (m_autoImport) {
0533             // re-use util to apply edits
0534             // (which takes care to use moving range, etc)
0535             if (!additionalTextEdits.isEmpty()) {
0536                 applyEdits(view->document(), nullptr, additionalTextEdits);
0537             } else if (!item.data.isNull() && m_server->capabilities().completionProvider.resolveProvider) {
0538                 QPointer<KTextEditor::Document> doc = view->document();
0539                 auto h = [doc](const LSPCompletionItem &c) {
0540                     if (doc && !c.additionalTextEdits.isEmpty()) {
0541                         applyEdits(doc, nullptr, c.additionalTextEdits);
0542                     }
0543                 };
0544                 m_server->documentCompletionResolve(item, this, h);
0545             }
0546         }
0547     }
0548 
0549     void aborted(KTextEditor::View *view) override
0550     {
0551         Q_UNUSED(view);
0552         beginResetModel();
0553         m_matches.clear();
0554         m_handle.cancel();
0555         m_handleSig.cancel();
0556         m_triggerSignature = false;
0557         endResetModel();
0558     }
0559 };
0560 
0561 LSPClientCompletion *LSPClientCompletion::new_(std::shared_ptr<LSPClientServerManager> manager)
0562 {
0563     return new LSPClientCompletionImpl(std::move(manager));
0564 }
0565 
0566 #include "lspclientcompletion.moc"
0567 #include "moc_lspclientcompletion.cpp"