/* * Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. * * The Qt Company portions: * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 * * Petr Mironychev portions: * QodeAssist is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * QodeAssist is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with QodeAssist. If not, see . */ #include "LLMSuggestion.hpp" #include #include #include namespace QodeAssist { static QStringList extractTokens(const QString &str) { QStringList tokens; QString currentToken; for (const QChar &ch : str) { if (ch.isLetterOrNumber() || ch == '_') { currentToken += ch; } else { if (!currentToken.isEmpty() && currentToken.length() > 1) { tokens.append(currentToken); } currentToken.clear(); } } if (!currentToken.isEmpty() && currentToken.length() > 1) { tokens.append(currentToken); } return tokens; } int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText, const QString &entireLine) { if (rightText.isEmpty()) { return 0; } QString structuralChars = "{}[]()<>;,"; bool hasStructuralOverlap = false; for (const QChar &ch : structuralChars) { if (suggestion.contains(ch) && rightText.contains(ch)) { hasStructuralOverlap = true; break; } } if (hasStructuralOverlap) { return rightText.length(); } const QStringList suggestionTokens = extractTokens(suggestion); const QStringList lineTokens = extractTokens(entireLine); for (const auto &token : suggestionTokens) { if (lineTokens.contains(token)) { return rightText.length(); } } 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; QString entireLine = blockText; if (!suggestionText.contains('\n')) { int replaceLength = calculateReplaceLength(suggestionText, rightText, entireLine); 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, entireLine); 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()); QString entireLine = currentBlock.text(); int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine); 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 textBeforeCursor = currentBlock.text().left(cursor.positionInBlock()); QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock()); QString entireLine = currentBlock.text(); 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, entireLine); if (replaceLength > 0) { editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength); editCursor.removeSelectedText(); } editCursor.insertText(firstLine + restOfText); } else { int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine); if (replaceLength > 0) { editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength); editCursor.removeSelectedText(); } editCursor.insertText(text); } editCursor.endEditBlock(); return true; } } // namespace QodeAssist