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 ¶m = 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"