// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2024-2026 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later #include "LLMSuggestion.hpp" #include #include #include namespace QodeAssist { static bool isClosingTail(const QString &s, int from) { static const QString closeChars = QStringLiteral("(){}[];,"); for (int i = from; i < s.size(); ++i) { const QChar c = s.at(i); if (!c.isSpace() && !closeChars.contains(c)) return false; } return true; } int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText) { if (rightText.isEmpty()) return 0; const int maxN = qMin(suggestion.size(), rightText.size()); int lcp = 0; while (lcp < maxN && suggestion.at(lcp) == rightText.at(lcp)) ++lcp; if (lcp > 0) { if (isClosingTail(rightText, lcp)) return rightText.size(); return lcp; } if (!isClosingTail(rightText, 0)) return 0; static const QString closeChars = QStringLiteral("(){}[];,"); int i = suggestion.size() - 1; while (i >= 0 && suggestion.at(i).isSpace()) --i; if (i >= 0 && closeChars.contains(suggestion.at(i)) && rightText.contains(suggestion.at(i))) return rightText.size(); return 0; } LLMSuggestion::LLMSuggestion( const QList &suggestions, QTextDocument *sourceDocument, int currentCompletion) : TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion) { const auto &data = suggestions[currentCompletion]; int startPos = data.range.begin.toPositionInDocument(sourceDocument); startPos = qBound(0, startPos, sourceDocument->characterCount()); QTextCursor cursor(sourceDocument); cursor.setPosition(startPos); QTextBlock block = cursor.block(); QString blockText = block.text(); int cursorPositionInBlock = cursor.positionInBlock(); QString leftText = blockText.left(cursorPositionInBlock); QString rightText = blockText.mid(cursorPositionInBlock); QString suggestionText = data.text; if (!suggestionText.contains('\n')) { int replaceLength = calculateReplaceLength(suggestionText, rightText); QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText; QString displayText = leftText + suggestionText + remainingRightText; replacementDocument()->setPlainText(displayText); } else { int firstLineEnd = suggestionText.indexOf('\n'); QString firstLine = suggestionText.left(firstLineEnd); QString restOfCompletion = suggestionText.mid(firstLineEnd); int replaceLength = calculateReplaceLength(firstLine, rightText); QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText; QString displayText = leftText + firstLine + remainingRightText + restOfCompletion; replacementDocument()->setPlainText(displayText); } } bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget) { return applyPart(Word, widget); } bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget) { return applyPart(Line, widget); } bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget) { const auto ¤tSuggestions = suggestions(); const auto ¤tData = currentSuggestions[currentSuggestion()]; const Utils::Text::Range range = currentData.range; const QTextCursor cursor = range.begin.toTextCursor(sourceDocument()); QTextCursor currentCursor = widget->textCursor(); const QString text = currentData.text; const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock() + (cursor.selectionEnd() - cursor.selectionStart()); int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos); if (next == -1) { if (part == Line) { next = text.length(); } else { return apply(); } } if (part == Line) ++next; QString subText = text.mid(startPos, next - startPos); if (subText.isEmpty()) { return false; } if (startPos == 0) { QTextBlock currentBlock = cursor.block(); QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock()); int replaceLength = calculateReplaceLength(text, textAfterCursor); if (replaceLength > 0) { currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength); currentCursor.removeSelectedText(); } } if (!subText.contains('\n')) { currentCursor.insertText(subText); const QString remainingText = text.mid(next); if (!remainingText.isEmpty()) { QTextCursor newCursor = widget->textCursor(); const Utils::Text::Position newStart = Utils::Text::Position::fromPositionInDocument( newCursor.document(), newCursor.position()); const Utils::Text::Position newEnd{newStart.line, newStart.column + int(remainingText.length())}; const Utils::Text::Range newRange{newStart, newEnd}; const QList newSuggestion{{newRange, newStart, remainingText}}; widget->insertSuggestion( std::make_unique(newSuggestion, widget->document(), 0)); } } else { currentCursor.insertText(subText); if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) { const QString newCompletionText = text.mid(startPos + seperatorPos + 1); if (!newCompletionText.isEmpty()) { const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0}; const Utils::Text::Position newEnd{newStart.line, int(newCompletionText.length())}; const Utils::Text::Range newRange{newStart, newEnd}; const QList newSuggestion{{newRange, newEnd, newCompletionText}}; widget->insertSuggestion( std::make_unique(newSuggestion, widget->document(), 0)); } } } return false; } bool LLMSuggestion::apply() { const auto ¤tSuggestions = suggestions(); const auto ¤tData = currentSuggestions[currentSuggestion()]; const Utils::Text::Range range = currentData.range; const QTextCursor cursor = range.begin.toTextCursor(sourceDocument()); QString text = currentData.text; QTextBlock currentBlock = cursor.block(); QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock()); QTextCursor editCursor = cursor; editCursor.beginEditBlock(); int firstLineEnd = text.indexOf('\n'); if (firstLineEnd != -1) { QString firstLine = text.left(firstLineEnd); QString restOfText = text.mid(firstLineEnd); int replaceLength = calculateReplaceLength(firstLine, textAfterCursor); if (replaceLength > 0) { editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength); editCursor.removeSelectedText(); } editCursor.insertText(firstLine + restOfText); } else { int replaceLength = calculateReplaceLength(text, textAfterCursor); if (replaceLength > 0) { editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength); editCursor.removeSelectedText(); } editCursor.insertText(text); } editCursor.endEditBlock(); return true; } } // namespace QodeAssist