diff --git a/CMakeLists.txt b/CMakeLists.txt index deb32f3..0b5d121 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,9 +45,9 @@ add_qtc_plugin(QodeAssist QodeAssist.qrc LSPCompletion.hpp LLMSuggestion.hpp LLMSuggestion.cpp - QodeAssistHoverHandler.hpp QodeAssistHoverHandler.cpp QodeAssistClient.hpp QodeAssistClient.cpp QodeAssistUtils.hpp DocumentContextReader.hpp DocumentContextReader.cpp QodeAssistData.hpp + utils/CounterTooltip.hpp utils/CounterTooltip.cpp ) diff --git a/LLMSuggestion.cpp b/LLMSuggestion.cpp index ab37c93..1fd50f5 100644 --- a/LLMSuggestion.cpp +++ b/LLMSuggestion.cpp @@ -19,10 +19,17 @@ #include "LLMSuggestion.hpp" +#include +#include +#include +#include +#include + namespace QodeAssist { LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin) : m_completion(completion) + , m_linesCount(0) { int startPos = completion.range().start().toPositionInDocument(origin); int endPos = completion.range().end().toPositionInDocument(origin); @@ -63,8 +70,35 @@ bool LLMSuggestion::apply() bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget) { - Q_UNUSED(widget) - return apply(); + return applyNextLine(widget); +} + +bool LLMSuggestion::applyNextLine(TextEditor::TextEditorWidget *widget) +{ + const QString text = m_completion.text(); + QStringList lines = text.split('\n'); + + if (m_linesCount < lines.size()) + m_linesCount++; + + showTooltip(widget, m_linesCount); + + return m_linesCount == lines.size() && !Utils::ToolTip::isVisible(); +} + +void LLMSuggestion::onCounterFinished(int count) +{ + Utils::ToolTip::hide(); + m_linesCount = 0; + QTextCursor cursor = m_completion.range().toSelection(m_start.document()); + cursor.beginEditBlock(); + cursor.removeSelectedText(); + + QStringList lines = m_completion.text().split('\n'); + QString textToInsert = lines.mid(0, count).join('\n'); + + cursor.insertText(textToInsert); + cursor.endEditBlock(); } void LLMSuggestion::reset() @@ -77,4 +111,14 @@ int LLMSuggestion::position() return m_start.position(); } +void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count) +{ + Utils::ToolTip::hide(); + QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight()); + pos += QPoint(-10, -50); + m_counterTooltip = new CounterTooltip(count); + Utils::ToolTip::show(pos, m_counterTooltip, widget); + connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished); +} + } // namespace QodeAssist diff --git a/LLMSuggestion.hpp b/LLMSuggestion.hpp index c5536f3..fcf716c 100644 --- a/LLMSuggestion.hpp +++ b/LLMSuggestion.hpp @@ -19,27 +19,37 @@ #pragma once +#include +#include "LSPCompletion.hpp" #include -#include "LSPCompletion.hpp" +#include "utils/CounterTooltip.hpp" namespace QodeAssist { -class LLMSuggestion final : public TextEditor::TextSuggestion +class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion { + Q_OBJECT public: LLMSuggestion(const Completion &completion, QTextDocument *origin); bool apply() final; bool applyWord(TextEditor::TextEditorWidget *widget) final; + bool applyNextLine(TextEditor::TextEditorWidget *widget); void reset() final; int position() final; const Completion &completion() const { return m_completion; } + void showTooltip(TextEditor::TextEditorWidget *widget, int count); + void onCounterFinished(int count); + private: Completion m_completion; QTextCursor m_start; + int m_linesCount; + + CounterTooltip *m_counterTooltip = nullptr; }; } // namespace QodeAssist diff --git a/LSPCompletion.hpp b/LSPCompletion.hpp index ea80fd7..7209999 100644 --- a/LSPCompletion.hpp +++ b/LSPCompletion.hpp @@ -43,6 +43,10 @@ public: { return typedValue(LanguageServerProtocol::positionKey); } + void setRange(const LanguageServerProtocol::Range &range) + { + insert(LanguageServerProtocol::rangeKey, range); + } LanguageServerProtocol::Range range() const { return typedValue(LanguageServerProtocol::rangeKey); diff --git a/QodeAssist.json.in b/QodeAssist.json.in index 264266e..9b060ef 100644 --- a/QodeAssist.json.in +++ b/QodeAssist.json.in @@ -1,6 +1,6 @@ { "Name" : "QodeAssist", - "Version" : "0.0.7", + "Version" : "0.0.8", "CompatVersion" : "${IDE_VERSION_COMPAT}", "Vendor" : "Petr Mironychev", "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd", diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index 260d122..296c519 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -190,7 +190,6 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r return; editor->insertSuggestion( std::make_unique(completions.first(), editor->document())); - editor->addHoverHandler(&m_hoverHandler); } } @@ -237,11 +236,6 @@ void QodeAssistClient::cleanupConnections() disconnect(m_documentOpenedConnection); disconnect(m_documentClosedConnection); - for (IEditor *editor : DocumentModel::editorsForOpenedDocuments()) { - if (auto textEditor = qobject_cast(editor)) - textEditor->editorWidget()->removeHoverHandler(&m_hoverHandler); - } - qDeleteAll(m_scheduledRequests); m_scheduledRequests.clear(); } diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index 73c1c7b..0271839 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -27,7 +27,6 @@ #include #include "LSPCompletion.hpp" -#include "QodeAssistHoverHandler.hpp" namespace QodeAssist { @@ -54,7 +53,6 @@ private: QHash m_runningRequests; QHash m_scheduledRequests; - QodeAssistHoverHandler m_hoverHandler; QMetaObject::Connection m_documentOpenedConnection; QMetaObject::Connection m_documentClosedConnection; }; diff --git a/QodeAssistHoverHandler.cpp b/QodeAssistHoverHandler.cpp deleted file mode 100644 index 8db717f..0000000 --- a/QodeAssistHoverHandler.cpp +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2023 The Qt Company Ltd. - * Copyright (C) 2024 Petr Mironychev - * - * This file is part of Qode Assist. - * - * 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 "QodeAssistHoverHandler.hpp" - -#include -#include -#include -#include - -#include -#include -#include - -#include -#include - -#include "LLMSuggestion.hpp" -#include "LSPCompletion.hpp" -#include "QodeAssisttr.h" - -using namespace TextEditor; -using namespace LanguageServerProtocol; -using namespace Utils; - -namespace QodeAssist { - -class QodeAssistCompletionToolTip : public QToolBar -{ -public: - QodeAssistCompletionToolTip(TextEditorWidget *editor) - : m_editor(editor) - { - auto apply = addAction(Tr::tr("Apply (%1)").arg(QKeySequence(Qt::Key_Tab).toString())); - connect(apply, &QAction::triggered, this, &QodeAssistCompletionToolTip::apply); - } - -private: - void apply() - { - if (TextSuggestion *suggestion = m_editor->currentSuggestion()) { - if (!suggestion->apply()) - return; - } - ToolTip::hide(); - } - - TextEditorWidget *m_editor; -}; - -void QodeAssistHoverHandler::identifyMatch(TextEditor::TextEditorWidget *editorWidget, - int pos, - ReportPriority report) -{ - QScopeGuard cleanup([&] { report(Priority_None); }); - if (!editorWidget->suggestionVisible()) - return; - - QTextCursor cursor(editorWidget->document()); - cursor.setPosition(pos); - m_block = cursor.block(); - auto *suggestion = dynamic_cast(TextDocumentLayout::suggestion(m_block)); - - if (!suggestion) - return; - - const Completion completion = suggestion->completion(); - if (completion.text().isEmpty()) - return; - - cleanup.dismiss(); - report(Priority_Suggestion); -} - -void QodeAssistHoverHandler::operateTooltip(TextEditor::TextEditorWidget *editorWidget, - const QPoint &point) -{ - Q_UNUSED(point) - auto *suggestion = dynamic_cast(TextDocumentLayout::suggestion(m_block)); - - if (!suggestion) - return; - - auto tooltipWidget = new QodeAssistCompletionToolTip(editorWidget); - - const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor()); - QPoint pos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft()) - - Utils::ToolTip::offsetFromPosition(); - pos.ry() -= tooltipWidget->sizeHint().height(); - ToolTip::show(pos, tooltipWidget, editorWidget); -} - -} // namespace QodeAssist diff --git a/QodeAssistSettings.cpp b/QodeAssistSettings.cpp index b280afe..4997af6 100644 --- a/QodeAssistSettings.cpp +++ b/QodeAssistSettings.cpp @@ -77,7 +77,7 @@ QodeAssistSettings::QodeAssistSettings() temperature.setDefaultValue(0.2); temperature.setRange(0.0, 10.0); - selectModels.m_buttonText = Tr::tr("Select Models"); + selectModels.m_buttonText = Tr::tr("Select Model"); ollamaLivetime.setSettingsKey(Constants::OLLAMA_LIVETIME); ollamaLivetime.setLabelText( @@ -145,9 +145,6 @@ QodeAssistSettings::QodeAssistSettings() frequencyPenalty.setDefaultValue(0.0); frequencyPenalty.setRange(-2.0, 2.0); - providerPaths.setSettingsKey(Constants::PROVIDER_PATHS); - providerPaths.setLabelText(Tr::tr("Provider Paths:")); - startSuggestionTimer.setSettingsKey(Constants::START_SUGGESTION_TIMER); startSuggestionTimer.setLabelText(Tr::tr("Start Suggestion Timer:")); startSuggestionTimer.setRange(10, 10000); @@ -219,7 +216,7 @@ QodeAssistSettings::QodeAssistSettings() enableLogging, Row{Stretch{1}, resetToDefaults}}}}, Group{title(Tr::tr("LLM Providers")), - Form{Column{llmProviders, Row{url, endPoint}, providerPaths}}}, + Form{Column{llmProviders, Row{url, endPoint}}}}, Group{title(Tr::tr("LLM Model Settings")), Form{Column{Row{selectModels, modelName}}}}, Group{title(Tr::tr("FIM Prompt Settings")), @@ -306,7 +303,7 @@ QStringList QodeAssistSettings::getInstalledModels() { auto *provider = LLMProvidersManager::instance().getCurrentProvider(); if (provider) { - auto env = getEnvironmentWithProviderPaths(); + Utils::Environment env = Utils::Environment::systemEnvironment(); return provider->getInstalledModels(env); } return {}; @@ -331,16 +328,6 @@ void QodeAssistSettings::showModelSelectionDialog() } } -Utils::Environment QodeAssistSettings::getEnvironmentWithProviderPaths() const -{ - Utils::Environment env = Utils::Environment::systemEnvironment(); - const QStringList additionalPaths = providerPaths.volatileValue(); - for (const QString &path : additionalPaths) { - env.prependOrSetPath(path); - } - return env; -} - void QodeAssistSettings::resetSettingsToDefaults() { QMessageBox::StandardButton reply; diff --git a/QodeAssistSettings.hpp b/QodeAssistSettings.hpp index f974874..641f9d2 100644 --- a/QodeAssistSettings.hpp +++ b/QodeAssistSettings.hpp @@ -88,8 +88,6 @@ public: Utils::BoolAspect useFrequencyPenalty{this}; Utils::DoubleAspect frequencyPenalty{this}; - Utils::StringListAspect providerPaths{this}; - Utils::IntegerAspect startSuggestionTimer{this}; Utils::IntegerAspect maxFileThreshold{this}; diff --git a/README.md b/README.md index 32f1559..5c80c62 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ QodeAssist has been tested with the following language models, all trained for F Ollama: - [starcoder2](https://ollama.com/library/starcoder2) - [codellama](https://ollama.com/library/codellama) +- DeepSeek-Coder-V2-Lite-Base LM studio: - [second-state/StarCoder2-7B-GGUF](https://huggingface.co/second-state/StarCoder2-7B-GGUF) @@ -30,7 +31,7 @@ If you've successfully used a model that's not listed here, please let us know b - [ ] Add chat functionality - [ ] Support for more providers and models -## Installation Plugin +## Plugin installation using Ollama as an example 1. Install QtCreator 14.0 2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation. @@ -50,14 +51,19 @@ ollama run starcoder2:7b 1. Open Qt Creator settings 2. Navigate to the "Qode Assist" tab 3. Choose your LLM provider (e.g., Ollama) - - If you haven't added the provider to your system PATH, specify the path to the provider executable in the "Provider Paths" field -4. Select the installed model - - If you need to enter the model name manually, it indicates that the plugin cannot locate the provider's executable file. However, this doesn't affect the plugin's functionality – it will still work correctly. This autoselector input option is provided for your convenience, allowing you to easily select and use different models +4. Select the installed model by the "Select Model" button + - For LM Studio you will see current load model 5. Choose the prompt template that corresponds to your model 6. Apply the settings You're all set! QodeAssist is now ready to use in Qt Creator. +## Hotkeys +- To insert the full suggestion, you can use the TAB key +- To insert line by line, you can use the "Move cursor word right" shortcut: + - On Mac: Option + Right Arrow + - On Windows: Alt + Right Arrow + ## Support the development of QodeAssist If you find QodeAssist helpful, there are several ways you can support the project: diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp index 946787d..4aa5df3 100644 --- a/providers/LMStudioProvider.cpp +++ b/providers/LMStudioProvider.cpp @@ -19,14 +19,15 @@ #include "LMStudioProvider.hpp" +#include #include #include #include #include -#include #include "PromptTemplateManager.hpp" #include "QodeAssistSettings.hpp" +#include "QodeAssistUtils.hpp" namespace QodeAssist::Providers { @@ -113,53 +114,32 @@ bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulated QList LMStudioProvider::getInstalledModels(const Utils::Environment &env) { - QProcess process; - process.setEnvironment(env.toStringList()); - QString lmsConsoleName; -#ifdef Q_OS_WIN - lmsConsoleName = "lms.exe"; -#else - lmsConsoleName = "lms"; -#endif - auto lmsPath = env.searchInPath(lmsConsoleName).toString(); + QList models; + QNetworkAccessManager manager; + QNetworkRequest request(QUrl(url() + "/v1/models")); - if (!QFileInfo::exists(lmsPath)) { - qWarning() << "LMS executable not found at" << lmsPath; - return {}; - } + QNetworkReply *reply = manager.get(request); - process.start(lmsPath, QStringList() << "ls"); - if (!process.waitForStarted()) { - qWarning() << "Failed to start LMS process:" << process.errorString(); - return {}; - } + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); - if (!process.waitForFinished()) { - qWarning() << "LMS process did not finish:" << process.errorString(); - return {}; - } + if (reply->error() == QNetworkReply::NoError) { + QByteArray responseData = reply->readAll(); + QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData); + QJsonObject jsonObject = jsonResponse.object(); + QJsonArray modelArray = jsonObject["data"].toArray(); - QStringList models; - if (process.exitCode() == 0) { - QString output = QString::fromUtf8(process.readAllStandardOutput()); - QStringList lines = output.split('\n', Qt::SkipEmptyParts); - - // Skip the header lines - for (int i = 2; i < lines.size(); ++i) { - QString line = lines[i].trimmed(); - if (!line.isEmpty()) { - // The model name is the first column - QString modelName = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts) - .first(); - models.append(modelName); - } + for (const QJsonValue &value : modelArray) { + QJsonObject modelObject = value.toObject(); + QString modelId = modelObject["id"].toString(); + models.append(modelId); } - qDebug() << "Models:" << models; } else { - // Handle error - qWarning() << "Error running 'lms list':" << process.errorString(); + logMessage(QString("Error fetching models: %1").arg(reply->errorString())); } + reply->deleteLater(); return models; } diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index 4da417e..41aaaa7 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -23,10 +23,11 @@ #include #include #include -#include +#include #include "PromptTemplateManager.hpp" #include "QodeAssistSettings.hpp" +#include "QodeAssistUtils.hpp" namespace QodeAssist::Providers { @@ -96,50 +97,31 @@ bool OllamaProvider::handleResponse(QNetworkReply *reply, QString &accumulatedRe QList OllamaProvider::getInstalledModels(const Utils::Environment &env) { - QProcess process; - process.setEnvironment(env.toStringList()); - QString ollamaConsoleName; -#ifdef Q_OS_WIN - ollamaConsoleName = "ollama.exe"; -#else - ollamaConsoleName = "ollama"; -#endif + QList models; + QNetworkAccessManager manager; + QNetworkRequest request(QUrl(url() + "/api/tags")); + QNetworkReply *reply = manager.get(request); - auto ollamaPath = env.searchInPath(ollamaConsoleName).toString(); + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); - if (!QFileInfo::exists(ollamaPath)) { - qWarning() << "Ollama executable not found at" << ollamaPath; - return {}; - } + if (reply->error() == QNetworkReply::NoError) { + QByteArray responseData = reply->readAll(); + QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData); + QJsonObject jsonObject = jsonResponse.object(); + QJsonArray modelArray = jsonObject["models"].toArray(); - process.start(ollamaPath, QStringList() << "list"); - if (!process.waitForStarted()) { - qWarning() << "Failed to start Ollama process:" << process.errorString(); - return {}; - } - - if (!process.waitForFinished()) { - qWarning() << "Ollama process did not finish:" << process.errorString(); - return {}; - } - - QStringList models; - if (process.exitCode() == 0) { - QString output = QString::fromUtf8(process.readAllStandardOutput()); - QStringList lines = output.split('\n', Qt::SkipEmptyParts); - - for (int i = 1; i < lines.size(); ++i) { - QString line = lines[i].trimmed(); - if (!line.isEmpty()) { - QString modelName = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts) - .first(); - models.append(modelName); - } + for (const QJsonValue &value : modelArray) { + QJsonObject modelObject = value.toObject(); + QString modelName = modelObject["name"].toString(); + models.append(modelName); } } else { - qWarning() << "Error running 'ollama list':" << process.errorString(); + logMessage(QString("Error fetching models: %1").arg(reply->errorString())); } + reply->deleteLater(); return models; } diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp index 51fdc54..64d0b3a 100644 --- a/providers/OpenAICompatProvider.cpp +++ b/providers/OpenAICompatProvider.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include "PromptTemplateManager.hpp" #include "QodeAssistSettings.hpp" diff --git a/utils/CounterTooltip.cpp b/utils/CounterTooltip.cpp new file mode 100644 index 0000000..ec5a699 --- /dev/null +++ b/utils/CounterTooltip.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 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 . + */ + +#include "CounterTooltip.hpp" + +namespace QodeAssist { + +CounterTooltip::CounterTooltip(int count) + : m_count(count) +{ + m_label = new QLabel(this); + addWidget(m_label); + updateLabel(); + + m_timer = new QTimer(this); + m_timer->setSingleShot(true); + m_timer->setInterval(2000); + + connect(m_timer, &QTimer::timeout, this, [this] { emit finished(m_count); }); + + m_timer->start(); +} + +CounterTooltip::~CounterTooltip() {} + +void CounterTooltip::updateLabel() +{ + const auto hotkey = QKeySequence(QKeySequence::MoveToNextWord).toString(); + m_label->setText(QString("Insert Next %1 line(s) (%2)").arg(m_count).arg(hotkey)); +} + +} // namespace QodeAssist diff --git a/QodeAssistHoverHandler.hpp b/utils/CounterTooltip.hpp similarity index 52% rename from QodeAssistHoverHandler.hpp rename to utils/CounterTooltip.hpp index 30733a2..5e53e00 100644 --- a/QodeAssistHoverHandler.hpp +++ b/utils/CounterTooltip.hpp @@ -1,13 +1,8 @@ -/* - * Copyright (C) 2023 The Qt Company Ltd. +/* * Copyright (C) 2024 Petr Mironychev * - * This file is part of Qode Assist. + * 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 @@ -24,24 +19,30 @@ #pragma once -#include -#include +#include +#include +#include +#include namespace QodeAssist { -class QodeAssistHoverHandler : public TextEditor::BaseHoverHandler +class CounterTooltip : public QToolBar { -public: - QodeAssistHoverHandler() = default; + Q_OBJECT -protected: - void identifyMatch(TextEditor::TextEditorWidget *editorWidget, - int pos, - ReportPriority report) final; - void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) final; +public: + CounterTooltip(int count); + ~CounterTooltip(); + +signals: + void finished(int count); private: - QTextBlock m_block; + void updateLabel(); + + QLabel *m_label; + QTimer *m_timer; + int m_count; }; } // namespace QodeAssist