mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 10:59:30 -04:00
feat: Improve assemble string after code suggestion
This commit is contained in:
@@ -9,55 +9,42 @@
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
static QStringList extractTokens(const QString &str)
|
static bool isClosingTail(const QString &s, int from)
|
||||||
{
|
{
|
||||||
QStringList tokens;
|
static const QString closeChars = QStringLiteral("(){}[];,");
|
||||||
QString currentToken;
|
for (int i = from; i < s.size(); ++i) {
|
||||||
for (const QChar &ch : str) {
|
const QChar c = s.at(i);
|
||||||
if (ch.isLetterOrNumber() || ch == '_') {
|
if (!c.isSpace() && !closeChars.contains(c))
|
||||||
currentToken += ch;
|
return false;
|
||||||
} else {
|
|
||||||
if (!currentToken.isEmpty() && currentToken.length() > 1) {
|
|
||||||
tokens.append(currentToken);
|
|
||||||
}
|
|
||||||
currentToken.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!currentToken.isEmpty() && currentToken.length() > 1) {
|
return true;
|
||||||
tokens.append(currentToken);
|
|
||||||
}
|
|
||||||
return tokens;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int LLMSuggestion::calculateReplaceLength(const QString &suggestion,
|
int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText)
|
||||||
const QString &rightText,
|
|
||||||
const QString &entireLine)
|
|
||||||
{
|
{
|
||||||
if (rightText.isEmpty()) {
|
if (rightText.isEmpty())
|
||||||
return 0;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString structuralChars = "{}[]()<>;,";
|
if (!isClosingTail(rightText, 0))
|
||||||
bool hasStructuralOverlap = false;
|
return 0;
|
||||||
for (const QChar &ch : structuralChars) {
|
|
||||||
if (suggestion.contains(ch) && rightText.contains(ch)) {
|
|
||||||
hasStructuralOverlap = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasStructuralOverlap) {
|
static const QString closeChars = QStringLiteral("(){}[];,");
|
||||||
return rightText.length();
|
int i = suggestion.size() - 1;
|
||||||
}
|
while (i >= 0 && suggestion.at(i).isSpace())
|
||||||
|
--i;
|
||||||
const QStringList suggestionTokens = extractTokens(suggestion);
|
if (i >= 0 && closeChars.contains(suggestion.at(i)) && rightText.contains(suggestion.at(i)))
|
||||||
const QStringList lineTokens = extractTokens(entireLine);
|
return rightText.size();
|
||||||
|
|
||||||
for (const auto &token : suggestionTokens) {
|
|
||||||
if (lineTokens.contains(token)) {
|
|
||||||
return rightText.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -82,10 +69,9 @@ LLMSuggestion::LLMSuggestion(
|
|||||||
QString rightText = blockText.mid(cursorPositionInBlock);
|
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||||
|
|
||||||
QString suggestionText = data.text;
|
QString suggestionText = data.text;
|
||||||
QString entireLine = blockText;
|
|
||||||
|
|
||||||
if (!suggestionText.contains('\n')) {
|
if (!suggestionText.contains('\n')) {
|
||||||
int replaceLength = calculateReplaceLength(suggestionText, rightText, entireLine);
|
int replaceLength = calculateReplaceLength(suggestionText, rightText);
|
||||||
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
||||||
|
|
||||||
QString displayText = leftText + suggestionText + remainingRightText;
|
QString displayText = leftText + suggestionText + remainingRightText;
|
||||||
@@ -95,7 +81,7 @@ LLMSuggestion::LLMSuggestion(
|
|||||||
QString firstLine = suggestionText.left(firstLineEnd);
|
QString firstLine = suggestionText.left(firstLineEnd);
|
||||||
QString restOfCompletion = suggestionText.mid(firstLineEnd);
|
QString restOfCompletion = suggestionText.mid(firstLineEnd);
|
||||||
|
|
||||||
int replaceLength = calculateReplaceLength(firstLine, rightText, entireLine);
|
int replaceLength = calculateReplaceLength(firstLine, rightText);
|
||||||
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
||||||
|
|
||||||
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
|
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
|
||||||
@@ -147,9 +133,8 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
|||||||
if (startPos == 0) {
|
if (startPos == 0) {
|
||||||
QTextBlock currentBlock = cursor.block();
|
QTextBlock currentBlock = cursor.block();
|
||||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||||
QString entireLine = currentBlock.text();
|
|
||||||
|
|
||||||
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
|
int replaceLength = calculateReplaceLength(text, textAfterCursor);
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
if (replaceLength > 0) {
|
||||||
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||||
@@ -200,9 +185,7 @@ bool LLMSuggestion::apply()
|
|||||||
QString text = currentData.text;
|
QString text = currentData.text;
|
||||||
|
|
||||||
QTextBlock currentBlock = cursor.block();
|
QTextBlock currentBlock = cursor.block();
|
||||||
QString textBeforeCursor = currentBlock.text().left(cursor.positionInBlock());
|
|
||||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||||
QString entireLine = currentBlock.text();
|
|
||||||
|
|
||||||
QTextCursor editCursor = cursor;
|
QTextCursor editCursor = cursor;
|
||||||
editCursor.beginEditBlock();
|
editCursor.beginEditBlock();
|
||||||
@@ -212,7 +195,7 @@ bool LLMSuggestion::apply()
|
|||||||
QString firstLine = text.left(firstLineEnd);
|
QString firstLine = text.left(firstLineEnd);
|
||||||
QString restOfText = text.mid(firstLineEnd);
|
QString restOfText = text.mid(firstLineEnd);
|
||||||
|
|
||||||
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor, entireLine);
|
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor);
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
if (replaceLength > 0) {
|
||||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||||
@@ -221,7 +204,7 @@ bool LLMSuggestion::apply()
|
|||||||
|
|
||||||
editCursor.insertText(firstLine + restOfText);
|
editCursor.insertText(firstLine + restOfText);
|
||||||
} else {
|
} else {
|
||||||
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
|
int replaceLength = calculateReplaceLength(text, textAfterCursor);
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
if (replaceLength > 0) {
|
||||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ public:
|
|||||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||||
bool apply() override;
|
bool apply() override;
|
||||||
|
|
||||||
static int calculateReplaceLength(const QString &suggestion,
|
static int calculateReplaceLength(const QString &suggestion, const QString &rightText);
|
||||||
const QString &rightText,
|
|
||||||
const QString &entireLine);
|
|
||||||
};
|
};
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -15,127 +15,97 @@ class LLMSuggestionTest : public QObject, public testing::Test
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basic tests
|
// Degenerate / no-op cases
|
||||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthEmptyRight)
|
TEST_F(LLMSuggestionTest, emptyRight)
|
||||||
{
|
{
|
||||||
int result = LLMSuggestion::calculateReplaceLength("foo", "", "foo");
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("foo", ""), 0);
|
||||||
EXPECT_EQ(result, 0); // No rightText to replace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthNoOverlap)
|
TEST_F(LLMSuggestionTest, noOverlap)
|
||||||
{
|
{
|
||||||
// No structural or token overlap
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("foo", "bar"), 0);
|
||||||
int result = LLMSuggestion::calculateReplaceLength("foo", "bar", "foobar");
|
|
||||||
EXPECT_EQ(result, 0); // Just insert, don't replace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structural overlap tests
|
TEST_F(LLMSuggestionTest, singleCharNoOverlap)
|
||||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthStructuralBraces)
|
|
||||||
{
|
{
|
||||||
// suggestion contains {}, rightText contains {}
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("a", "b"), 0);
|
||||||
int result = LLMSuggestion::calculateReplaceLength("= {\"red\"}", "{};", "colors{};");
|
|
||||||
EXPECT_EQ(result, 3); // Replace all rightText
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthStructuralSemicolon)
|
// LCP: model echoed the existing right side as its own prefix
|
||||||
|
TEST_F(LLMSuggestionTest, lcpExtendsRight)
|
||||||
{
|
{
|
||||||
// suggestion contains ;, rightText contains ;
|
// cursor: int |myVariable ; suggestion echoes "myVar" then adds nothing.
|
||||||
int result = LLMSuggestion::calculateReplaceLength("x;", ";", "int x;");
|
// LCP=5, remainder "iable" not a closing tail -> replace 5.
|
||||||
EXPECT_EQ(result, 1); // Replace the ;
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("myVar", "myVariable"), 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthStructuralParens)
|
TEST_F(LLMSuggestionTest, lcpPartialEngineCall)
|
||||||
{
|
{
|
||||||
// suggestion contains (), rightText contains )
|
// cursor: |engine.rootContext() ; suggestion: engine.load()
|
||||||
int result = LLMSuggestion::calculateReplaceLength("arg1, arg2)", ")", "foo(arg1, arg2)");
|
// LCP="engine." (7); remainder "rootContext()" is not closing-only -> keep LCP
|
||||||
EXPECT_EQ(result, 1); // Replace the )
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("engine.load()", "engine.rootContext()"), 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthStructuralBrackets)
|
// LCP + closing-only tail: extend to full right
|
||||||
|
TEST_F(LLMSuggestionTest, lcpThenClosingTailExtendsFull)
|
||||||
{
|
{
|
||||||
// suggestion contains [], rightText contains ]
|
// cursor: QStringList |colors{}; ; suggestion: colors << "red"
|
||||||
int result = LLMSuggestion::calculateReplaceLength("[0]", "];", "arr[0];");
|
// LCP=6 ("colors"); remainder "{};" is punctuation -> replace all (9)
|
||||||
EXPECT_EQ(result, 2); // Replace ];
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("colors << \"red\"", "colors{};"), 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token overlap tests
|
// LCP=0, right is closing-only, suggestion ends with a matching closer -> replace full right
|
||||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthCommonToken)
|
TEST_F(LLMSuggestionTest, closingTailReplaceSemicolon)
|
||||||
{
|
{
|
||||||
// suggestion contains "colors", entireLine contains "colors"
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("x;", ";"), 1);
|
||||||
int result = LLMSuggestion::calculateReplaceLength("colors << \"red\"", "colors{};", "QStringList colors{};");
|
|
||||||
EXPECT_EQ(result, 9); // Replace all rightText due to common token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthMultipleCommonTokens)
|
TEST_F(LLMSuggestionTest, closingTailReplaceParen)
|
||||||
{
|
{
|
||||||
// Multiple tokens in common
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("arg1, arg2)", ")"), 1);
|
||||||
int result = LLMSuggestion::calculateReplaceLength("engine.load()", "engine.rootContext()", "QmlEngine engine.rootContext()");
|
|
||||||
EXPECT_EQ(result, 20); // Replace all rightText
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Real-world scenarios
|
TEST_F(LLMSuggestionTest, closingTailReplaceBrackets)
|
||||||
TEST_F(LLMSuggestionTest, testCursorInBraces)
|
|
||||||
{
|
{
|
||||||
// Cursor in braces: QStringList colors{<cursor>};
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("[0]", "];"), 2);
|
||||||
// LLM sends: "\"red\", \"green\"", rightText: "};"
|
|
||||||
// No common tokens ("red" and "green" are strings, not identifiers in entireLine)
|
|
||||||
// No structural overlap (suggestion doesn't contain } or ;)
|
|
||||||
int result = LLMSuggestion::calculateReplaceLength("\"red\", \"green\"", "};", "QStringList colors{};");
|
|
||||||
EXPECT_EQ(result, 0); // No overlap, just insert
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCursorBeforeBraces)
|
TEST_F(LLMSuggestionTest, closingTailReplaceBracesAndSemi)
|
||||||
{
|
{
|
||||||
// Cursor before braces: QStringList colors<cursor>{};
|
// cursor: colors|{}; ; suggestion: = {"red"}
|
||||||
// LLM sends: " = {\"red\"}", rightText: "{};"
|
// LCP=0; right "{};" is closing-only; suggestion ends on "}" which is in right -> replace 3
|
||||||
int result = LLMSuggestion::calculateReplaceLength(" = {\"red\"}", "{};", "QStringList colors{};");
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("= {\"red\"}", "{};"), 3);
|
||||||
EXPECT_EQ(result, 3); // Structural overlap - replace all
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCursorAfterType)
|
TEST_F(LLMSuggestionTest, closingTailWithLeadingSpace)
|
||||||
{
|
{
|
||||||
// Cursor after type: QStringList <cursor>colors{};
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength(" = {\"red\"}", "{};"), 3);
|
||||||
// LLM sends: "colors << \"red\"", rightText: "colors{};"
|
|
||||||
int result = LLMSuggestion::calculateReplaceLength("colors << \"red\"", "colors{};", "QStringList colors{};");
|
|
||||||
EXPECT_EQ(result, 9); // Common token - replace all
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCursorInMiddleNoConflict)
|
TEST_F(LLMSuggestionTest, closingTailEqualsSemi)
|
||||||
{
|
{
|
||||||
// Cursor in middle: int <cursor>myVar = 5;
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("= 5;", ";"), 1);
|
||||||
// LLM sends: "myVar", rightText: " = 5;", entireLine: "int myVar = 5;"
|
|
||||||
// "myVar" is a common token -> replace rightText
|
|
||||||
int result = LLMSuggestion::calculateReplaceLength("myVar", " = 5;", "int myVar = 5;");
|
|
||||||
EXPECT_EQ(result, 5); // Common token found, replace all rightText
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testCursorWithEqualsSign)
|
// LCP=0, right is closing-only but suggestion does not end on any of its chars -> leave right alone
|
||||||
|
TEST_F(LLMSuggestionTest, closingTailNoMatchingClose)
|
||||||
{
|
{
|
||||||
// LLM sends code with = and ;
|
// right "};" closes; suggestion ends on '"' (not in close set) -> 0
|
||||||
int result = LLMSuggestion::calculateReplaceLength("= 5;", ";", "int x;");
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("\"red\", \"green\"", "};"), 0);
|
||||||
EXPECT_EQ(result, 1); // Structural overlap on ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edge cases
|
// Right side has real code (not closing-only) and no LCP -> leave it alone
|
||||||
TEST_F(LLMSuggestionTest, testNoStructuralButHasToken)
|
TEST_F(LLMSuggestionTest, realCodeRightNoLcp)
|
||||||
{
|
{
|
||||||
// Token overlap but no structural
|
// cursor: int |myVar = 5; ; suggestion: myVar
|
||||||
int result = LLMSuggestion::calculateReplaceLength("myVar", "myVariable", "int myVariable");
|
// LCP=0, right " = 5;" is not closing-only (has '5') -> 0
|
||||||
EXPECT_EQ(result, 0); // No structural overlap, tokens too different (length > 1 check)
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("myVar", " = 5;"), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testOnlyWhitespace)
|
// Trailing whitespace is not treated as a closer, suggestion has no closer -> leave alone
|
||||||
|
TEST_F(LLMSuggestionTest, trailingWhitespaceOnlyLeftAlone)
|
||||||
{
|
{
|
||||||
// rightText is just whitespace, but "code" is common token
|
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("code", " "), 0);
|
||||||
int result = LLMSuggestion::calculateReplaceLength("code", " ", "code ");
|
|
||||||
EXPECT_EQ(result, 3); // Common token "code", replace rightText
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(LLMSuggestionTest, testSingleCharTokenIgnored)
|
|
||||||
{
|
|
||||||
// Tokens must be > 1 character
|
|
||||||
int result = LLMSuggestion::calculateReplaceLength("a", "b", "ab");
|
|
||||||
EXPECT_EQ(result, 0); // Single char tokens ignored
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "LLMSuggestionTest.moc"
|
#include "LLMSuggestionTest.moc"
|
||||||
|
|||||||
Reference in New Issue
Block a user