From 204cffd7d03e085ce67b5a6798f7bc429cb08bcd Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:53:46 +0100 Subject: [PATCH] feat: Add custom instructions for quick refactor (#258) * feat: Add custom commands to quick refactor * doc: Update for quick refactor feature --- CMakeLists.txt | 2 + README.md | 13 +- docs/quick-refactoring.md | 276 ++++++++++++++++++++++++- qodeassist.cpp | 3 + widgets/AddCustomInstructionDialog.cpp | 102 +++++++++ widgets/AddCustomInstructionDialog.hpp | 52 +++++ widgets/CustomInstructionsManager.cpp | 225 ++++++++++++++++++++ widgets/CustomInstructionsManager.hpp | 63 ++++++ widgets/QuickRefactorDialog.cpp | 252 +++++++++++++++++++++- widgets/QuickRefactorDialog.hpp | 14 ++ 10 files changed, 985 insertions(+), 17 deletions(-) create mode 100644 widgets/AddCustomInstructionDialog.cpp create mode 100644 widgets/AddCustomInstructionDialog.hpp create mode 100644 widgets/CustomInstructionsManager.cpp create mode 100644 widgets/CustomInstructionsManager.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4537044..ce8200c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,6 +117,8 @@ add_qtc_plugin(QodeAssist widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp + widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp + widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp QuickRefactorHandler.hpp QuickRefactorHandler.cpp tools/ToolsFactory.hpp tools/ToolsFactory.cpp diff --git a/README.md b/README.md index 6088bcb..fa680c0 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,11 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance: - **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML - **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel) -- **Quick Refactoring**: AI-assisted code improvements and alternative suggestions +- **Quick Refactoring**: Inline AI-assisted code improvements directly in editor with custom instructions library - **File Context**: Attach or link files for better AI understanding - **Tool Calling**: AI can read project files, search code, and access diagnostics - **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more -- **Customizable**: Project-specific rules and extensive model templates +- **Customizable**: Project-specific rules, custom instructions, and extensive model templates **Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users! @@ -159,9 +159,12 @@ QodeAssist supports multiple LLM providers. Choose your preferred provider and f - Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks ### Quick Refactoring -- Fast code refactoring with AI assistance -- Selection-based improvements -- Alternative code suggestions +- Inline code refactoring directly in the editor with AI assistance +- Selection-based improvements with instant code replacement +- Built-in quick actions (repeat, improve, alternative) +- **Custom instructions library** with search and autocomplete +- Create, edit, and manage reusable refactoring templates +- Combine base instructions with specific details - **[Learn more](docs/quick-refactoring.md)** ### Tools & Function Calling diff --git a/docs/quick-refactoring.md b/docs/quick-refactoring.md index 8574f69..023f6cd 100644 --- a/docs/quick-refactoring.md +++ b/docs/quick-refactoring.md @@ -1,18 +1,276 @@ # Quick Refactoring Feature +Quick Refactoring provides AI-assisted code improvements directly in your editor. Select code, press the hotkey, and get instant refactoring suggestions. + +## Overview + +Quick Refactoring provides AI-assisted code refactoring with its own dedicated provider and model configuration, allowing you to use different settings than your Chat Assistant. You can use built-in quick actions or create your own custom instructions library. + ## Setup -Since this is actually a small chat with redirected output, the main settings of the provider, model and template are taken from the chat settings. +Quick Refactoring has independent configuration separate from Chat Assistant: -## Using +### Provider & Model Configuration -The request to model consist of instructions to model, selection code and cursor position. +Configure provider and model in: `Qt Creator → Preferences → QodeAssist → General Settings` -The default instruction is: "Refactor the code to improve its quality and maintainability." and sending if text field is empty. +Under the **Quick Refactor** section, you can set: +- **Provider**: Choose from Ollama, Claude, OpenAI, Google AI, etc. +- **Model**: Select the specific model for refactoring tasks +- **Template**: Choose the chat template for this provider +- **URL**: Set the API endpoint +- **API Key**: Configure authentication (for cloud providers) -Also there buttons to quick call instractions: -- Repeat latest instruction, will activate after sending first request in QtCreator session -- Improve current selection code -- Suggestion alternative variant of selection code -- Other instructions[TBD] +This allows you to: +- Use a faster/cheaper model for refactoring than for chat +- Use a local model for refactoring and cloud model for chat +- Optimize costs by using different providers for different tasks + +### Quick Refactor Settings + +Additional refactoring-specific options in: `Qt Creator → Preferences → QodeAssist → Quick Refactor` + +Configure: +- **Context Settings**: How much code context to send + - Read full file or N lines before/after selection +- **LLM Parameters**: Temperature, max tokens, top_p, top_k +- **Advanced Options**: Penalties, context window size +- **Features**: Tool calling, extended thinking mode +- **System Prompt**: Customize the base prompt for refactoring + +## Using Quick Refactoring + +### Basic Usage + +1. **Select Code** (or place cursor for line-level refactoring) +2. **Trigger Quick Refactor**: Press `Ctrl+Alt+R` (Windows/Linux) or `⌥⌘R` (macOS) +3. **Choose Action**: + - Use a built-in quick action button + - Select a custom instruction from the dropdown + - Type your own instruction +4. **Get Results**: AI generates refactored code directly replacing your selection + +### Quick Action Buttons + +The dialog provides three built-in quick actions: + +| Button | Description | +|--------|-------------| +| **Repeat Last** | Reuses the last instruction from your session (enabled after first use) | +| **Improve Code** | Enhances readability, efficiency, and maintainability with best practices | +| **Alternative Solution** | Suggests different implementation approaches and patterns | + +## Custom Instructions + +### Overview + +Custom Instructions allow you to create a reusable library of refactoring templates. Instead of typing the same instructions repeatedly, save them once and access them instantly through the searchable dropdown. + +**Key Features:** +- **Quick Access**: Search and select instructions by typing +- **Flexible**: Use as-is or add extra details for each use +- **Manageable**: Easy create, edit, and delete interface +- **Persistent**: Instructions saved locally and loaded on startup +- **Accessible**: Direct access to instruction files folder + +### Creating Custom Instructions + +1. Click the **`+`** button in the Quick Refactor dialog +2. Fill in the form: + - **Name**: Short descriptive title (e.g., "Add Documentation") + - **Instruction Body**: Detailed prompt for the LLM + +**Example instruction:** + +``` +Name: Add Documentation +Body: Add comprehensive documentation to the selected code or code afer cursor following: + Doxygen style. Include parameter descriptions, return value + documentation, and usage examples where applicable. +``` + + +### Using Custom Instructions + +#### Method 1: Select and Use +1. Open Quick Refactor dialog (`Ctrl+Alt+R` / `⌥⌘R`) +2. Click the dropdown or start typing instruction name +3. Select instruction (autocomplete will help) +4. Optionally add extra details in the text field below +5. Press OK + + +#### Method 2: Search by Typing +1. Open Quick Refactor dialog +2. Start typing in the instruction dropdown (e.g., "doc...") +3. Autocomplete shows matching instructions +4. Select with arrow keys or click +5. Add optional details and execute + +**Search Features:** +- Case-insensitive search +- Match anywhere in instruction name +- Keyboard navigation (arrow keys, Enter) +- Instant filtering as you type + +### Combining Instructions with Additional Details + +Custom instructions serve as **base templates** that you can augment with specific requirements: + +**Example 1 - Use instruction as-is:** +``` +Selected: "Add Documentation" +Additional text: [empty] +→ Sends: "Add comprehensive documentation..." +``` + +**Example 2 - Add specific requirements:** +``` +Selected: "Optimize Performance" +Additional text: "Focus on reducing memory allocations" +→ Sends: "Optimize Performance instructions... + +Focus on reducing memory allocations" +``` + +This approach allows maximum flexibility while maintaining a clean instruction library. + +### Managing Custom Instructions + +The Quick Refactor dialog provides full CRUD operations: + +| Button | Action | Description | +|--------|--------|-------------| +| **+** | Add | Create new custom instruction | +| **✎** | Edit | Modify selected instruction | +| **−** | Delete | Remove selected instruction (with confirmation) | +| **📁** | Open Folder | Open instructions directory in file manager | + +**Edit/Delete:** +- Select an instruction from dropdown (or type its name) +- Click Edit (✎) or Delete (−) button +- Confirm changes + + + +### Storage Location + +Custom instructions are stored as JSON files in: + +``` +~/.config/QtProject/qtcreator/qodeassist/quick_refactor/instructions/ +``` + +**File Naming Format:** +``` +Instruction_Name_with_underscores_{unique-uuid}.json +``` + +**Examples:** +``` +Add_Documentation_a7f3c92d-8e4b-4f1a-9c0e-1d2f3a4b5c6d.json +Optimize_Performance_3b8e4f9a-7c2d-4e1b-8f3a-9c1d2e3f4a5b.json +Fix_Code_Style_c5d6e7f8-9a0b-1c2d-3e4f-5a6b7c8d9e0f.json +``` + +**File Format:** +```json +{ + "id": "unique-uuid", + "name": "Add Documentation", + "body": "Add comprehensive documentation...", + "version": "0.1" +} +``` + +### Backup and Sharing + +Since instructions are simple JSON files, you can: + +1. **Backup**: Copy the instructions directory +2. **Share**: Share JSON files with team members +3. **Version Control**: Add to your dotfiles repository +4. **Edit Manually**: Modify JSON files directly if needed + +Click the **📁** button to quickly open the instructions folder in your file manager. + +## Context and Scope + +### What Gets Sent to the LLM + +The LLM receives: +- **Selected Code** (or current line if no selection) +- **Context**: Surrounding code (configurable amount) +- **File Information**: Language, file path +- **Cursor Position**: Marked with `` tag +- **Selection Markers**: `` and `` tags +- **Your Instructions**: Built-in, custom, or typed +- **Project Rules**: If configured (see [Project Rules](project-rules.md)) + +### Context Configuration + +Configure context amount in: `Qt Creator → Preferences → QodeAssist → Quick Refactor` + +Options: +- **Read Full File**: Send entire file as context +- **Read File Parts**: Send N lines before/after selection (configurable in "Read Strings Before/After Cursor") + +## Advanced Settings + +Access all refactoring settings in: `Qt Creator → Preferences → QodeAssist → Quick Refactor` + +### Available Options: + +**Context Settings:** +- Read full file vs. file parts +- Number of lines before/after cursor + +**LLM Parameters:** +- Temperature (creativity/randomness) +- Max tokens (response length) +- Top P (nucleus sampling) +- Top K (vocabulary filtering) +- Presence penalty +- Frequency penalty + +**Ollama-specific:** +- Lifetime parameter +- Context window size + +**Features:** +- Enable/disable tool calling +- Extended thinking mode (for supported models) +- Thinking budget and max tokens + +**Customization:** +- System prompt editing +- Use open files in context (optional) + +## Troubleshooting + +### Instruction Not Found +- Ensure you've typed the exact name or selected from dropdown +- Check if instruction file exists in instructions directory +- Reload Qt Creator if instructions were added externally + +### Poor Results +- Try adding more specific details in the additional text field +- Adjust context settings to provide more/less code +- Use extended thinking mode for complex refactorings +- Check if your model supports the complexity of the task + +### Instructions Not Loading +- Verify folder exists: `~/.config/QtProject/qtcreator/qodeassist/quick_refactor/instructions/` +- Check JSON file format validity +- Review Qt Creator logs for parsing errors +- Try restarting Qt Creator + +Fully local setup for offline or secure environments. + +## Related Documentation + +- [Project Rules](project-rules.md) - Project-specific AI behavior customization +- [File Context](file-context.md) - Attaching files to chat context +- [Ignoring Files](ignoring-files.md) - Exclude files from AI context +- [Provider Configuration](../README.md#configuration) - Setting up LLM providers diff --git a/qodeassist.cpp b/qodeassist.cpp index f85ab8d..77d63f3 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -59,6 +59,7 @@ #include "settings/ProjectSettingsPanel.hpp" #include "settings/SettingsConstants.hpp" #include "templates/Templates.hpp" +#include "widgets/CustomInstructionsManager.hpp" #include "widgets/QuickRefactorDialog.hpp" #include #include @@ -127,6 +128,8 @@ public: Providers::registerProviders(); Templates::registerTemplates(); + + CustomInstructionsManager::instance().loadInstructions(); Utils::Icon QCODEASSIST_ICON( {{":/resources/images/qoderassist-icon.png", Utils::Theme::IconsBaseColor}}); diff --git a/widgets/AddCustomInstructionDialog.cpp b/widgets/AddCustomInstructionDialog.cpp new file mode 100644 index 0000000..1aba178 --- /dev/null +++ b/widgets/AddCustomInstructionDialog.cpp @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +#include "AddCustomInstructionDialog.hpp" +#include "QodeAssisttr.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace QodeAssist { + +AddCustomInstructionDialog::AddCustomInstructionDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(Tr::tr("Add Custom Instruction")); + setupUi(); + resize(500, 400); +} + +AddCustomInstructionDialog::AddCustomInstructionDialog(const CustomInstruction &instruction, QWidget *parent) + : QDialog(parent) + , m_instruction(instruction) +{ + setWindowTitle(Tr::tr("Edit Custom Instruction")); + setupUi(); + m_nameEdit->setText(instruction.name); + m_bodyEdit->setPlainText(instruction.body); + resize(500, 400); +} + +void AddCustomInstructionDialog::setupUi() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(10, 10, 10, 10); + mainLayout->setSpacing(10); + + QFormLayout *formLayout = new QFormLayout(); + m_nameEdit = new QLineEdit(this); + m_nameEdit->setPlaceholderText(Tr::tr("Enter instruction name...")); + formLayout->addRow(Tr::tr("Name:"), m_nameEdit); + + mainLayout->addLayout(formLayout); + + QLabel *bodyLabel = new QLabel(Tr::tr("Instruction Body:"), this); + mainLayout->addWidget(bodyLabel); + + m_bodyEdit = new QPlainTextEdit(this); + m_bodyEdit->setPlaceholderText( + Tr::tr("Enter the refactoring instruction that will be sent to the LLM...")); + mainLayout->addWidget(m_bodyEdit); + + QDialogButtonBox *buttonBox + = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, this); + + connect(buttonBox, &QDialogButtonBox::accepted, this, [this]() { + if (m_nameEdit->text().trimmed().isEmpty()) { + QMessageBox::warning(this, Tr::tr("Invalid Input"), Tr::tr("Instruction name cannot be empty.")); + return; + } + if (m_bodyEdit->toPlainText().trimmed().isEmpty()) { + QMessageBox::warning(this, Tr::tr("Invalid Input"), Tr::tr("Instruction body cannot be empty.")); + return; + } + accept(); + }); + + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + mainLayout->addWidget(buttonBox); +} + +CustomInstruction AddCustomInstructionDialog::getInstruction() const +{ + CustomInstruction instruction = m_instruction; + instruction.name = m_nameEdit->text().trimmed(); + instruction.body = m_bodyEdit->toPlainText().trimmed(); + return instruction; +} + +} // namespace QodeAssist + diff --git a/widgets/AddCustomInstructionDialog.hpp b/widgets/AddCustomInstructionDialog.hpp new file mode 100644 index 0000000..fc3968b --- /dev/null +++ b/widgets/AddCustomInstructionDialog.hpp @@ -0,0 +1,52 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include "CustomInstructionsManager.hpp" + +class QLineEdit; +class QPlainTextEdit; + +namespace QodeAssist { + +class AddCustomInstructionDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AddCustomInstructionDialog(QWidget *parent = nullptr); + explicit AddCustomInstructionDialog(const CustomInstruction &instruction, QWidget *parent = nullptr); + ~AddCustomInstructionDialog() override = default; + + CustomInstruction getInstruction() const; + +private: + void setupUi(); + + QLineEdit *m_nameEdit; + QPlainTextEdit *m_bodyEdit; + CustomInstruction m_instruction; +}; + +} // namespace QodeAssist + diff --git a/widgets/CustomInstructionsManager.cpp b/widgets/CustomInstructionsManager.cpp new file mode 100644 index 0000000..93fb3b6 --- /dev/null +++ b/widgets/CustomInstructionsManager.cpp @@ -0,0 +1,225 @@ +/* + * 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 . + */ + +#include "CustomInstructionsManager.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace QodeAssist { + +CustomInstructionsManager::CustomInstructionsManager(QObject *parent) + : QObject(parent) +{} + +CustomInstructionsManager &CustomInstructionsManager::instance() +{ + static CustomInstructionsManager instance; + return instance; +} + +QString CustomInstructionsManager::getInstructionsDirectory() const +{ + QString path = QString("%1/qodeassist/quick_refactor/instructions") + .arg(Core::ICore::userResourcePath().toFSPathString()); + return path; +} + +bool CustomInstructionsManager::ensureDirectoryExists() const +{ + QDir dir(getInstructionsDirectory()); + if (!dir.exists()) { + return dir.mkpath("."); + } + return true; +} + +bool CustomInstructionsManager::loadInstructions() +{ + m_instructions.clear(); + + if (!ensureDirectoryExists()) { + LOG_MESSAGE("Failed to create instructions directory"); + return false; + } + + QDir dir(getInstructionsDirectory()); + QStringList filters; + filters << "*.json"; + QFileInfoList files = dir.entryInfoList(filters, QDir::Files); + + for (const QFileInfo &fileInfo : files) { + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QIODevice::ReadOnly)) { + LOG_MESSAGE(QString("Failed to open instruction file: %1").arg(fileInfo.fileName())); + continue; + } + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + LOG_MESSAGE( + QString("Failed to parse instruction file %1: %2") + .arg(fileInfo.fileName(), error.errorString())); + continue; + } + + QJsonObject obj = doc.object(); + CustomInstruction instruction; + instruction.id = obj["id"].toString(); + instruction.name = obj["name"].toString(); + instruction.body = obj["body"].toString(); + + if (instruction.id.isEmpty() || instruction.name.isEmpty()) { + LOG_MESSAGE(QString("Invalid instruction in file: %1").arg(fileInfo.fileName())); + continue; + } + + m_instructions.append(instruction); + } + + LOG_MESSAGE(QString("Loaded %1 custom instructions").arg(m_instructions.size())); + return true; +} + +bool CustomInstructionsManager::saveInstruction(const CustomInstruction &instruction) +{ + if (!ensureDirectoryExists()) { + LOG_MESSAGE("Failed to create instructions directory"); + return false; + } + + CustomInstruction newInstruction = instruction; + QString oldFileName; + + if (newInstruction.id.isEmpty()) { + newInstruction.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + } else { + // Check if instruction with this ID already exists and get old file name + for (int i = 0; i < m_instructions.size(); ++i) { + if (m_instructions[i].id == newInstruction.id) { + // Build old filename to delete it if name changed + QString oldName = m_instructions[i].name; + oldName.replace(' ', '_'); + oldFileName = QString("%1/%2_%3.json") + .arg(getInstructionsDirectory(), oldName, newInstruction.id); + break; + } + } + } + + int existingIndex = -1; + for (int i = 0; i < m_instructions.size(); ++i) { + if (m_instructions[i].id == newInstruction.id) { + existingIndex = i; + break; + } + } + + QJsonObject obj; + obj["id"] = newInstruction.id; + obj["name"] = newInstruction.name; + obj["body"] = newInstruction.body; + obj["version"] = "0.1"; + + QJsonDocument doc(obj); + + QString sanitizedName = newInstruction.name; + sanitizedName.replace(' ', '_'); + QString fileName = QString("%1/%2_%3.json") + .arg(getInstructionsDirectory(), sanitizedName, newInstruction.id); + + if (!oldFileName.isEmpty() && oldFileName != fileName) { + QFile::remove(oldFileName); + } + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + LOG_MESSAGE(QString("Failed to save instruction to file: %1").arg(fileName)); + return false; + } + + if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) { + LOG_MESSAGE(QString("Failed to write instruction data: %1").arg(file.errorString())); + return false; + } + + if (existingIndex >= 0) { + m_instructions[existingIndex] = newInstruction; + } else { + m_instructions.append(newInstruction); + } + + emit instructionsChanged(); + LOG_MESSAGE(QString("Saved custom instruction: %1").arg(newInstruction.name)); + return true; +} + +bool CustomInstructionsManager::deleteInstruction(const QString &id) +{ + int index = -1; + for (int i = 0; i < m_instructions.size(); ++i) { + if (m_instructions[i].id == id) { + index = i; + break; + } + } + + if (index < 0) { + LOG_MESSAGE(QString("Instruction not found: %1").arg(id)); + return false; + } + + QString sanitizedName = m_instructions[index].name; + sanitizedName.replace(' ', '_'); + QString fileName = QString("%1/%2_%3.json") + .arg(getInstructionsDirectory(), sanitizedName, id); + + QFile file(fileName); + if (!file.remove()) { + LOG_MESSAGE(QString("Failed to delete instruction file: %1").arg(fileName)); + return false; + } + + m_instructions.removeAt(index); + emit instructionsChanged(); + LOG_MESSAGE(QString("Deleted custom instruction with id: %1").arg(id)); + return true; +} + +CustomInstruction CustomInstructionsManager::getInstructionById(const QString &id) const +{ + for (const CustomInstruction &instruction : m_instructions) { + if (instruction.id == id) { + return instruction; + } + } + return CustomInstruction(); +} + +} // namespace QodeAssist + diff --git a/widgets/CustomInstructionsManager.hpp b/widgets/CustomInstructionsManager.hpp new file mode 100644 index 0000000..b08432c --- /dev/null +++ b/widgets/CustomInstructionsManager.hpp @@ -0,0 +1,63 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include +#include + +namespace QodeAssist { + +struct CustomInstruction +{ + QString id; + QString name; + QString body; +}; + +class CustomInstructionsManager : public QObject +{ + Q_OBJECT + +public: + static CustomInstructionsManager &instance(); + + bool loadInstructions(); + bool saveInstruction(const CustomInstruction &instruction); + bool deleteInstruction(const QString &id); + + QVector instructions() const { return m_instructions; } + CustomInstruction getInstructionById(const QString &id) const; + +signals: + void instructionsChanged(); + +private: + explicit CustomInstructionsManager(QObject *parent = nullptr); + ~CustomInstructionsManager() override = default; + + QString getInstructionsDirectory() const; + bool ensureDirectoryExists() const; + + QVector m_instructions; +}; + +} // namespace QodeAssist + diff --git a/widgets/QuickRefactorDialog.cpp b/widgets/QuickRefactorDialog.cpp index e53a6ef..3d02233 100644 --- a/widgets/QuickRefactorDialog.cpp +++ b/widgets/QuickRefactorDialog.cpp @@ -18,19 +18,31 @@ */ #include "QuickRefactorDialog.hpp" +#include "AddCustomInstructionDialog.hpp" +#include "CustomInstructionsManager.hpp" #include "QodeAssisttr.h" #include +#include +#include +#include #include +#include #include #include #include +#include +#include #include #include +#include #include #include +#include #include +#include + #include #include @@ -63,17 +75,79 @@ void QuickRefactorDialog::setupUi() actionsLayout->addStretch(); mainLayout->addLayout(actionsLayout); - m_instructionsLabel = new QLabel(Tr::tr("Enter refactoring instructions:"), this); + QHBoxLayout *instructionsLayout = new QHBoxLayout(); + instructionsLayout->setSpacing(4); + + QLabel *instructionsLabel = new QLabel(Tr::tr("Custom Instructions:"), this); + instructionsLayout->addWidget(instructionsLabel); + + m_commandsComboBox = new QComboBox(this); + m_commandsComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + m_commandsComboBox->setEditable(true); + m_commandsComboBox->setInsertPolicy(QComboBox::NoInsert); + m_commandsComboBox->lineEdit()->setPlaceholderText("Search or select instruction..."); + + QCompleter *completer = new QCompleter(this); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + m_commandsComboBox->setCompleter(completer); + + instructionsLayout->addWidget(m_commandsComboBox); + + m_addCommandButton = new QToolButton(this); + m_addCommandButton->setText("+"); + m_addCommandButton->setToolTip(Tr::tr("Add Custom Instruction")); + instructionsLayout->addWidget(m_addCommandButton); + + m_editCommandButton = new QToolButton(this); + m_editCommandButton->setText("✎"); + m_editCommandButton->setToolTip(Tr::tr("Edit Custom Instruction")); + instructionsLayout->addWidget(m_editCommandButton); + + m_deleteCommandButton = new QToolButton(this); + m_deleteCommandButton->setText("−"); + m_deleteCommandButton->setToolTip(Tr::tr("Delete Custom Instruction")); + instructionsLayout->addWidget(m_deleteCommandButton); + + m_openFolderButton = new QToolButton(this); + m_openFolderButton->setText("📁"); + m_openFolderButton->setToolTip(Tr::tr("Open Instructions Folder")); + instructionsLayout->addWidget(m_openFolderButton); + + mainLayout->addLayout(instructionsLayout); + + m_instructionsLabel = new QLabel(Tr::tr("Additional instructions (optional):"), this); mainLayout->addWidget(m_instructionsLabel); m_textEdit = new QPlainTextEdit(this); m_textEdit->setMinimumHeight(100); - m_textEdit->setPlaceholderText(Tr::tr("Type your refactoring instructions here...")); + m_textEdit->setPlaceholderText(Tr::tr("Add extra details or modifications to the selected instruction...")); connect(m_textEdit, &QPlainTextEdit::textChanged, this, &QuickRefactorDialog::updateDialogSize); + connect( + m_commandsComboBox, + QOverload::of(&QComboBox::currentIndexChanged), + this, + &QuickRefactorDialog::onCommandSelected); + connect(m_addCommandButton, &QToolButton::clicked, this, &QuickRefactorDialog::onAddCustomCommand); + connect( + m_editCommandButton, &QToolButton::clicked, this, &QuickRefactorDialog::onEditCustomCommand); + connect( + m_deleteCommandButton, + &QToolButton::clicked, + this, + &QuickRefactorDialog::onDeleteCustomCommand); + connect( + m_openFolderButton, + &QToolButton::clicked, + this, + &QuickRefactorDialog::onOpenInstructionsFolder); mainLayout->addWidget(m_textEdit); + loadCustomCommands(); + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); @@ -113,7 +187,22 @@ void QuickRefactorDialog::createActionButtons() QString QuickRefactorDialog::instructions() const { - return m_textEdit->toPlainText(); + QString result; + + CustomInstruction instruction = findCurrentInstruction(); + if (!instruction.id.isEmpty()) { + result = instruction.body; + } + + QString additionalText = m_textEdit->toPlainText().trimmed(); + if (!additionalText.isEmpty()) { + if (!result.isEmpty()) { + result += "\n\n"; + } + result += additionalText; + } + + return result; } void QuickRefactorDialog::setInstructions(const QString &instructions) @@ -145,6 +234,8 @@ bool QuickRefactorDialog::eventFilter(QObject *watched, QEvent *event) void QuickRefactorDialog::useLastInstructions() { if (!m_lastInstructions.isEmpty()) { + m_commandsComboBox->setCurrentIndex(0); + m_commandsComboBox->clearEditText(); // Clear search text m_textEdit->setPlainText(m_lastInstructions); m_selectedAction = Action::RepeatLast; } @@ -153,6 +244,8 @@ void QuickRefactorDialog::useLastInstructions() void QuickRefactorDialog::useImproveCodeTemplate() { + m_commandsComboBox->setCurrentIndex(0); + m_commandsComboBox->clearEditText(); // Clear search text m_textEdit->setPlainText(Tr::tr( "Improve the selected code by enhancing readability, efficiency, and maintainability. " "Follow best practices for C++/Qt and fix any potential issues.")); @@ -162,6 +255,8 @@ void QuickRefactorDialog::useImproveCodeTemplate() void QuickRefactorDialog::useAlternativeSolutionTemplate() { + m_commandsComboBox->setCurrentIndex(0); + m_commandsComboBox->clearEditText(); // Clear search text m_textEdit->setPlainText( Tr::tr("Suggest an alternative implementation approach for the selected code. " "Provide a different solution that might be cleaner, more efficient, " @@ -214,4 +309,155 @@ void QuickRefactorDialog::updateDialogSize() resize(newWidth, newHeight); } +void QuickRefactorDialog::loadCustomCommands() +{ + m_commandsComboBox->clear(); + m_commandsComboBox->addItem("", QString()); // Empty item for no selection + + auto &manager = CustomInstructionsManager::instance(); + const QVector &instructions = manager.instructions(); + + QStringList instructionNames; + for (const CustomInstruction &instruction : instructions) { + m_commandsComboBox->addItem(instruction.name, instruction.id); + instructionNames.append(instruction.name); + } + + if (m_commandsComboBox->completer()) { + QStringListModel *model = new QStringListModel(instructionNames, this); + m_commandsComboBox->completer()->setModel(model); + } + + bool hasInstructions = !instructions.isEmpty(); + m_editCommandButton->setEnabled(hasInstructions); + m_deleteCommandButton->setEnabled(hasInstructions); +} + +CustomInstruction QuickRefactorDialog::findCurrentInstruction() const +{ + QString currentText = m_commandsComboBox->currentText().trimmed(); + if (currentText.isEmpty()) { + return CustomInstruction(); + } + + auto &manager = CustomInstructionsManager::instance(); + const QVector &instructions = manager.instructions(); + + for (const CustomInstruction &instruction : instructions) { + if (instruction.name == currentText) { + return instruction; + } + } + + int currentIndex = m_commandsComboBox->currentIndex(); + if (currentIndex > 0) { + QString instructionId = m_commandsComboBox->itemData(currentIndex).toString(); + if (!instructionId.isEmpty()) { + return manager.getInstructionById(instructionId); + } + } + + return CustomInstruction(); +} + +void QuickRefactorDialog::onCommandSelected(int index) +{ + Q_UNUSED(index); +} + +void QuickRefactorDialog::onAddCustomCommand() +{ + AddCustomInstructionDialog dialog(this); + if (dialog.exec() == QDialog::Accepted) { + CustomInstruction instruction = dialog.getInstruction(); + auto &manager = CustomInstructionsManager::instance(); + + if (manager.saveInstruction(instruction)) { + loadCustomCommands(); + + m_commandsComboBox->setCurrentText(instruction.name); + + m_textEdit->clear(); + } else { + QMessageBox::warning( + this, + Tr::tr("Error"), + Tr::tr("Failed to save custom instruction. Check logs for details.")); + } + } +} + +void QuickRefactorDialog::onEditCustomCommand() +{ + CustomInstruction instruction = findCurrentInstruction(); + + if (instruction.id.isEmpty()) { + QMessageBox::information( + this, Tr::tr("No Instruction Selected"), Tr::tr("Please select an instruction to edit.")); + return; + } + + AddCustomInstructionDialog dialog(instruction, this); + if (dialog.exec() == QDialog::Accepted) { + CustomInstruction updatedInstruction = dialog.getInstruction(); + auto &manager = CustomInstructionsManager::instance(); + + if (manager.saveInstruction(updatedInstruction)) { + loadCustomCommands(); + m_commandsComboBox->setCurrentText(updatedInstruction.name); + m_textEdit->clear(); + } else { + QMessageBox::warning( + this, + Tr::tr("Error"), + Tr::tr("Failed to update custom instruction. Check logs for details.")); + } + } +} + +void QuickRefactorDialog::onDeleteCustomCommand() +{ + CustomInstruction instruction = findCurrentInstruction(); + + if (instruction.id.isEmpty()) { + QMessageBox::information( + this, Tr::tr("No Instruction Selected"), Tr::tr("Please select an instruction to delete.")); + return; + } + + QMessageBox::StandardButton reply = QMessageBox::question( + this, + Tr::tr("Confirm Deletion"), + Tr::tr("Are you sure you want to delete the instruction '%1'?").arg(instruction.name), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + auto &manager = CustomInstructionsManager::instance(); + if (manager.deleteInstruction(instruction.id)) { + loadCustomCommands(); + m_commandsComboBox->setCurrentIndex(0); + m_commandsComboBox->clearEditText(); + } else { + QMessageBox::warning( + this, + Tr::tr("Error"), + Tr::tr("Failed to delete custom instruction. Check logs for details.")); + } + } +} + +void QuickRefactorDialog::onOpenInstructionsFolder() +{ + QString path = QString("%1/qodeassist/quick_refactor/instructions") + .arg(Core::ICore::userResourcePath().toFSPathString()); + + QDir dir(path); + if (!dir.exists()) { + dir.mkpath("."); + } + + QUrl url = QUrl::fromLocalFile(dir.absolutePath()); + QDesktopServices::openUrl(url); +} + } // namespace QodeAssist diff --git a/widgets/QuickRefactorDialog.hpp b/widgets/QuickRefactorDialog.hpp index 0f5b7e8..df3f73f 100644 --- a/widgets/QuickRefactorDialog.hpp +++ b/widgets/QuickRefactorDialog.hpp @@ -21,10 +21,12 @@ #include #include +#include "CustomInstructionsManager.hpp" class QPlainTextEdit; class QToolButton; class QLabel; +class QComboBox; namespace QodeAssist { @@ -51,15 +53,27 @@ private slots: void useImproveCodeTemplate(); void useAlternativeSolutionTemplate(); void updateDialogSize(); + void onCommandSelected(int index); + void onAddCustomCommand(); + void onEditCustomCommand(); + void onDeleteCustomCommand(); + void onOpenInstructionsFolder(); + void loadCustomCommands(); private: void setupUi(); void createActionButtons(); + CustomInstruction findCurrentInstruction() const; QPlainTextEdit *m_textEdit; QToolButton *m_repeatButton; QToolButton *m_improveButton; QToolButton *m_alternativeButton; + QToolButton *m_addCommandButton; + QToolButton *m_editCommandButton; + QToolButton *m_deleteCommandButton; + QToolButton *m_openFolderButton; + QComboBox *m_commandsComboBox; QLabel *m_instructionsLabel; Action m_selectedAction = Action::Custom;