File indexing completed on 2024-04-28 05:48:56
0001 /* 0002 SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com> 0003 SPDX-License-Identifier: LGPL-2.0-or-later 0004 */ 0005 #include "inlayhints.h" 0006 0007 #include "lspclientservermanager.h" 0008 #include <ktexteditor_utils.h> 0009 0010 #include <KSyntaxHighlighting/Theme> 0011 #include <KTextEditor/Document> 0012 #include <KTextEditor/InlineNote> 0013 0014 #include <QApplication> 0015 #include <QPainter> 0016 #include <QSet> 0017 0018 static constexpr int textChangedDelay = 1000; // 1s 0019 0020 size_t qHash(const LSPInlayHint &s, size_t seed = 0) 0021 { 0022 return qHashMulti(seed, s.position, s.label); 0023 } 0024 0025 [[maybe_unused]] static bool operator==(const LSPInlayHint &l, const LSPInlayHint &r) 0026 { 0027 return l.position == r.position && l.label == r.label; 0028 } 0029 0030 template<typename T> 0031 static auto binaryFind(T &&hints, int line) 0032 { 0033 auto it = std::lower_bound(hints.begin(), hints.end(), line, [](const LSPInlayHint &h, int l) { 0034 return h.position.line() < l; 0035 }); 0036 if (it != hints.end() && it->position.line() == line) { 0037 return it; 0038 } 0039 return hints.end(); 0040 } 0041 0042 static auto binaryFind(const QList<LSPInlayHint> &hints, KTextEditor::Cursor pos) 0043 { 0044 auto it = std::lower_bound(hints.begin(), hints.end(), pos, [](const LSPInlayHint &h, KTextEditor::Cursor p) { 0045 return h.position < p; 0046 }); 0047 if (it != hints.end() && it->position == pos) { 0048 return it; 0049 } 0050 return hints.end(); 0051 } 0052 0053 static void removeInvalidRanges(QList<LSPInlayHint> &hints, QList<LSPInlayHint>::iterator begin, QList<LSPInlayHint>::iterator end) 0054 { 0055 hints.erase(std::remove_if(begin, 0056 end, 0057 [](const LSPInlayHint &h) { 0058 return !h.position.isValid(); 0059 }), 0060 end); 0061 } 0062 0063 InlayHintNoteProvider::InlayHintNoteProvider() 0064 { 0065 } 0066 0067 void InlayHintNoteProvider::setView(KTextEditor::View *v) 0068 { 0069 m_view = v; 0070 if (v) { 0071 m_noteColor = QColor::fromRgba(v->theme().textColor(KSyntaxHighlighting::Theme::Normal)); 0072 m_noteBgColor = m_noteColor; 0073 m_noteBgColor.setAlphaF(0.1f); 0074 m_noteColor.setAlphaF(0.5f); 0075 } 0076 m_hints = {}; 0077 } 0078 0079 void InlayHintNoteProvider::setHints(const QList<LSPInlayHint> &hints) 0080 { 0081 m_hints = hints; 0082 } 0083 0084 QList<int> InlayHintNoteProvider::inlineNotes(int line) const 0085 { 0086 QList<int> ret; 0087 auto it = binaryFind(m_hints, line); 0088 while (it != m_hints.cend() && it->position.line() == line) { 0089 ret.push_back(it->position.column()); 0090 ++it; 0091 } 0092 return ret; 0093 } 0094 0095 QSize InlayHintNoteProvider::inlineNoteSize(const KTextEditor::InlineNote ¬e) const 0096 { 0097 auto it = binaryFind(m_hints, note.position()); 0098 if (it == m_hints.end()) { 0099 qWarning() << Q_FUNC_INFO << note.view()->document()->documentName() << "failed to find note in m_hints, Note.position:" << note.position(); 0100 return {}; 0101 } 0102 0103 const LSPInlayHint &hint = *it; 0104 const int padding = (hint.paddingLeft || hint.paddingRight) ? 4 : 0; 0105 if (hint.width == 0) { 0106 const auto font = qApp->font(); 0107 const auto fm = QFontMetrics(font); 0108 const_cast<LSPInlayHint &>(hint).width = fm.horizontalAdvance(hint.label) + padding; 0109 } 0110 return {hint.width, note.lineHeight()}; 0111 } 0112 0113 void InlayHintNoteProvider::paintInlineNote(const KTextEditor::InlineNote ¬e, QPainter &painter, Qt::LayoutDirection) const 0114 { 0115 auto it = binaryFind(m_hints, note.position()); 0116 if (it != m_hints.end()) { 0117 const auto font = qApp->font(); 0118 painter.setFont(font); 0119 QRectF r{0., 0., (qreal)it->width, (qreal)note.lineHeight()}; 0120 0121 // draw background rectangle 0122 painter.setRenderHint(QPainter::Antialiasing); 0123 painter.setBrush(m_noteBgColor); 0124 painter.setPen(Qt::NoPen); 0125 auto bgRect = r; 0126 bgRect.setHeight(QFontMetricsF(font).height()); 0127 bgRect.moveTop((r.height() - bgRect.height()) / 2.); 0128 painter.drawRoundedRect(bgRect, 3., 3.); 0129 0130 // draw the hint 0131 painter.setPen(m_noteColor); 0132 if (it->paddingLeft) { 0133 r.adjust(4., 0., 0., 0.); 0134 } else if (it->paddingRight) { 0135 r.adjust(0., 0., -4., 0.); 0136 } 0137 painter.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, it->label); 0138 } 0139 } 0140 0141 InlayHintsManager::InlayHintsManager(const std::shared_ptr<LSPClientServerManager> &manager, QObject *parent) 0142 : QObject(parent) 0143 , m_serverManager(manager) 0144 { 0145 m_requestTimer.setSingleShot(true); 0146 m_requestTimer.callOnTimeout(this, &InlayHintsManager::sendPendingRequests); 0147 } 0148 0149 InlayHintsManager::~InlayHintsManager() 0150 { 0151 unregisterView(m_currentView); 0152 } 0153 0154 void InlayHintsManager::registerView(KTextEditor::View *v) 0155 { 0156 using namespace KTextEditor; 0157 if (v) { 0158 // when reloading the view is same 0159 bool reloaded = m_currentView == v; 0160 m_currentView = v; 0161 m_currentView->registerInlineNoteProvider(&m_noteProvider); 0162 m_noteProvider.setView(v); 0163 auto d = v->document(); 0164 0165 connect(d, &Document::textInserted, this, &InlayHintsManager::onTextInserted, Qt::UniqueConnection); 0166 connect(d, &Document::textRemoved, this, &InlayHintsManager::onTextRemoved, Qt::UniqueConnection); 0167 connect(d, &Document::lineWrapped, this, &InlayHintsManager::onWrapped, Qt::UniqueConnection); 0168 connect(d, &Document::lineUnwrapped, this, &InlayHintsManager::onUnwrapped, Qt::UniqueConnection); 0169 0170 auto it = std::find_if(m_hintDataByDoc.begin(), m_hintDataByDoc.end(), [doc = v->document()](const HintData &hd) { 0171 return hd.doc == doc; 0172 }); 0173 0174 // If the document was found and checksum hasn't changed 0175 if (it != m_hintDataByDoc.end() && it->checksum == d->checksum() && !it->m_hints.empty() && !reloaded) { 0176 m_noteProvider.setHints(it->m_hints); 0177 m_noteProvider.inlineNotesReset(); 0178 } else { 0179 if (it != m_hintDataByDoc.end()) { 0180 m_hintDataByDoc.erase(it); 0181 } 0182 // clear hints from the inline note provider and reset it 0183 m_noteProvider.setHints({}); 0184 m_noteProvider.inlineNotesReset(); 0185 // Send delayed request for inlay hints 0186 sendRequestDelayed(v->document()->documentRange(), 1); 0187 } 0188 } 0189 0190 clearHintsForDoc(nullptr); 0191 } 0192 0193 void InlayHintsManager::unregisterView(KTextEditor::View *v) 0194 { 0195 if (v) { 0196 v->disconnect(this); 0197 v->document()->disconnect(this); 0198 m_currentView->unregisterInlineNoteProvider(&m_noteProvider); 0199 auto it = std::find_if(m_hintDataByDoc.begin(), m_hintDataByDoc.end(), [doc = v->document()](const HintData &hd) { 0200 return hd.doc == doc; 0201 }); 0202 // update checksum 0203 // we use it to check if doc was changed when restoring hints 0204 if (it != m_hintDataByDoc.end()) { 0205 it->checksum = v->document()->checksum(); 0206 } 0207 } 0208 m_noteProvider.setView(nullptr); 0209 } 0210 0211 void InlayHintsManager::setActiveView(KTextEditor::View *v) 0212 { 0213 unregisterView(m_currentView); 0214 registerView(v); 0215 } 0216 0217 void InlayHintsManager::disable() 0218 { 0219 unregisterView(m_currentView); 0220 m_currentView.clear(); 0221 } 0222 0223 void InlayHintsManager::sendRequestDelayed(KTextEditor::Range r, int delay) 0224 { 0225 // If its a single line range and the last range is on the same line 0226 if (r.onSingleLine() && !pendingRanges.isEmpty() && pendingRanges.back().onSingleLine() && pendingRanges.back().end().line() == r.start().line()) { 0227 pendingRanges.back() = r; 0228 // start column must always be zero 0229 pendingRanges.back().start().setColumn(0); 0230 } else { 0231 pendingRanges.append(r); 0232 } 0233 0234 m_requestTimer.start(delay); 0235 } 0236 0237 void InlayHintsManager::sendPendingRequests() 0238 { 0239 if (pendingRanges.empty()) { 0240 return; 0241 } 0242 0243 KTextEditor::Range rangeToRequest = pendingRanges.first(); 0244 for (auto r : std::as_const(pendingRanges)) { 0245 rangeToRequest.expandToRange(r); 0246 } 0247 pendingRanges.clear(); 0248 0249 if (rangeToRequest.isValid()) { 0250 sendRequest(rangeToRequest); 0251 } 0252 } 0253 0254 void InlayHintsManager::sendRequest(KTextEditor::Range rangeToRequest) 0255 { 0256 if (!m_currentView || !m_currentView->document()) { 0257 return; 0258 } 0259 0260 const auto url = m_currentView->document()->url(); 0261 0262 auto v = m_currentView; 0263 auto server = m_serverManager->findServer(v, false); 0264 if (server) { 0265 server->documentInlayHint(url, rangeToRequest, this, [v = QPointer(m_currentView), rangeToRequest, this](QList<LSPInlayHint> hints) { 0266 if (!v || m_currentView != v) { 0267 return; 0268 } 0269 0270 // Some server e.g., dart-analyzer will just ignore the range we sent in the request 0271 // and send over inlay hints for the full document anyways. To avoid issues, we 0272 // remove all the inlay hints that fall outside the range we requested hints for. 0273 if (rangeToRequest.isValid()) { 0274 hints.erase(std::remove_if(hints.begin(), 0275 hints.end(), 0276 [rangeToRequest](const LSPInlayHint &h) { 0277 return !rangeToRequest.contains(h.position); 0278 }), 0279 hints.end()); 0280 } 0281 0282 const auto result = insertHintsForDoc(v->document(), rangeToRequest, hints); 0283 m_noteProvider.setHints(result.addedHints); 0284 if (result.newDoc) { 0285 m_noteProvider.inlineNotesReset(); 0286 } else { 0287 // qDebug() << "got hints: " << hints.size() << "changed lines: " << result.changedLines.size(); 0288 for (const auto &line : result.changedLines) { 0289 m_noteProvider.inlineNotesChanged(line); 0290 } 0291 } 0292 }); 0293 } 0294 } 0295 0296 void InlayHintsManager::onTextInserted(KTextEditor::Document *doc, KTextEditor::Cursor pos, const QString &text) 0297 { 0298 auto it = std::find_if(m_hintDataByDoc.begin(), m_hintDataByDoc.end(), [doc](const HintData &hd) { 0299 return hd.doc == doc; 0300 }); 0301 if (it != m_hintDataByDoc.end()) { 0302 auto &list = it->m_hints; 0303 bool changed = false; 0304 auto bit = binaryFind(list, pos.line()); 0305 for (; bit != list.end(); ++bit) { 0306 if (bit->position.line() > pos.line()) { 0307 break; 0308 } 0309 if (bit->position > pos) { 0310 bit->position.setColumn(bit->position.column() + text.size()); 0311 changed = true; 0312 } 0313 } 0314 if (changed) { 0315 m_noteProvider.setHints(list); 0316 } 0317 } 0318 0319 KTextEditor::Range r(pos.line(), 0, pos.line(), doc->lineLength(pos.line())); 0320 sendRequestDelayed(r, textChangedDelay); 0321 } 0322 0323 void InlayHintsManager::onTextRemoved(KTextEditor::Document *doc, KTextEditor::Range range, const QString &t) 0324 { 0325 if (!range.onSingleLine()) { 0326 KTextEditor::Range r(range.start().line(), 0, range.end().line(), doc->lineLength(range.end().line())); 0327 sendRequestDelayed(r, textChangedDelay); 0328 return; 0329 } 0330 0331 auto it = std::find_if(m_hintDataByDoc.begin(), m_hintDataByDoc.end(), [doc](const HintData &hd) { 0332 return hd.doc == doc; 0333 }); 0334 if (it == m_hintDataByDoc.end()) { 0335 return; 0336 } 0337 auto &list = it->m_hints; 0338 auto bit = binaryFind(list, range.start().line()); 0339 auto lineStartIt = bit; 0340 auto removeBegin = bit; 0341 auto removeEnd = list.end(); 0342 bool changed = false; 0343 bool resort = false; 0344 for (; bit != list.end(); ++bit) { 0345 if (bit->position.line() > range.start().line()) { 0346 removeEnd = bit; 0347 break; 0348 } 0349 if (range.contains(bit->position) && range.start() < bit->position) { 0350 // was inside range? remove this note 0351 bit->position = KTextEditor::Cursor::invalid(); 0352 changed = true; 0353 } else if (bit->position > range.end()) { 0354 // in front? => adjust position 0355 bit->position.setColumn(bit->position.column() - t.size()); 0356 changed = true; 0357 resort = true; 0358 } 0359 } 0360 if (changed) { 0361 removeInvalidRanges(list, removeBegin, removeEnd); 0362 if (resort) { 0363 // resort the hints on this line 0364 std::sort(lineStartIt, removeEnd, [](const LSPInlayHint &l, const LSPInlayHint &r) { 0365 return l.position < r.position; 0366 }); 0367 } 0368 m_noteProvider.setHints(list); 0369 } 0370 0371 KTextEditor::Range r(range.start().line(), 0, range.end().line(), doc->lineLength(range.end().line())); 0372 sendRequestDelayed(r, textChangedDelay); 0373 } 0374 0375 void InlayHintsManager::onWrapped(KTextEditor::Document *doc, KTextEditor::Cursor position) 0376 { 0377 auto it = std::find_if(m_hintDataByDoc.begin(), m_hintDataByDoc.end(), [doc](const HintData &hd) { 0378 return hd.doc == doc; 0379 }); 0380 if (it == m_hintDataByDoc.end()) { 0381 return; 0382 } 0383 auto &hints = it->m_hints; 0384 auto bit = std::lower_bound(hints.begin(), hints.end(), position.line(), [](const LSPInlayHint &h, int l) { 0385 return h.position.line() < l; 0386 }); 0387 0388 // Invalidate the ranges on the line that are after @p position 0389 auto removeBegin = bit; 0390 auto removeEnd = hints.end(); 0391 bool changed = false; 0392 while (bit != hints.end()) { 0393 if (bit->position.line() > position.line()) { 0394 removeEnd = bit; 0395 break; 0396 } 0397 if (bit->position >= position) { 0398 // invalidate 0399 changed = true; 0400 bit->position = KTextEditor::Cursor::invalid(); 0401 } 0402 ++bit; 0403 } 0404 changed = changed || bit != hints.end(); 0405 while (bit != hints.end()) { 0406 bit->position.setLine(bit->position.line() + 1); 0407 ++bit; 0408 } 0409 0410 // remove invalidated stuff 0411 if (changed) { 0412 removeInvalidRanges(hints, removeBegin, removeEnd); 0413 m_noteProvider.setHints(hints); 0414 } 0415 0416 KTextEditor::Range r(position.line(), 0, position.line(), doc->lineLength(position.line())); 0417 sendRequestDelayed(r, textChangedDelay); 0418 } 0419 0420 void InlayHintsManager::onUnwrapped(KTextEditor::Document *doc, int line) 0421 { 0422 auto it = std::find_if(m_hintDataByDoc.begin(), m_hintDataByDoc.end(), [doc](const HintData &hd) { 0423 return hd.doc == doc; 0424 }); 0425 if (it == m_hintDataByDoc.end()) { 0426 return; 0427 } 0428 0429 auto &hints = it->m_hints; 0430 auto bit = std::lower_bound(hints.begin(), hints.end(), line, [](const LSPInlayHint &h, int l) { 0431 return h.position.line() < l; 0432 }); 0433 0434 auto removeBegin = bit; 0435 auto removeEnd = hints.end(); 0436 bool changed = false; 0437 while (bit != hints.end()) { 0438 if (bit->position.line() > line) { 0439 removeEnd = bit; 0440 break; 0441 } 0442 // invalidate 0443 changed = true; 0444 bit->position = KTextEditor::Cursor::invalid(); 0445 ++bit; 0446 } 0447 0448 changed = changed || bit != hints.end(); 0449 while (bit != hints.end()) { 0450 bit->position.setLine(bit->position.line() - 1); 0451 ++bit; 0452 } 0453 0454 // remove invalidated stuff 0455 if (changed) { 0456 removeInvalidRanges(hints, removeBegin, removeEnd); 0457 m_noteProvider.setHints(hints); 0458 } 0459 0460 KTextEditor::Range r(line - 1, 0, line - 1, doc->lineLength(line)); 0461 sendRequestDelayed(r, textChangedDelay); 0462 } 0463 0464 void InlayHintsManager::clearHintsForDoc(KTextEditor::Document *doc) 0465 { 0466 m_hintDataByDoc.erase(std::remove_if(m_hintDataByDoc.begin(), 0467 m_hintDataByDoc.end(), 0468 [doc](const HintData &hd) { 0469 if (!doc) { 0470 // remove all null docs and docs where checksum doesn't match 0471 return !hd.doc || (hd.doc->checksum() != hd.checksum); 0472 } 0473 return hd.doc == doc; 0474 }), 0475 m_hintDataByDoc.end()); 0476 } 0477 0478 InlayHintsManager::InsertResult 0479 InlayHintsManager::insertHintsForDoc(KTextEditor::Document *doc, KTextEditor::Range requestedRange, const QList<LSPInlayHint> &newHints) 0480 { 0481 auto it = std::find_if(m_hintDataByDoc.begin(), m_hintDataByDoc.end(), [doc](const HintData &hd) { 0482 return hd.doc == doc; 0483 }); 0484 // New document 0485 if (it == m_hintDataByDoc.end()) { 0486 auto &r = m_hintDataByDoc.emplace_back(); 0487 r = HintData{doc, doc->checksum(), newHints}; 0488 return {true, {}, r.m_hints}; 0489 } 0490 // Old 0491 auto &existing = it->m_hints; 0492 if (newHints.isEmpty()) { 0493 const auto r = requestedRange; 0494 auto bit = std::lower_bound(existing.begin(), existing.end(), r.start().line(), [](const LSPInlayHint &h, int l) { 0495 return h.position.line() < l; 0496 }); 0497 if (bit != existing.end()) { 0498 QSet<int> affectedLines; 0499 existing.erase(std::remove_if(bit, 0500 existing.end(), 0501 [r, &affectedLines](const LSPInlayHint &h) { 0502 bool contains = r.contains(h.position); 0503 if (contains) { 0504 affectedLines.insert(h.position.line()); 0505 return true; 0506 } 0507 return false; 0508 }), 0509 existing.end()); 0510 return {false, {affectedLines.begin(), affectedLines.end()}, existing}; 0511 } 0512 return {}; 0513 } 0514 0515 QSet<int> affectedLines; 0516 for (const auto &newHint : newHints) { 0517 affectedLines.insert(newHint.position.line()); 0518 } 0519 0520 QSet<LSPInlayHint> rangesToInsert(newHints.begin(), newHints.end()); 0521 0522 auto pred = [&affectedLines, &rangesToInsert](const LSPInlayHint &h) { 0523 if (affectedLines.contains(h.position.line())) { 0524 auto i = rangesToInsert.find(h); 0525 if (i != rangesToInsert.end()) { 0526 // if this range already exists then it doesn't need to be reinserted 0527 rangesToInsert.erase(i); 0528 return false; 0529 } 0530 return true; 0531 } 0532 return false; 0533 }; 0534 // remove existing hints that contain affectedLines 0535 existing.erase(std::remove_if(existing.begin(), existing.end(), pred), existing.end()); 0536 0537 // now add new ones 0538 affectedLines.clear(); 0539 for (const auto &h : rangesToInsert) { 0540 existing.append(h); 0541 // update affectedLines with lines that are actually changed 0542 affectedLines.insert(h.position.line()); 0543 } 0544 0545 // and sort them 0546 std::sort(existing.begin(), existing.end(), [](const LSPInlayHint &l, const LSPInlayHint &r) { 0547 return l.position < r.position; 0548 }); 0549 0550 return {false, {affectedLines.begin(), affectedLines.end()}, existing}; 0551 } 0552 0553 #include "moc_inlayhints.cpp"