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 &note) 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 &note, 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"