mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-09 00:30:31 -05:00
Compare commits
6 Commits
v0.8.2
...
feat-impro
| Author | SHA1 | Date | |
|---|---|---|---|
| d3f3752864 | |||
| 0ca1decd97 | |||
| baf129f0dc | |||
| 8570b9667a | |||
| f5a445b021 | |||
| 30885c0373 |
12
.github/workflows/build_cmake.yml
vendored
12
.github/workflows/build_cmake.yml
vendored
@ -291,6 +291,18 @@ jobs:
|
||||
mkdir release
|
||||
mv release-with-dirs/*/* release/
|
||||
|
||||
- name: Download QodeAssistUpdater
|
||||
run: |
|
||||
# Get latest release info and download assets
|
||||
LATEST_RELEASE=$(curl -s https://api.github.com/repos/Palm1r/QodeAssistUpdater/releases/latest)
|
||||
|
||||
# Download all assets except .sha256 files
|
||||
echo "$LATEST_RELEASE" | jq -r '.assets[].browser_download_url' | grep -v '\.sha256$' | while read url; do
|
||||
filename=$(basename "$url")
|
||||
echo "Downloading $filename..."
|
||||
curl -L -o "release/$filename" "$url"
|
||||
done
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
|
||||
@ -217,11 +217,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::isThinkingSupportChanged);
|
||||
connect(
|
||||
&Settings::toolsSettings().debugToolsAndThinkingComponent,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::isToolDebugging);
|
||||
}
|
||||
|
||||
ChatModel *ChatRootView::chatModel() const
|
||||
@ -1131,9 +1126,5 @@ bool ChatRootView::isThinkingSupport() const
|
||||
return provider && provider->supportThinking();
|
||||
}
|
||||
|
||||
bool ChatRootView::isToolDebugging() const
|
||||
{
|
||||
return Settings::toolsSettings().debugToolsAndThinkingComponent();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -58,7 +58,6 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
||||
Q_PROPERTY(bool isToolDebugging READ isToolDebugging NOTIFY isToolDebuggingChanged FINAL)
|
||||
|
||||
QML_ELEMENT
|
||||
|
||||
@ -143,8 +142,6 @@ public:
|
||||
|
||||
bool isThinkingSupport() const;
|
||||
|
||||
bool isToolDebugging() const;
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message);
|
||||
void copyToClipboard(const QString &text);
|
||||
@ -180,8 +177,6 @@ signals:
|
||||
|
||||
void isThinkingSupportChanged();
|
||||
|
||||
void isToolDebuggingChanged();
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||
QString getChatsHistoryDir() const;
|
||||
|
||||
@ -135,7 +135,7 @@ ChatRootView {
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (componentLoader.sourceComponent == chatItemComponent && !root.isToolDebugging) {
|
||||
if (componentLoader.sourceComponent == chatItemComponent) {
|
||||
chatListView.hideServiceComponents(index)
|
||||
}
|
||||
}
|
||||
@ -194,15 +194,11 @@ ChatRootView {
|
||||
width: parent.width
|
||||
toolContent: model.content
|
||||
|
||||
FadeListItemAnimation{
|
||||
id: toolFadeAnimation
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: chatListView
|
||||
function onHideServiceComponents(itemIndex) {
|
||||
if (index !== itemIndex) {
|
||||
toolFadeAnimation.start()
|
||||
toolsItem.headerOpacity = 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -238,6 +234,8 @@ ChatRootView {
|
||||
id: thinkingMessageComponent
|
||||
|
||||
ThinkingStatusItem {
|
||||
id: thinking
|
||||
|
||||
width: parent.width
|
||||
thinkingContent: {
|
||||
let content = model.content
|
||||
@ -249,15 +247,11 @@ ChatRootView {
|
||||
}
|
||||
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
|
||||
|
||||
FadeListItemAnimation{
|
||||
id: thinkingFadeAnimation
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: chatListView
|
||||
function onHideServiceComponents(itemIndex) {
|
||||
if (index !== itemIndex) {
|
||||
thinkingFadeAnimation.start()
|
||||
thinking.headerOpacity = 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@ Rectangle {
|
||||
property bool isRedacted: false
|
||||
property bool expanded: false
|
||||
|
||||
property alias headerOpacity: headerRow.opacity
|
||||
|
||||
radius: 6
|
||||
color: palette.base
|
||||
clip: true
|
||||
|
||||
@ -26,6 +26,8 @@ Rectangle {
|
||||
property string toolContent: ""
|
||||
property bool expanded: false
|
||||
|
||||
property alias headerOpacity: headerRow.opacity
|
||||
|
||||
readonly property int firstNewline: toolContent.indexOf('\n')
|
||||
readonly property string toolName: firstNewline > 0 ? toolContent.substring(0, firstNewline) : toolContent
|
||||
readonly property string toolResult: firstNewline > 0 ? toolContent.substring(firstNewline + 1) : ""
|
||||
|
||||
@ -418,12 +418,20 @@ void LLMClientInterface::sendCompletionToClient(
|
||||
: completion;
|
||||
}
|
||||
|
||||
if (processedCompletion.endsWith('\n')) {
|
||||
QString withoutTrailing = processedCompletion.chopped(1);
|
||||
if (!withoutTrailing.contains('\n')) {
|
||||
LOG_MESSAGE(QString("Removed trailing newline from single-line completion"));
|
||||
processedCompletion = withoutTrailing;
|
||||
}
|
||||
}
|
||||
|
||||
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
|
||||
|
||||
QJsonObject range;
|
||||
range["start"] = position;
|
||||
QJsonObject end = position;
|
||||
end["character"] = position["character"].toInt() + processedCompletion.length();
|
||||
range["end"] = end;
|
||||
range["end"] = position;
|
||||
|
||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||
completions.append(completionItem);
|
||||
|
||||
@ -29,34 +29,57 @@
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
QString mergeWithRightText(const QString &suggestion, const QString &rightText)
|
||||
static QStringList extractTokens(const QString &str)
|
||||
{
|
||||
if (suggestion.isEmpty() || rightText.isEmpty()) {
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
int j = 0;
|
||||
QString processed = rightText;
|
||||
QSet<int> matchedPositions;
|
||||
|
||||
for (int i = 0; i < suggestion.length() && j < processed.length(); ++i) {
|
||||
if (suggestion[i] == processed[j]) {
|
||||
matchedPositions.insert(j);
|
||||
++j;
|
||||
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;
|
||||
}
|
||||
|
||||
if (matchedPositions.isEmpty()) {
|
||||
return suggestion + rightText;
|
||||
int LLMSuggestion::calculateReplaceLength(const QString &suggestion,
|
||||
const QString &rightText,
|
||||
const QString &entireLine)
|
||||
{
|
||||
if (rightText.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
QList<int> positions = matchedPositions.values();
|
||||
std::sort(positions.begin(), positions.end(), std::greater<int>());
|
||||
for (int pos : positions) {
|
||||
processed.remove(pos, 1);
|
||||
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();
|
||||
}
|
||||
|
||||
return suggestion;
|
||||
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(
|
||||
@ -66,10 +89,8 @@ LLMSuggestion::LLMSuggestion(
|
||||
const auto &data = suggestions[currentCompletion];
|
||||
|
||||
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
|
||||
int endPos = data.range.end.toPositionInDocument(sourceDocument);
|
||||
|
||||
startPos = qBound(0, startPos, sourceDocument->characterCount());
|
||||
endPos = qBound(startPos, endPos, sourceDocument->characterCount());
|
||||
|
||||
QTextCursor cursor(sourceDocument);
|
||||
cursor.setPosition(startPos);
|
||||
@ -77,17 +98,27 @@ LLMSuggestion::LLMSuggestion(
|
||||
QString blockText = block.text();
|
||||
|
||||
int cursorPositionInBlock = cursor.positionInBlock();
|
||||
|
||||
QString leftText = blockText.left(cursorPositionInBlock);
|
||||
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||
|
||||
if (!data.text.contains('\n')) {
|
||||
QString processedRightText = mergeWithRightText(data.text, rightText);
|
||||
processedRightText = processedRightText.mid(data.text.length());
|
||||
QString displayText = blockText.left(cursorPositionInBlock) + data.text
|
||||
+ processedRightText;
|
||||
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 {
|
||||
QString displayText = blockText.left(cursorPositionInBlock) + data.text;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -104,10 +135,12 @@ bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
|
||||
|
||||
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||
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 = suggestions()[currentSuggestion()].text;
|
||||
const QString text = currentData.text;
|
||||
|
||||
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
|
||||
+ (cursor.selectionEnd() - cursor.selectionStart());
|
||||
@ -131,6 +164,19 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||
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);
|
||||
|
||||
@ -167,34 +213,47 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||
|
||||
bool LLMSuggestion::apply()
|
||||
{
|
||||
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||
const auto ¤tSuggestions = suggestions();
|
||||
const auto ¤tData = currentSuggestions[currentSuggestion()];
|
||||
const Utils::Text::Range range = currentData.range;
|
||||
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||
const QString text = suggestions()[currentSuggestion()].text;
|
||||
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);
|
||||
|
||||
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
editCursor.removeSelectedText();
|
||||
|
||||
QString mergedFirstLine = mergeWithRightText(firstLine, textAfterCursor);
|
||||
editCursor.insertText(mergedFirstLine + restOfText);
|
||||
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor, entireLine);
|
||||
|
||||
if (replaceLength > 0) {
|
||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||
editCursor.removeSelectedText();
|
||||
}
|
||||
|
||||
editCursor.insertText(firstLine + restOfText);
|
||||
} else {
|
||||
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
editCursor.removeSelectedText();
|
||||
|
||||
QString mergedText = mergeWithRightText(text, textAfterCursor);
|
||||
editCursor.insertText(mergedText);
|
||||
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
|
||||
|
||||
|
||||
@ -41,5 +41,9 @@ public:
|
||||
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||
bool apply() override;
|
||||
|
||||
static int calculateReplaceLength(const QString &suggestion,
|
||||
const QString &rightText,
|
||||
const QString &entireLine);
|
||||
};
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.8.2",
|
||||
"Version" : "0.8.3",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
|
||||
@ -100,7 +100,49 @@ void GoogleAIProvider::prepareRequest(
|
||||
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||
applyModelParams(Settings::codeCompletionSettings());
|
||||
} else {
|
||||
applyModelParams(Settings::chatAssistantSettings());
|
||||
const auto &chatSettings = Settings::chatAssistantSettings();
|
||||
|
||||
if (chatSettings.enableThinkingMode()) {
|
||||
QJsonObject generationConfig;
|
||||
generationConfig["maxOutputTokens"] = chatSettings.thinkingMaxTokens();
|
||||
|
||||
if (chatSettings.useTopP())
|
||||
generationConfig["topP"] = chatSettings.topP();
|
||||
if (chatSettings.useTopK())
|
||||
generationConfig["topK"] = chatSettings.topK();
|
||||
|
||||
// Set temperature to 1.0 for thinking mode
|
||||
generationConfig["temperature"] = 1.0;
|
||||
|
||||
// Add thinkingConfig
|
||||
QJsonObject thinkingConfig;
|
||||
int budgetTokens = chatSettings.thinkingBudgetTokens();
|
||||
|
||||
// Dynamic thinking: -1 (let model decide)
|
||||
// Disabled: 0 (no thinking)
|
||||
// Custom budget: positive integer
|
||||
if (budgetTokens == -1) {
|
||||
// Dynamic thinking - omit budget to let model decide
|
||||
thinkingConfig["includeThoughts"] = true;
|
||||
} else if (budgetTokens == 0) {
|
||||
// Disabled thinking
|
||||
thinkingConfig["thinkingBudget"] = 0;
|
||||
thinkingConfig["includeThoughts"] = false;
|
||||
} else {
|
||||
// Custom budget
|
||||
thinkingConfig["thinkingBudget"] = budgetTokens;
|
||||
thinkingConfig["includeThoughts"] = true;
|
||||
}
|
||||
|
||||
generationConfig["thinkingConfig"] = thinkingConfig;
|
||||
request["generationConfig"] = generationConfig;
|
||||
|
||||
LOG_MESSAGE(QString("Google AI thinking mode enabled: budget=%1 tokens, maxTokens=%2")
|
||||
.arg(budgetTokens)
|
||||
.arg(chatSettings.thinkingMaxTokens()));
|
||||
} else {
|
||||
applyModelParams(chatSettings);
|
||||
}
|
||||
}
|
||||
|
||||
if (isToolsEnabled) {
|
||||
@ -164,7 +206,13 @@ QList<QString> GoogleAIProvider::validateRequest(
|
||||
{"contents", QJsonArray{}},
|
||||
{"system_instruction", QJsonArray{}},
|
||||
{"generationConfig",
|
||||
QJsonObject{{"temperature", {}}, {"maxOutputTokens", {}}, {"topP", {}}, {"topK", {}}}},
|
||||
QJsonObject{
|
||||
{"temperature", {}},
|
||||
{"maxOutputTokens", {}},
|
||||
{"topP", {}},
|
||||
{"topK", {}},
|
||||
{"thinkingConfig",
|
||||
QJsonObject{{"thinkingBudget", {}}, {"includeThoughts", {}}}}}},
|
||||
{"safetySettings", QJsonArray{}},
|
||||
{"tools", QJsonArray{}}};
|
||||
|
||||
@ -219,6 +267,11 @@ bool GoogleAIProvider::supportsTools() const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GoogleAIProvider::supportThinking() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void GoogleAIProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||
{
|
||||
LOG_MESSAGE(QString("GoogleAIProvider: Cancelling request %1").arg(requestId));
|
||||
@ -277,8 +330,18 @@ void GoogleAIProvider::onRequestFinished(
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_failedRequests.contains(requestId)) {
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
emitPendingThinkingBlocks(requestId);
|
||||
|
||||
if (m_messages.contains(requestId)) {
|
||||
GoogleMessage *message = m_messages[requestId];
|
||||
|
||||
handleMessageComplete(requestId);
|
||||
|
||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId));
|
||||
m_dataBuffers.remove(requestId);
|
||||
@ -289,9 +352,12 @@ void GoogleAIProvider::onRequestFinished(
|
||||
if (m_dataBuffers.contains(requestId)) {
|
||||
const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
if (!buffers.responseContent.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Emitting full response for %1").arg(requestId));
|
||||
emit fullResponseReceived(requestId, buffers.responseContent);
|
||||
} else {
|
||||
emit fullResponseReceived(requestId, QString());
|
||||
}
|
||||
} else {
|
||||
emit fullResponseReceived(requestId, QString());
|
||||
}
|
||||
|
||||
cleanupRequest(requestId);
|
||||
@ -306,8 +372,6 @@ void GoogleAIProvider::onToolExecutionComplete(
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Tool execution complete for Google AI request %1").arg(requestId));
|
||||
|
||||
for (auto it = toolResults.begin(); it != toolResults.end(); ++it) {
|
||||
GoogleMessage *message = m_messages[requestId];
|
||||
auto toolContent = message->getCurrentToolUseContent();
|
||||
@ -334,10 +398,6 @@ void GoogleAIProvider::onToolExecutionComplete(
|
||||
|
||||
continuationRequest["contents"] = contents;
|
||||
|
||||
LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results")
|
||||
.arg(requestId)
|
||||
.arg(toolResults.size()));
|
||||
|
||||
sendRequest(requestId, m_requestUrls[requestId], continuationRequest);
|
||||
}
|
||||
|
||||
@ -361,6 +421,7 @@ void GoogleAIProvider::processStreamChunk(const QString &requestId, const QJsonO
|
||||
m_dataBuffers.contains(requestId)
|
||||
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||
message->startNewContinuation();
|
||||
m_emittedThinkingBlocksCount[requestId] = 0;
|
||||
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
@ -377,12 +438,34 @@ void GoogleAIProvider::processStreamChunk(const QString &requestId, const QJsonO
|
||||
|
||||
if (partObj.contains("text")) {
|
||||
QString text = partObj["text"].toString();
|
||||
message->handleContentDelta(text);
|
||||
bool isThought = partObj.value("thought").toBool(false);
|
||||
|
||||
if (isThought) {
|
||||
message->handleThoughtDelta(text);
|
||||
|
||||
if (partObj.contains("signature")) {
|
||||
QString signature = partObj["signature"].toString();
|
||||
message->handleThoughtSignature(signature);
|
||||
}
|
||||
} else {
|
||||
emitPendingThinkingBlocks(requestId);
|
||||
|
||||
message->handleContentDelta(text);
|
||||
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
buffers.responseContent += text;
|
||||
emit partialResponseReceived(requestId, text);
|
||||
} else if (partObj.contains("functionCall")) {
|
||||
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||
buffers.responseContent += text;
|
||||
emit partialResponseReceived(requestId, text);
|
||||
}
|
||||
}
|
||||
|
||||
if (partObj.contains("thoughtSignature")) {
|
||||
QString signature = partObj["thoughtSignature"].toString();
|
||||
message->handleThoughtSignature(signature);
|
||||
}
|
||||
|
||||
if (partObj.contains("functionCall")) {
|
||||
emitPendingThinkingBlocks(requestId);
|
||||
|
||||
QJsonObject functionCall = partObj["functionCall"].toObject();
|
||||
QString name = functionCall["name"].toString();
|
||||
QJsonObject args = functionCall["args"].toObject();
|
||||
@ -399,9 +482,55 @@ void GoogleAIProvider::processStreamChunk(const QString &requestId, const QJsonO
|
||||
if (candidateObj.contains("finishReason")) {
|
||||
QString finishReason = candidateObj["finishReason"].toString();
|
||||
message->handleFinishReason(finishReason);
|
||||
handleMessageComplete(requestId);
|
||||
|
||||
if (message->isErrorFinishReason()) {
|
||||
QString errorMessage = message->getErrorMessage();
|
||||
LOG_MESSAGE(QString("Google AI error: %1").arg(errorMessage));
|
||||
m_failedRequests.insert(requestId);
|
||||
emit requestFailed(requestId, errorMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.contains("usageMetadata")) {
|
||||
QJsonObject usageMetadata = chunk["usageMetadata"].toObject();
|
||||
int thoughtsTokenCount = usageMetadata.value("thoughtsTokenCount").toInt(0);
|
||||
int candidatesTokenCount = usageMetadata.value("candidatesTokenCount").toInt(0);
|
||||
int totalTokenCount = usageMetadata.value("totalTokenCount").toInt(0);
|
||||
|
||||
if (totalTokenCount > 0) {
|
||||
LOG_MESSAGE(QString("Google AI tokens: %1 (thoughts: %2, output: %3)")
|
||||
.arg(totalTokenCount)
|
||||
.arg(thoughtsTokenCount)
|
||||
.arg(candidatesTokenCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GoogleAIProvider::emitPendingThinkingBlocks(const QString &requestId)
|
||||
{
|
||||
if (!m_messages.contains(requestId))
|
||||
return;
|
||||
|
||||
GoogleMessage *message = m_messages[requestId];
|
||||
auto thinkingBlocks = message->getCurrentThinkingContent();
|
||||
|
||||
if (thinkingBlocks.isEmpty())
|
||||
return;
|
||||
|
||||
int alreadyEmitted = m_emittedThinkingBlocksCount.value(requestId, 0);
|
||||
int totalBlocks = thinkingBlocks.size();
|
||||
|
||||
for (int i = alreadyEmitted; i < totalBlocks; ++i) {
|
||||
auto thinkingContent = thinkingBlocks[i];
|
||||
emit thinkingBlockReceived(
|
||||
requestId,
|
||||
thinkingContent->thinking(),
|
||||
thinkingContent->signature());
|
||||
}
|
||||
|
||||
m_emittedThinkingBlocksCount[requestId] = totalBlocks;
|
||||
}
|
||||
|
||||
void GoogleAIProvider::handleMessageComplete(const QString &requestId)
|
||||
@ -445,6 +574,8 @@ void GoogleAIProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||
m_dataBuffers.remove(requestId);
|
||||
m_requestUrls.remove(requestId);
|
||||
m_originalRequests.remove(requestId);
|
||||
m_emittedThinkingBlocksCount.remove(requestId);
|
||||
m_failedRequests.remove(requestId);
|
||||
m_toolsManager->cleanupRequest(requestId);
|
||||
}
|
||||
|
||||
|
||||
@ -52,6 +52,7 @@ public:
|
||||
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override;
|
||||
|
||||
bool supportsTools() const override;
|
||||
bool supportThinking() const override;
|
||||
void cancelRequest(const LLMCore::RequestID &requestId) override;
|
||||
|
||||
public slots:
|
||||
@ -69,11 +70,14 @@ private slots:
|
||||
private:
|
||||
void processStreamChunk(const QString &requestId, const QJsonObject &chunk);
|
||||
void handleMessageComplete(const QString &requestId);
|
||||
void emitPendingThinkingBlocks(const QString &requestId);
|
||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||
|
||||
QHash<LLMCore::RequestID, GoogleMessage *> m_messages;
|
||||
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||
QHash<LLMCore::RequestID, int> m_emittedThinkingBlocksCount;
|
||||
QSet<LLMCore::RequestID> m_failedRequests;
|
||||
Tools::ToolsManager *m_toolsManager;
|
||||
};
|
||||
|
||||
|
||||
@ -43,12 +43,38 @@ void GoogleMessage::handleContentDelta(const QString &text)
|
||||
}
|
||||
}
|
||||
|
||||
void GoogleMessage::handleThoughtDelta(const QString &text)
|
||||
{
|
||||
if (m_currentBlocks.isEmpty() || !qobject_cast<LLMCore::ThinkingContent *>(m_currentBlocks.last())) {
|
||||
auto thinkingContent = new LLMCore::ThinkingContent();
|
||||
thinkingContent->setParent(this);
|
||||
m_currentBlocks.append(thinkingContent);
|
||||
}
|
||||
|
||||
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(m_currentBlocks.last())) {
|
||||
thinkingContent->appendThinking(text);
|
||||
}
|
||||
}
|
||||
|
||||
void GoogleMessage::handleThoughtSignature(const QString &signature)
|
||||
{
|
||||
for (int i = m_currentBlocks.size() - 1; i >= 0; --i) {
|
||||
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(m_currentBlocks[i])) {
|
||||
thinkingContent->setSignature(signature);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto thinkingContent = new LLMCore::ThinkingContent();
|
||||
thinkingContent->setParent(this);
|
||||
thinkingContent->setSignature(signature);
|
||||
m_currentBlocks.append(thinkingContent);
|
||||
}
|
||||
|
||||
void GoogleMessage::handleFunctionCallStart(const QString &name)
|
||||
{
|
||||
m_currentFunctionName = name;
|
||||
m_pendingFunctionArgs.clear();
|
||||
|
||||
LOG_MESSAGE(QString("Google: Starting function call: %1").arg(name));
|
||||
}
|
||||
|
||||
void GoogleMessage::handleFunctionCallArgsDelta(const QString &argsJson)
|
||||
@ -75,10 +101,6 @@ void GoogleMessage::handleFunctionCallComplete()
|
||||
toolContent->setParent(this);
|
||||
m_currentBlocks.append(toolContent);
|
||||
|
||||
LOG_MESSAGE(QString("Google: Completed function call: name=%1, args=%2")
|
||||
.arg(m_currentFunctionName)
|
||||
.arg(QString::fromUtf8(QJsonDocument(args).toJson(QJsonDocument::Compact))));
|
||||
|
||||
m_currentFunctionName.clear();
|
||||
m_pendingFunctionArgs.clear();
|
||||
}
|
||||
@ -87,9 +109,6 @@ void GoogleMessage::handleFinishReason(const QString &reason)
|
||||
{
|
||||
m_finishReason = reason;
|
||||
updateStateFromFinishReason();
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("Google: Finish reason: %1, state: %2").arg(reason).arg(static_cast<int>(m_state)));
|
||||
}
|
||||
|
||||
QJsonObject GoogleMessage::toProviderFormat() const
|
||||
@ -110,6 +129,19 @@ QJsonObject GoogleMessage::toProviderFormat() const
|
||||
functionCall["name"] = tool->name();
|
||||
functionCall["args"] = tool->input();
|
||||
parts.append(QJsonObject{{"functionCall", functionCall}});
|
||||
} else if (auto thinking = qobject_cast<LLMCore::ThinkingContent *>(block)) {
|
||||
// Include thinking blocks with their text
|
||||
QJsonObject thinkingPart;
|
||||
thinkingPart["text"] = thinking->thinking();
|
||||
thinkingPart["thought"] = true;
|
||||
parts.append(thinkingPart);
|
||||
|
||||
// If there's a signature, add it as a separate part
|
||||
if (!thinking->signature().isEmpty()) {
|
||||
QJsonObject signaturePart;
|
||||
signaturePart["thoughtSignature"] = thinking->signature();
|
||||
parts.append(signaturePart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,6 +180,17 @@ QList<LLMCore::ToolUseContent *> GoogleMessage::getCurrentToolUseContent() const
|
||||
return toolBlocks;
|
||||
}
|
||||
|
||||
QList<LLMCore::ThinkingContent *> GoogleMessage::getCurrentThinkingContent() const
|
||||
{
|
||||
QList<LLMCore::ThinkingContent *> thinkingBlocks;
|
||||
for (auto block : m_currentBlocks) {
|
||||
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(block)) {
|
||||
thinkingBlocks.append(thinkingContent);
|
||||
}
|
||||
}
|
||||
return thinkingBlocks;
|
||||
}
|
||||
|
||||
void GoogleMessage::startNewContinuation()
|
||||
{
|
||||
LOG_MESSAGE(QString("GoogleMessage: Starting new continuation"));
|
||||
@ -159,6 +202,34 @@ void GoogleMessage::startNewContinuation()
|
||||
m_state = LLMCore::MessageState::Building;
|
||||
}
|
||||
|
||||
bool GoogleMessage::isErrorFinishReason() const
|
||||
{
|
||||
return m_finishReason == "SAFETY"
|
||||
|| m_finishReason == "RECITATION"
|
||||
|| m_finishReason == "MALFORMED_FUNCTION_CALL"
|
||||
|| m_finishReason == "PROHIBITED_CONTENT"
|
||||
|| m_finishReason == "SPII"
|
||||
|| m_finishReason == "OTHER";
|
||||
}
|
||||
|
||||
QString GoogleMessage::getErrorMessage() const
|
||||
{
|
||||
if (m_finishReason == "SAFETY") {
|
||||
return "Response blocked by safety filters";
|
||||
} else if (m_finishReason == "RECITATION") {
|
||||
return "Response blocked due to recitation of copyrighted content";
|
||||
} else if (m_finishReason == "MALFORMED_FUNCTION_CALL") {
|
||||
return "Model attempted to call a function with malformed arguments. Please try rephrasing your request or disabling tools.";
|
||||
} else if (m_finishReason == "PROHIBITED_CONTENT") {
|
||||
return "Response blocked due to prohibited content";
|
||||
} else if (m_finishReason == "SPII") {
|
||||
return "Response blocked due to sensitive personally identifiable information";
|
||||
} else if (m_finishReason == "OTHER") {
|
||||
return "Request failed due to an unknown reason";
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void GoogleMessage::updateStateFromFinishReason()
|
||||
{
|
||||
if (m_finishReason == "STOP" || m_finishReason == "MAX_TOKENS") {
|
||||
|
||||
@ -35,6 +35,8 @@ public:
|
||||
explicit GoogleMessage(QObject *parent = nullptr);
|
||||
|
||||
void handleContentDelta(const QString &text);
|
||||
void handleThoughtDelta(const QString &text);
|
||||
void handleThoughtSignature(const QString &signature);
|
||||
void handleFunctionCallStart(const QString &name);
|
||||
void handleFunctionCallArgsDelta(const QString &argsJson);
|
||||
void handleFunctionCallComplete();
|
||||
@ -44,9 +46,13 @@ public:
|
||||
QJsonArray createToolResultParts(const QHash<QString, QString> &toolResults) const;
|
||||
|
||||
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
||||
QList<LLMCore::ThinkingContent *> getCurrentThinkingContent() const;
|
||||
QList<LLMCore::ContentBlock *> currentBlocks() const { return m_currentBlocks; }
|
||||
|
||||
LLMCore::MessageState state() const { return m_state; }
|
||||
QString finishReason() const { return m_finishReason; }
|
||||
bool isErrorFinishReason() const;
|
||||
QString getErrorMessage() const;
|
||||
void startNewContinuation();
|
||||
|
||||
private:
|
||||
|
||||
@ -91,7 +91,6 @@ const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite";
|
||||
const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject";
|
||||
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool";
|
||||
const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectTool";
|
||||
const char CA_DEBUG_TOOLS_AND_THINKING_COMPONENT[] = "QodeAssist.caDebugToolsAndThinkingComponent";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||
|
||||
@ -89,12 +89,6 @@ ToolsSettings::ToolsSettings()
|
||||
"project. This feature is under testing and may have unexpected behavior."));
|
||||
enableBuildProjectTool.setDefaultValue(false);
|
||||
|
||||
debugToolsAndThinkingComponent.setSettingsKey(Constants::CA_DEBUG_TOOLS_AND_THINKING_COMPONENT);
|
||||
debugToolsAndThinkingComponent.setLabelText(Tr::tr("Always show Tools and Thinking Components in chat"));
|
||||
debugToolsAndThinkingComponent.setToolTip(
|
||||
Tr::tr("Disable disapearing tools and thinking component from chat"));
|
||||
debugToolsAndThinkingComponent.setDefaultValue(false);
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
readSettings();
|
||||
@ -114,8 +108,7 @@ ToolsSettings::ToolsSettings()
|
||||
Space{8},
|
||||
allowFileSystemRead,
|
||||
allowFileSystemWrite,
|
||||
allowAccessOutsideProject,
|
||||
debugToolsAndThinkingComponent
|
||||
allowAccessOutsideProject
|
||||
}},
|
||||
Space{8},
|
||||
Group{
|
||||
@ -151,7 +144,6 @@ void ToolsSettings::resetSettingsToDefaults()
|
||||
resetAspect(autoApplyFileEdits);
|
||||
resetAspect(enableEditFileTool);
|
||||
resetAspect(enableBuildProjectTool);
|
||||
resetAspect(debugToolsAndThinkingComponent);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,6 @@ public:
|
||||
Utils::BoolAspect allowFileSystemRead{this};
|
||||
Utils::BoolAspect allowFileSystemWrite{this};
|
||||
Utils::BoolAspect allowAccessOutsideProject{this};
|
||||
Utils::BoolAspect debugToolsAndThinkingComponent{this};
|
||||
|
||||
// Experimental features
|
||||
Utils::BoolAspect enableEditFileTool{this};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
@ -47,13 +47,39 @@ UpdateDialog::UpdateDialog(QWidget *parent)
|
||||
m_layout->addWidget(supportLabel);
|
||||
|
||||
auto *supportLink = new QLabel(
|
||||
tr("<a href='https://ko-fi.com/qodeassist' style='color: #0066cc;'>Support on Ko-fi "
|
||||
"☕</a>"),
|
||||
"<a href='https://ko-fi.com/qodeassist' style='color: #0066cc;'>Support on Ko-fi "
|
||||
"☕</a>",
|
||||
this);
|
||||
supportLink->setOpenExternalLinks(true);
|
||||
supportLink->setTextFormat(Qt::RichText);
|
||||
supportLink->setAlignment(Qt::AlignCenter);
|
||||
m_layout->addWidget(supportLink);
|
||||
auto *githubSupportLink = new QLabel(
|
||||
"<a "
|
||||
"href='https://github.com/Palm1r/"
|
||||
"QodeAssist?tab=readme-ov-file#support-the-development-of-qodeassist' style='color: #0066cc;' > Support page on github </a>",
|
||||
this);
|
||||
githubSupportLink->setOpenExternalLinks(true);
|
||||
githubSupportLink->setTextFormat(Qt::RichText);
|
||||
githubSupportLink->setAlignment(Qt::AlignCenter);
|
||||
m_layout->addWidget(githubSupportLink);
|
||||
|
||||
m_layout->addSpacing(20);
|
||||
|
||||
auto *updaterInfoLabel = new QLabel(
|
||||
tr("QodeAssistUpdater - convenient tool for plugin installation and updates"),
|
||||
this);
|
||||
updaterInfoLabel->setAlignment(Qt::AlignCenter);
|
||||
updaterInfoLabel->setWordWrap(true);
|
||||
m_layout->addWidget(updaterInfoLabel);
|
||||
|
||||
m_buttonOpenUpdaterRelease = new QPushButton(tr("Download QodeAssistUpdater"), this);
|
||||
m_buttonOpenUpdaterRelease->setMaximumWidth(250);
|
||||
auto *updaterButtonLayout = new QHBoxLayout;
|
||||
updaterButtonLayout->addStretch();
|
||||
updaterButtonLayout->addWidget(m_buttonOpenUpdaterRelease);
|
||||
updaterButtonLayout->addStretch();
|
||||
m_layout->addLayout(updaterButtonLayout);
|
||||
|
||||
m_layout->addSpacing(20);
|
||||
|
||||
@ -90,6 +116,7 @@ UpdateDialog::UpdateDialog(QWidget *parent)
|
||||
connect(m_updater, &PluginUpdater::updateCheckFinished, this, &UpdateDialog::handleUpdateInfo);
|
||||
connect(m_buttonOpenReleasePage, &QPushButton::clicked, this, &UpdateDialog::openReleasePage);
|
||||
connect(m_buttonOpenPluginFolder, &QPushButton::clicked, this, &UpdateDialog::openPluginFolder);
|
||||
connect(m_buttonOpenUpdaterRelease, &QPushButton::clicked, this, &UpdateDialog::openUpdaterReleasePage);
|
||||
connect(m_closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
|
||||
m_updater->checkForUpdates();
|
||||
@ -145,4 +172,9 @@ void UpdateDialog::openPluginFolder()
|
||||
accept();
|
||||
}
|
||||
|
||||
void UpdateDialog::openUpdaterReleasePage()
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl("https://github.com/Palm1r/QodeAssistUpdater"));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
@ -43,6 +43,7 @@ private slots:
|
||||
void handleUpdateInfo(const PluginUpdater::UpdateInfo &info);
|
||||
void openReleasePage();
|
||||
void openPluginFolder();
|
||||
void openUpdaterReleasePage();
|
||||
|
||||
private:
|
||||
PluginUpdater *m_updater;
|
||||
@ -53,6 +54,7 @@ private:
|
||||
QTextEdit *m_changelogText;
|
||||
QPushButton *m_buttonOpenReleasePage;
|
||||
QPushButton *m_buttonOpenPluginFolder;
|
||||
QPushButton *m_buttonOpenUpdaterRelease;
|
||||
QPushButton *m_closeButton;
|
||||
PluginUpdater::UpdateInfo m_updateInfo;
|
||||
};
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
add_executable(QodeAssistTest
|
||||
../CodeHandler.cpp
|
||||
../LLMClientInterface.cpp
|
||||
../LLMSuggestion.cpp
|
||||
CodeHandlerTest.cpp
|
||||
DocumentContextReaderTest.cpp
|
||||
LLMSuggestionTest.cpp
|
||||
# LLMClientInterfaceTests.cpp
|
||||
unittest_main.cpp
|
||||
)
|
||||
|
||||
157
test/LLMSuggestionTest.cpp
Normal file
157
test/LLMSuggestionTest.cpp
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "LLMSuggestion.hpp"
|
||||
#include "TestUtils.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
using namespace QodeAssist;
|
||||
|
||||
class LLMSuggestionTest : public QObject, public testing::Test
|
||||
{
|
||||
Q_OBJECT
|
||||
};
|
||||
|
||||
// Basic tests
|
||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthEmptyRight)
|
||||
{
|
||||
int result = LLMSuggestion::calculateReplaceLength("foo", "", "foo");
|
||||
EXPECT_EQ(result, 0); // No rightText to replace
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthNoOverlap)
|
||||
{
|
||||
// No structural or token overlap
|
||||
int result = LLMSuggestion::calculateReplaceLength("foo", "bar", "foobar");
|
||||
EXPECT_EQ(result, 0); // Just insert, don't replace
|
||||
}
|
||||
|
||||
// Structural overlap tests
|
||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthStructuralBraces)
|
||||
{
|
||||
// suggestion contains {}, rightText contains {}
|
||||
int result = LLMSuggestion::calculateReplaceLength("= {\"red\"}", "{};", "colors{};");
|
||||
EXPECT_EQ(result, 3); // Replace all rightText
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthStructuralSemicolon)
|
||||
{
|
||||
// suggestion contains ;, rightText contains ;
|
||||
int result = LLMSuggestion::calculateReplaceLength("x;", ";", "int x;");
|
||||
EXPECT_EQ(result, 1); // Replace the ;
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthStructuralParens)
|
||||
{
|
||||
// suggestion contains (), rightText contains )
|
||||
int result = LLMSuggestion::calculateReplaceLength("arg1, arg2)", ")", "foo(arg1, arg2)");
|
||||
EXPECT_EQ(result, 1); // Replace the )
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthStructuralBrackets)
|
||||
{
|
||||
// suggestion contains [], rightText contains ]
|
||||
int result = LLMSuggestion::calculateReplaceLength("[0]", "];", "arr[0];");
|
||||
EXPECT_EQ(result, 2); // Replace ];
|
||||
}
|
||||
|
||||
// Token overlap tests
|
||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthCommonToken)
|
||||
{
|
||||
// suggestion contains "colors", entireLine contains "colors"
|
||||
int result = LLMSuggestion::calculateReplaceLength("colors << \"red\"", "colors{};", "QStringList colors{};");
|
||||
EXPECT_EQ(result, 9); // Replace all rightText due to common token
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, testCalculateReplaceLengthMultipleCommonTokens)
|
||||
{
|
||||
// Multiple tokens in common
|
||||
int result = LLMSuggestion::calculateReplaceLength("engine.load()", "engine.rootContext()", "QmlEngine engine.rootContext()");
|
||||
EXPECT_EQ(result, 20); // Replace all rightText
|
||||
}
|
||||
|
||||
// Real-world scenarios
|
||||
TEST_F(LLMSuggestionTest, testCursorInBraces)
|
||||
{
|
||||
// Cursor in braces: QStringList colors{<cursor>};
|
||||
// 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)
|
||||
{
|
||||
// Cursor before braces: QStringList colors<cursor>{};
|
||||
// LLM sends: " = {\"red\"}", rightText: "{};"
|
||||
int result = LLMSuggestion::calculateReplaceLength(" = {\"red\"}", "{};", "QStringList colors{};");
|
||||
EXPECT_EQ(result, 3); // Structural overlap - replace all
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, testCursorAfterType)
|
||||
{
|
||||
// Cursor after type: QStringList <cursor>colors{};
|
||||
// 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)
|
||||
{
|
||||
// Cursor in middle: int <cursor>myVar = 5;
|
||||
// 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)
|
||||
{
|
||||
// LLM sends code with = and ;
|
||||
int result = LLMSuggestion::calculateReplaceLength("= 5;", ";", "int x;");
|
||||
EXPECT_EQ(result, 1); // Structural overlap on ;
|
||||
}
|
||||
|
||||
// Edge cases
|
||||
TEST_F(LLMSuggestionTest, testNoStructuralButHasToken)
|
||||
{
|
||||
// Token overlap but no structural
|
||||
int result = LLMSuggestion::calculateReplaceLength("myVar", "myVariable", "int myVariable");
|
||||
EXPECT_EQ(result, 0); // No structural overlap, tokens too different (length > 1 check)
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, testOnlyWhitespace)
|
||||
{
|
||||
// rightText is just whitespace, but "code" is common token
|
||||
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"
|
||||
Reference in New Issue
Block a user