Skip to content

Commit b0777a5

Browse files
committed
#3407 ai-autocomplete: fix autocompletion
Signed-off-by: Patrizio Bekerle <[email protected]>
1 parent e55aded commit b0777a5

File tree

4 files changed

+232
-35
lines changed

4 files changed

+232
-35
lines changed

src/services/openaiservice.cpp

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
#include "scriptingservice.h"
2727
#include "services/settingsservice.h"
2828

29+
// Forward declaration to avoid circular dependency
30+
class QOwnNotesMarkdownTextEdit;
31+
2932
using namespace std;
3033

3134
QT_USE_NAMESPACE
@@ -34,11 +37,59 @@ OpenAiService::OpenAiService(QObject* parent) : QObject(parent) {
3437
initializeBackends();
3538
initializeCompleter(parent);
3639

40+
// Set up the global autocomplete callback that routes to the active editor
41+
// This is done once here instead of in each widget to avoid widgets overwriting each other
42+
setAutocompleteCallback([](const QString& result) {
43+
qDebug() << "*** GLOBAL AUTOCOMPLETE CALLBACK RECEIVED, result length:" << result.length();
44+
// Forward declaration to avoid circular dependency
45+
class QOwnNotesMarkdownTextEdit;
46+
47+
// Get the active editor using QMetaObject since we can't include the header
48+
QObject* activeEditorObj = nullptr;
49+
QMetaObject::invokeMethod(
50+
nullptr,
51+
[]() -> QObject* {
52+
// Access the static method through the class name
53+
// We need to get QOwnNotesMarkdownTextEdit::getActiveEditorForAutocomplete()
54+
// but we can't call it directly due to circular includes
55+
// So we'll use a different approach - store it in QApplication
56+
return qApp->property("activeAutocompleteEditor").value<QObject*>();
57+
},
58+
Qt::DirectConnection, Q_RETURN_ARG(QObject*, activeEditorObj));
59+
60+
if (!activeEditorObj) {
61+
// Try to get from application property
62+
activeEditorObj = qApp->property("activeAutocompleteEditor").value<QObject*>();
63+
}
64+
65+
if (activeEditorObj) {
66+
qDebug() << "*** Calling onAiAutocompleteCompleted on active editor:"
67+
<< activeEditorObj;
68+
QMetaObject::invokeMethod(activeEditorObj, "onAiAutocompleteCompleted",
69+
Qt::DirectConnection, Q_ARG(QString, result));
70+
} else {
71+
qDebug() << "*** No active editor found in callback!";
72+
}
73+
});
74+
3775
QObject::connect(_completer, &OpenAiCompleter::completed, this, [this](const QString& result) {
3876
qDebug() << "'result': " << result;
39-
qDebug() << "OpenAiService - emitting autocompleteCompleted signal";
40-
emit autocompleteCompleted(result);
41-
qDebug() << "OpenAiService - autocompleteCompleted signal emitted";
77+
qDebug() << "OpenAiService - processing autocomplete result";
78+
qDebug() << "OpenAiService - this pointer:" << this;
79+
qDebug() << "OpenAiService - Number of signal receivers:"
80+
<< receivers(SIGNAL(autocompleteCompleted(QString)));
81+
qDebug() << "OpenAiService - Has callback:" << (_autocompleteCallback != nullptr);
82+
83+
// Try callback first (direct call to active editor)
84+
if (_autocompleteCallback) {
85+
qDebug() << "OpenAiService - Calling autocomplete callback directly";
86+
_autocompleteCallback(result);
87+
} else {
88+
qDebug() << "OpenAiService - No callback, emitting signal";
89+
emit autocompleteCompleted(result);
90+
}
91+
92+
qDebug() << "OpenAiService - autocomplete processing completed";
4293
});
4394
QObject::connect(_completer, &OpenAiCompleter::errorOccurred, this,
4495
[this](const QString& errorString) {
@@ -86,9 +137,36 @@ void OpenAiService::deleteInstance() {
86137

87138
void OpenAiService::initializeBackends() {
88139
_backendModels.clear();
89-
_backendModels[QStringLiteral("openai")] =
90-
QStringList{"gpt-4o", "chatgpt-4o-latest", "gpt-4o-mini", "o1-mini",
91-
"o1-preview", "gpt-4-turbo", "gpt-3.5-turbo", "gpt-4"};
140+
_backendModels[QStringLiteral("openai")] = QStringList{"gpt-5.1",
141+
"gpt-5",
142+
"gpt-5-mini",
143+
"gpt-5-nano",
144+
"gpt-5.1-chat-latest",
145+
"gpt-5-chat-latest",
146+
"gpt-5.1-codex-max",
147+
"gpt-5.1-codex",
148+
"gpt-5-codex",
149+
"gpt-5-pro",
150+
"gpt-4.1",
151+
"gpt-4.1-mini",
152+
"gpt-4.1-nano",
153+
"gpt-4o",
154+
"gpt-4o-2024-05-13",
155+
"gpt-4o-mini",
156+
"o1",
157+
"o1-pro",
158+
"o3-pro",
159+
"o3",
160+
"o3-deep-research",
161+
"o4-mini",
162+
"o4-mini-deep-research",
163+
"o3-mini",
164+
"o1-mini",
165+
"gpt-5.1-codex-mini",
166+
"codex-mini-latest",
167+
"gpt-5-search-api",
168+
"gpt-4o-mini-search-preview",
169+
"gpt-4o-search-preview"};
92170
_backendModels[QStringLiteral("groq")] =
93171
QStringList{"llama-3.1-8b-instant",
94172
"deepseek-r1-distill-llama-70b",
@@ -99,9 +177,6 @@ void OpenAiService::initializeBackends() {
99177
"groq/compound-mini",
100178
"meta-llama/llama-4-maverick-17b-128e-instruct",
101179
"meta-llama/llama-4-scout-17b-16e-instruct",
102-
"meta-llama/llama-guard-4-12b",
103-
"meta-llama/llama-prompt-guard-2-86m",
104-
"meta-llama/llama-prompt-guard-2-22m",
105180
"moonshotai/kimi-k2-instruct",
106181
"moonshotai/kimi-k2-instruct-0905",
107182
"openai/gpt-oss-120b",
@@ -243,8 +318,10 @@ QString OpenAiService::getModelId() {
243318
// If not set yet try to read the settings
244319
if (this->_modelId.isEmpty()) {
245320
SettingsService settings;
246-
this->_modelId =
247-
settings.value(getCurrentModelSettingsKey(), _backendModels[getBackendId()]).toString();
321+
// Get the first model as default if no model is set
322+
const QStringList& models = getModelsForCurrentBackend();
323+
QString defaultModel = models.isEmpty() ? QLatin1String("") : models.first();
324+
this->_modelId = settings.value(getCurrentModelSettingsKey(), defaultModel).toString();
248325
}
249326

250327
// If still not set get the first of the models
@@ -310,6 +387,13 @@ QString OpenAiService::complete(const QString& prompt) {
310387
return _completer->completeSync(prompt);
311388
}
312389

390+
void OpenAiService::setAutocompleteCallback(AutocompleteCallback callback) {
391+
qDebug() << __func__
392+
<< " - Setting autocomplete callback, was:" << (_autocompleteCallback != nullptr)
393+
<< "now:" << (callback != nullptr);
394+
_autocompleteCallback = callback;
395+
}
396+
313397
OpenAiCompleter::OpenAiCompleter(QString apiKey, QString modelId, QString apiBaseUrl,
314398
QObject* parent)
315399
: QObject(parent),

src/services/openaiservice.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <QNetworkAccessManager>
1919
#include <QNetworkReply>
2020
#include <QObject>
21+
#include <functional>
2122

2223
#include "services/settingsservice.h"
2324

@@ -71,9 +72,13 @@ class OpenAiService : public QObject {
7172
QMap<QString, QString> getBackendNames();
7273
QString getApiBaseUrlForBackend(const QString& backendId);
7374

75+
// Callback for autocomplete results (avoids circular dependency)
76+
using AutocompleteCallback = std::function<void(const QString&)>;
77+
void setAutocompleteCallback(AutocompleteCallback callback);
78+
7479
signals:
75-
void autocompleteCompleted(QString result);
76-
void autocompleteErrorOccurred(QString errorString);
80+
void autocompleteCompleted(const QString& result);
81+
void autocompleteErrorOccurred(const QString& errorString);
7782

7883
private:
7984
QMap<QString, QStringList> _backendModels;
@@ -89,5 +94,6 @@ class OpenAiService : public QObject {
8994
static QString getApiKeySettingsKeyForBackend(const QString& backendId);
9095
void initializeCompleter(QObject* parent);
9196
QString getApiBaseUrlForCurrentBackend();
97+
AutocompleteCallback _autocompleteCallback;
9298
QString getApiKeyForCurrentBackend();
9399
};

src/widgets/qownnotesmarkdowntextedit.cpp

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
#include "services/settingsservice.h"
2929
#include "utils/urlhandler.h"
3030

31+
// Initialize static member
32+
QOwnNotesMarkdownTextEdit *QOwnNotesMarkdownTextEdit::_activeAutocompleteEditor = nullptr;
33+
3134
QOwnNotesMarkdownTextEdit::QOwnNotesMarkdownTextEdit(QWidget *parent)
3235
: QMarkdownTextEdit(parent, false) {
3336
// We need to set the internal variable to true, because we start with a highlighter
@@ -47,20 +50,32 @@ QOwnNotesMarkdownTextEdit::QOwnNotesMarkdownTextEdit(QWidget *parent)
4750
connect(_aiAutocompleteTimer, &QTimer::timeout, this,
4851
&QOwnNotesMarkdownTextEdit::requestAiAutocomplete);
4952

50-
// Connect to OpenAI service signals for all widgets
51-
qDebug() << __func__ << " - Connecting AI autocomplete signals for widget:" << objectName()
52-
<< "parent:" << (parent ? parent->objectName() : "null");
53-
qDebug() << __func__ << " - OpenAiService instance:" << OpenAiService::instance();
54-
qDebug() << __func__ << " - this instance:" << this;
55-
56-
// Use old-style SIGNAL/SLOT macros for better compatibility
57-
bool conn1 = connect(OpenAiService::instance(), SIGNAL(autocompleteCompleted(QString)), this,
58-
SLOT(onAiAutocompleteCompleted(QString)), Qt::QueuedConnection);
59-
bool conn2 = connect(OpenAiService::instance(), SIGNAL(autocompleteErrorOccurred(QString)),
60-
this, SLOT(onAiAutocompleteTimeout(QString)), Qt::QueuedConnection);
53+
// NOTE: Callback registration is now done globally in OpenAiService constructor
54+
// We just need to register this editor as active if it's a note editor
55+
if (!parent || parent->objectName() != QStringLiteral("LogWidget")) {
56+
qDebug() << __func__ << " - Registering as active editor";
57+
registerAsActiveEditor();
58+
} else {
59+
qDebug() << __func__ << " - Skipping registration for non-editor widget:" << objectName();
60+
}
6161

62-
qDebug() << __func__ << " - AI autocomplete signals connected, conn1:" << conn1
62+
// Use DirectConnection to ensure signal is delivered immediately before widget can be destroyed
63+
// This prevents the "0 receivers" problem when widgets are recreated
64+
bool conn1 = connect(
65+
OpenAiService::instance(), &OpenAiService::autocompleteCompleted, this,
66+
[this](const QString &result) {
67+
qDebug() << "*** LAMBDA RECEIVED autocompleteCompleted signal for widget:" << this
68+
<< objectName();
69+
this->onAiAutocompleteCompleted(result);
70+
},
71+
Qt::DirectConnection);
72+
bool conn2 = connect(OpenAiService::instance(), &OpenAiService::autocompleteErrorOccurred, this,
73+
&QOwnNotesMarkdownTextEdit::onAiAutocompleteTimeout, Qt::DirectConnection);
74+
75+
qDebug() << __func__
76+
<< " - AI autocomplete signals connected (DirectConnection), conn1:" << conn1
6377
<< "conn2:" << conn2;
78+
qDebug() << __func__ << " - Connected to OpenAiService instance:" << OpenAiService::instance();
6479

6580
// Test: Call the slot directly to verify it exists
6681
qDebug() << __func__ << " - Testing slot by calling it directly...";
@@ -1313,8 +1328,15 @@ void QOwnNotesMarkdownTextEdit::requestAiAutocomplete() {
13131328
* Shows the AI autocomplete suggestion
13141329
*/
13151330
void QOwnNotesMarkdownTextEdit::showAiAutocompleteSuggestion(const QString &suggestion) {
1331+
qDebug() << "=== showAiAutocompleteSuggestion CALLED ===" << this;
1332+
qDebug() << __func__ << " - Widget:" << objectName();
1333+
13161334
SettingsService settings;
1317-
if (!settings.value(QStringLiteral("ai/autocompleteEnabled")).toBool()) {
1335+
bool enabled = settings.value(QStringLiteral("ai/autocompleteEnabled")).toBool();
1336+
qDebug() << __func__ << " - ai/autocompleteEnabled setting:" << enabled;
1337+
1338+
if (!enabled) {
1339+
qDebug() << __func__ << " - autocomplete not enabled in settings, returning";
13181340
return;
13191341
}
13201342

@@ -1339,13 +1361,16 @@ void QOwnNotesMarkdownTextEdit::showAiAutocompleteSuggestion(const QString &sugg
13391361
_aiAutocompleteSuggestion = suggestion;
13401362
_isInsertingAiSuggestion = true;
13411363

1342-
qDebug() << __func__ << " - inserting suggestion";
1364+
qDebug() << __func__ << " - inserting suggestion text...";
13431365

13441366
// Insert the suggestion with a gray color format
13451367
QTextCursor cursor = textCursor();
13461368
cursor.beginEditBlock();
13471369

1348-
// Store the format
1370+
// Store the original format to restore it later
1371+
QTextCharFormat originalFormat = cursor.charFormat();
1372+
1373+
// Create the suggestion format
13491374
QTextCharFormat format;
13501375
format.setForeground(QColor(128, 128, 128)); // Gray color
13511376
format.setFontItalic(true);
@@ -1357,10 +1382,14 @@ void QOwnNotesMarkdownTextEdit::showAiAutocompleteSuggestion(const QString &sugg
13571382
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor,
13581383
_aiAutocompleteSuggestion.length());
13591384

1385+
// Reset the character format to the original to prevent the italic format
1386+
// from affecting subsequent text
1387+
cursor.setCharFormat(originalFormat);
1388+
13601389
cursor.endEditBlock();
13611390
setTextCursor(cursor);
13621391

1363-
qDebug() << __func__ << " - suggestion inserted and selected";
1392+
qDebug() << __func__ << " - suggestion inserted and selected successfully!";
13641393

13651394
_isInsertingAiSuggestion = false;
13661395
}
@@ -1424,12 +1453,18 @@ void QOwnNotesMarkdownTextEdit::acceptAiAutocompleteSuggestion() {
14241453
* Called when AI autocomplete is completed
14251454
*/
14261455
void QOwnNotesMarkdownTextEdit::onAiAutocompleteCompleted(const QString &result) {
1456+
qDebug() << "=== onAiAutocompleteCompleted CALLED ===" << this;
1457+
qDebug() << __func__ << " - Widget:" << objectName()
1458+
<< "Parent:" << (parent() ? parent()->objectName() : "null");
14271459
qDebug() << __func__ << " - 'result': " << result;
14281460

14291461
if (result.isEmpty()) {
1462+
qDebug() << __func__ << " - result is empty, returning";
14301463
return;
14311464
}
14321465

1466+
qDebug() << __func__ << " - Processing result, length:" << result.length();
1467+
14331468
// Extract the first line or first sentence as suggestion
14341469
QString suggestion = result.trimmed();
14351470

@@ -1450,7 +1485,9 @@ void QOwnNotesMarkdownTextEdit::onAiAutocompleteCompleted(const QString &result)
14501485
qDebug() << __func__ << " - 'cursor position': " << textCursor().position();
14511486
qDebug() << __func__ << " - '_aiAutocompletePosition': " << _aiAutocompletePosition;
14521487

1488+
qDebug() << __func__ << " - Calling showAiAutocompleteSuggestion...";
14531489
showAiAutocompleteSuggestion(suggestion);
1490+
qDebug() << __func__ << " - showAiAutocompleteSuggestion returned";
14541491
}
14551492

14561493
/**
@@ -1489,3 +1526,53 @@ void QOwnNotesMarkdownTextEdit::keyPressEvent(QKeyEvent *e) {
14891526
// Call parent implementation
14901527
QMarkdownTextEdit::keyPressEvent(e);
14911528
}
1529+
1530+
/**
1531+
* Override focusInEvent to register this editor as active when it receives focus
1532+
*/
1533+
void QOwnNotesMarkdownTextEdit::focusInEvent(QFocusEvent *e) {
1534+
// Register as the active editor for autocomplete when receiving focus
1535+
// Skip for log widgets
1536+
if (objectName() != QStringLiteral("logTextEdit")) {
1537+
qDebug() << __func__ << " - Registering as active editor:" << this << objectName();
1538+
registerAsActiveEditor();
1539+
}
1540+
1541+
// Call parent implementation
1542+
QMarkdownTextEdit::focusInEvent(e);
1543+
}
1544+
1545+
/**
1546+
* Register this editor as the active one for AI autocomplete
1547+
*/
1548+
void QOwnNotesMarkdownTextEdit::registerAsActiveEditor() {
1549+
qDebug() << __func__ << " - Registering editor:" << this << objectName();
1550+
_activeAutocompleteEditor = this;
1551+
// Also store in QApplication property for access from OpenAiService callback
1552+
qApp->setProperty("activeAutocompleteEditor", QVariant::fromValue<QObject *>(this));
1553+
}
1554+
1555+
/**
1556+
* Unregister this editor from receiving AI autocomplete
1557+
*/
1558+
void QOwnNotesMarkdownTextEdit::unregisterAsActiveEditor() {
1559+
qDebug() << __func__ << " - Unregistering editor:" << this << objectName();
1560+
if (_activeAutocompleteEditor == this) {
1561+
_activeAutocompleteEditor = nullptr;
1562+
// Also clear QApplication property
1563+
qApp->setProperty("activeAutocompleteEditor", QVariant::fromValue<QObject *>(nullptr));
1564+
}
1565+
}
1566+
1567+
/**
1568+
* Get the currently active editor for AI autocomplete
1569+
*/
1570+
QOwnNotesMarkdownTextEdit *QOwnNotesMarkdownTextEdit::getActiveEditorForAutocomplete() {
1571+
return _activeAutocompleteEditor;
1572+
}
1573+
1574+
QOwnNotesMarkdownTextEdit::~QOwnNotesMarkdownTextEdit() {
1575+
qDebug() << "*** QOwnNotesMarkdownTextEdit DESTROYED ***" << this << objectName();
1576+
// Unregister if this was the active editor
1577+
unregisterAsActiveEditor();
1578+
}

0 commit comments

Comments
 (0)