Compare commits

..

1 Commits

Author SHA1 Message Date
Petr Mironychev
17b0eb8186 🐛 fix: Set content type for requests 2024-12-12 15:55:27 +01:00
203 changed files with 1922 additions and 12416 deletions

View File

@@ -2,7 +2,7 @@
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug labels: ''
assignees: '' assignees: ''
--- ---
@@ -23,6 +23,16 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Log** **Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@@ -1,89 +0,0 @@
{
"name": "QodeAssist",
"vendor": "Petr Mironychev",
"tags": [
"code assistant",
"llm",
"ai"
],
"compatibility": "Qt 6.8.3",
"platforms": [
"Windows",
"macOS",
"Linux"
],
"license": "GPLv3",
"version": "0.5.8",
"status": "draft",
"is_pack": false,
"released_at": null,
"version_history": [
{
"version": "0.4.0",
"is_latest": false,
"released_at": "2024-01-24T15:00:00Z"
},
{
"version": "0.5.2",
"is_latest": false,
"released_at": "2025-03-13T17:00:00Z"
},
{
"version": "0.5.3",
"is_latest": false,
"released_at": "2025-03-14T11:00:00Z"
},
{
"version": "0.5.4",
"is_latest": false,
"released_at": "2025-03-17T03:00:00Z"
},
{
"version": "0.5.5",
"is_latest": false,
"released_at": "2025-03-20T19:00:00Z"
},
{
"version": "0.5.6",
"is_latest": false,
"released_at": "2025-04-04T19:00:00Z"
},
{
"version": "0.5.7",
"is_latest": false,
"released_at": "2025-04-14T01:00:00Z"
},
{
"version": "0.5.8",
"is_latest": true,
"released_at": "2025-04-17T10:00:00Z"
}
],
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
"small_icon": "https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41",
"description_paragraphs": [
{
"header": "Description",
"text": [
"QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment."
]
}
],
"description_links": [
{
"url": "https://github.com/Palm1r/QodeAssist",
"link_text": "Site"
}
],
"description_images": [
{
"url": "https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a",
"image_label": "Code Completion"
}
],
"copyright": "(C) Petr Mironychev",
"download_history": {
"download_count": 0
},
"plugin_sets": []
}

View File

@@ -1,147 +0,0 @@
const fs = require('fs');
const path = require('path');
const updatePluginData = (plugin, env, pluginQtcData) => {
const dictionary_platform = {
'Windows': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Windows-x64.7z`,
'Linux': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Linux-x64.7z`,
'macOS': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-macOS-universal.7z`
};
plugin.core_compat_version = env.QT_CREATOR_VERSION_INTERNAL;
plugin.core_version = env.QT_CREATOR_VERSION_INTERNAL;
plugin.status = "draft";
plugin.plugins.forEach(pluginsEntry => {
pluginsEntry.url = dictionary_platform[plugin.host_os];
pluginsEntry.meta_data = pluginQtcData;
});
return plugin;
};
const createNewPluginData = (env, platform, pluginQtcData) => {
const pluginJson = {
"status": "draft",
"core_compat_version": "<placeholder>",
"core_version": "<placeholder>",
"host_os": platform,
"host_os_version": "0", // TODO: pass the real data
"host_os_architecture": "x86_64", // TODO: pass the real data
"plugins": [
{
"url": "",
"size": 5000, // TODO: check if it is needed, pass the real data
"meta_data": {},
"dependencies": []
}
]
};
updatePluginData(pluginJson, env, pluginQtcData);
return pluginJson;
}
const updateServerPluginJson = (endJsonData, pluginQtcData, env) => {
// Update the global data in mainData
endJsonData.name = pluginQtcData.Name;
endJsonData.vendor = pluginQtcData.Vendor;
endJsonData.version = pluginQtcData.Version;
endJsonData.copyright = pluginQtcData.Copyright;
endJsonData.status = "draft";
endJsonData.version_history[0].version = pluginQtcData.Version;
endJsonData.description_paragraphs = [
{
header: "Description",
text: [
pluginQtcData.Description
]
}
];
let found = false;
// Update or Add the plugin data for the current Qt Creator version
for (const plugin of endJsonData.plugin_sets) {
if (plugin.core_compat_version === env.QT_CREATOR_VERSION_INTERNAL) {
updatePluginData(plugin, env, pluginQtcData);
found = true;
}
}
if (!found) {
for (const platform of ['Windows', 'Linux', 'macOS']) {
endJsonData.plugin_sets.push(createNewPluginData(env, platform, pluginQtcData));
}
}
// Save the updated JSON file
const serverPluginJsonPath = path.join(__dirname, `${env.PLUGIN_NAME}.json`);
fs.writeFileSync(serverPluginJsonPath, JSON.stringify(endJsonData, null, 2), 'utf8');
};
const request = async (type, url, token, data) => {
const response = await fetch(url, {
method: type,
headers: {
'Authorization': `Bearer ${token}`,
'accept': 'application/json',
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : undefined
});
if (!response.ok) {
const errorResponse = await response.json();
console.error(`${type} Request Error Response:`, errorResponse); // Log the error response
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
const put = (url, token, data) => request('PUT', url, token, data)
const post = (url, token, data) => request('POST', url, token, data)
const get = (url, token) => request('GET', url, token)
const purgeCache = async (env) => {
try {
await post(`${env.API_URL}api/v1/cache/purgeall`, env.TOKEN, {});
console.log('Cache purged successfully');
} catch (error) {
console.error('Error:', error);
}
};
async function main() {
const env = {
PLUGIN_DOWNLOAD_URL: process.env.PLUGIN_DOWNLOAD_URL || process.argv[2],
PLUGIN_NAME: process.env.PLUGIN_NAME || process.argv[3],
QT_CREATOR_VERSION: process.env.QT_CREATOR_VERSION || process.argv[4],
QT_CREATOR_VERSION_INTERNAL: process.env.QT_CREATOR_VERSION_INTERNAL || process.argv[5],
TOKEN: process.env.TOKEN || process.argv[6],
API_URL: process.env.API_URL || process.argv[7] || ''
};
const pluginQtcData = require(`../../${env.PLUGIN_NAME}-origin/${env.PLUGIN_NAME}.json`);
const templateFileData = require('./plugin.json');
if (env.API_URL === '') {
updateServerPluginJson(templateFileData, pluginQtcData, env);
process.exit(0);
}
const response = await get(`${env.API_URL}api/v1/admin/extensions?search=${env.PLUGIN_NAME}`, env.TOKEN);
if (response.items.length > 0 && response.items[0].extension_id !== '') {
const pluginId = response.items[0].extension_id;
console.log('Plugin found. Updating the plugin');
updateServerPluginJson(response.items[0], pluginQtcData, env);
await put(`${env.API_URL}api/v1/admin/extensions/${pluginId}`, env.TOKEN, response.items[0]);
} else {
console.log('No plugin found. Creating a new plugin');
updateServerPluginJson(templateFileData, pluginQtcData, env);
await post(`${env.API_URL}api/v1/admin/extensions`, env.TOKEN, templateFileData);
}
// await purgeCache(env);
}
main().then(() => console.log('JSON file updated successfully'));

View File

@@ -9,12 +9,11 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
env: env:
PLUGIN_NAME: QodeAssist PLUGIN_NAME: QodeAssist
QT_VERSION: 6.8.3 QT_VERSION: 6.7.3
QT_CREATOR_VERSION: 16.0.1 QT_CREATOR_VERSION: 14.0.2
QT_CREATOR_VERSION_INTERNAL: 16.0.1 QT_CREATOR_SNAPSHOT: NO
MACOS_DEPLOYMENT_TARGET: "11.0" MACOS_DEPLOYMENT_TARGET: "11.0"
CMAKE_VERSION: "3.29.6" CMAKE_VERSION: "3.29.6"
NINJA_VERSION: "1.12.1" NINJA_VERSION: "1.12.1"
@@ -31,46 +30,76 @@ jobs:
- { - {
name: "Windows Latest MSVC", artifact: "Windows-x64", name: "Windows Latest MSVC", artifact: "Windows-x64",
os: windows-latest, os: windows-latest,
platform: windows_x64,
cc: "cl", cxx: "cl", cc: "cl", cxx: "cl",
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat", environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
} }
- { - {
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64", name: "Ubuntu Latest GCC", artifact: "Linux-x64",
os: ubuntu-22.04, os: ubuntu-latest,
platform: linux_x64,
cc: "gcc", cxx: "g++" cc: "gcc", cxx: "g++"
} }
- { - {
name: "macOS Latest Clang", artifact: "macOS-universal", name: "macOS Latest Clang", artifact: "macOS-universal",
os: macos-latest, os: macos-latest,
platform: mac_x64,
cc: "clang", cxx: "clang++" cc: "clang", cxx: "clang++"
} }
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Checkout submodules - name: Checkout submodules
id: git id: git
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
if (${{github.ref}} MATCHES "tags/v(.*)") if (${{github.ref}} MATCHES "tags/v(.*)")
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}") file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}\n")
else() else()
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}") file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}\n")
endif() endif()
- name: Download Ninja and CMake - name: Download Ninja and CMake
uses: lukka/get-cmake@latest shell: cmake -P {0}
with: run: |
cmakeVersion: ${{ env.CMAKE_VERSION }} set(cmake_version "$ENV{CMAKE_VERSION}")
ninjaVersion: ${{ env.NINJA_VERSION }} set(ninja_version "$ENV{NINJA_VERSION}")
- name: Install dependencies if ("${{ runner.os }}" STREQUAL "Windows")
set(ninja_suffix "win.zip")
set(cmake_suffix "windows-x86_64.zip")
set(cmake_dir "cmake-${cmake_version}-windows-x86_64/bin")
elseif ("${{ runner.os }}" STREQUAL "Linux")
set(ninja_suffix "linux.zip")
set(cmake_suffix "linux-x86_64.tar.gz")
set(cmake_dir "cmake-${cmake_version}-linux-x86_64/bin")
elseif ("${{ runner.os }}" STREQUAL "macOS")
set(ninja_suffix "mac.zip")
set(cmake_suffix "macos-universal.tar.gz")
set(cmake_dir "cmake-${cmake_version}-macos-universal/CMake.app/Contents/bin")
endif()
set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")
file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)
set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")
file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)
# Add to PATH environment variable
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)
set(path_separator ":")
if ("${{ runner.os }}" STREQUAL "Windows")
set(path_separator ";")
endif()
file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}")
if (NOT "${{ runner.os }}" STREQUAL "Windows")
execute_process(
COMMAND chmod +x ninja
COMMAND chmod +x ${cmake_dir}/cmake
)
endif()
- name: Install system libs
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
if ("${{ runner.os }}" STREQUAL "Linux") if ("${{ runner.os }}" STREQUAL "Linux")
@@ -78,13 +107,7 @@ jobs:
COMMAND sudo apt update COMMAND sudo apt update
) )
execute_process( execute_process(
COMMAND sudo apt install COMMAND sudo apt install libgl1-mesa-dev libcups2-dev
# build dependencies
libgl1-mesa-dev libgtest-dev libgmock-dev
# runtime dependencies for tests (Qt is downloaded outside package manager,
# thus minimal dependencies must be installed explicitly)
libsecret-1-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0
libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb-xkb1 libxkbcommon-x11-0 xvfb
RESULT_VARIABLE result RESULT_VARIABLE result
) )
if (NOT result EQUAL 0) if (NOT result EQUAL 0)
@@ -101,9 +124,9 @@ jobs:
string(REPLACE "." "" qt_version_dotless "${qt_version}") string(REPLACE "." "" qt_version_dotless "${qt_version}")
if ("${{ runner.os }}" STREQUAL "Windows") if ("${{ runner.os }}" STREQUAL "Windows")
set(url_os "windows_x86") set(url_os "windows_x86")
set(qt_package_arch_suffix "win64_msvc2022_64") set(qt_package_arch_suffix "win64_msvc2019_64")
set(qt_dir_prefix "${qt_version}/msvc2022_64") set(qt_dir_prefix "${qt_version}/msvc2019_64")
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64") set(qt_package_suffix "-Windows-Windows_10_22H2-MSVC2019-Windows-Windows_10_22H2-X86_64")
elseif ("${{ runner.os }}" STREQUAL "Linux") elseif ("${{ runner.os }}" STREQUAL "Linux")
set(url_os "linux_x64") set(url_os "linux_x64")
if (qt_version VERSION_LESS "6.7.0") if (qt_version VERSION_LESS "6.7.0")
@@ -112,15 +135,15 @@ jobs:
set(qt_package_arch_suffix "linux_gcc_64") set(qt_package_arch_suffix "linux_gcc_64")
endif() endif()
set(qt_dir_prefix "${qt_version}/gcc_64") set(qt_dir_prefix "${qt_version}/gcc_64")
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64") set(qt_package_suffix "-Linux-RHEL_8_8-GCC-Linux-RHEL_8_8-X86_64")
elseif ("${{ runner.os }}" STREQUAL "macOS") elseif ("${{ runner.os }}" STREQUAL "macOS")
set(url_os "mac_x64") set(url_os "mac_x64")
set(qt_package_arch_suffix "clang_64") set(qt_package_arch_suffix "clang_64")
set(qt_dir_prefix "${qt_version}/macos") set(qt_dir_prefix "${qt_version}/macos")
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64") set(qt_package_suffix "-MacOS-MacOS_13-Clang-MacOS-MacOS_13-X86_64-ARM64")
endif() endif()
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}") set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}")
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS) file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
file(READ ./Updates.xml updates_xml) file(READ ./Updates.xml updates_xml)
@@ -130,7 +153,7 @@ jobs:
file(MAKE_DIRECTORY qt6) file(MAKE_DIRECTORY qt6)
# Save the path for other steps # Save the path for other steps
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6" qt_dir) file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6/${qt_dir_prefix}" qt_dir)
file(APPEND "$ENV{GITHUB_OUTPUT}" "qt_dir=${qt_dir}") file(APPEND "$ENV{GITHUB_OUTPUT}" "qt_dir=${qt_dir}")
message("Downloading Qt to ${qt_dir}") message("Downloading Qt to ${qt_dir}")
@@ -149,17 +172,11 @@ jobs:
foreach(package qt5compat qtshadertools) foreach(package qt5compat qtshadertools)
downloadAndExtract( downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z" "${qt_base_url}/qt.qt6.${qt_version_dotless}.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z ${package}.7z
) )
endforeach() endforeach()
function(downloadAndExtractLibicu url archive)
message("Downloading ${url}")
file(DOWNLOAD "${url}" ./${archive} SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../../${archive} WORKING_DIRECTORY qt6/lib)
endfunction()
# uic depends on libicu*.so # uic depends on libicu*.so
if ("${{ runner.os }}" STREQUAL "Linux") if ("${{ runner.os }}" STREQUAL "Linux")
if (qt_version VERSION_LESS "6.7.0") if (qt_version VERSION_LESS "6.7.0")
@@ -167,25 +184,47 @@ jobs:
else() else()
set(uic_suffix "Rhel8.6-x86_64") set(uic_suffix "Rhel8.6-x86_64")
endif() endif()
downloadAndExtractLibicu( downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-${uic_suffix}.7z" "${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-${uic_suffix}.7z"
icu.7z icu.7z
) )
endif() endif()
- name: Download Qt Creator - name: Download Qt Creator
uses: qt-creator/install-dev-package@v1.2
with:
version: ${{ env.QT_CREATOR_VERSION }}
unzip-to: 'qtcreator'
- name: Extract Qt Creator
id: qt_creator id: qt_creator
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
string(REGEX MATCH "([0-9]+.[0-9]+).[0-9]+" outvar "$ENV{QT_CREATOR_VERSION}")
set(qtc_base_url "https://download.qt.io/official_releases/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source")
set(qtc_snapshot "$ENV{QT_CREATOR_SNAPSHOT}")
if (qtc_snapshot)
set(qtc_base_url "https://download.qt.io/snapshots/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source/${qtc_snapshot}")
endif()
if ("${{ runner.os }}" STREQUAL "Windows")
set(qtc_platform "windows_x64")
elseif ("${{ runner.os }}" STREQUAL "Linux")
set(qtc_platform "linux_x64")
elseif ("${{ runner.os }}" STREQUAL "macOS")
set(qtc_platform "mac_x64")
endif()
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qtcreator" qtc_dir) file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qtcreator" qtc_dir)
# Save the path for other steps
file(APPEND "$ENV{GITHUB_OUTPUT}" "qtc_dir=${qtc_dir}") file(APPEND "$ENV{GITHUB_OUTPUT}" "qtc_dir=${qtc_dir}")
file(MAKE_DIRECTORY qtcreator)
message("Downloading Qt Creator from ${qtc_base_url}/${qtc_platform}")
foreach(package qtcreator qtcreator_dev)
file(DOWNLOAD
"${qtc_base_url}/${qtc_platform}/${package}.7z" ./${package}.7z SHOW_PROGRESS)
execute_process(COMMAND
${CMAKE_COMMAND} -E tar xvf ../${package}.7z WORKING_DIRECTORY qtcreator)
endforeach()
- name: Build - name: Build
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
@@ -223,7 +262,7 @@ jobs:
COMMAND python COMMAND python
-u -u
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}" "${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}" --name "$ENV{PLUGIN_NAME}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
--src . --src .
--build build --build build
--qt-path "${{ steps.qt.outputs.qt_dir }}" --qt-path "${{ steps.qt.outputs.qt_dir }}"
@@ -241,68 +280,13 @@ jobs:
- name: Upload - name: Upload
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
# The json is the same for all platforms, but we need to save one
- name: Upload plugin json
if: startsWith(matrix.config.os, 'ubuntu')
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./build/build/${{ env.PLUGIN_NAME }}.json
- name: Run unit tests
if: startsWith(matrix.config.os, 'ubuntu')
run: |
xvfb-run ./build/build/test/QodeAssistTest
update_json:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-22.04
needs: build
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Download the JSON file
uses: actions/download-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./${{ env.PLUGIN_NAME }}-origin
- name: Store Release upload_url
run: |
RELEASE_HTML_URL=$(echo "${{github.event.repository.html_url}}/releases/download/v${{ needs.build.outputs.tag }}")
echo "RELEASE_HTML_URL=${RELEASE_HTML_URL}" >> $GITHUB_ENV
- name: Run the Node.js script to update JSON
env:
QT_TOKEN: ${{ secrets.TOKEN }}
API_URL: ${{ secrets.API_URL }}
run: |
node .github/scripts/registerPlugin.js ${{ env.RELEASE_HTML_URL }} ${{ env.PLUGIN_NAME }} ${{ env.QT_CREATOR_VERSION }} ${{ env.QT_CREATOR_VERSION_INTERNAL }} ${{ env.QT_TOKEN }} ${{ env.API_URL }}
- name: Delete previous json artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.PLUGIN_NAME }}*-json
- name: Upload the modified JSON file as an artifact
uses: actions/upload-artifact@v4
with:
name: plugin-json
path: .github/scripts/${{ env.PLUGIN_NAME }}.json
release: release:
if: contains(github.ref, 'tags/v') if: contains(github.ref, 'tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build, update_json] needs: build
steps: steps:
- name: Download artifacts - name: Download artifacts

View File

@@ -1,24 +0,0 @@
name: Check formatting
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang-format-19
- name: Check formatting
run: |
clang-format-19 --style=file -i $(git ls-files | fgrep .hpp)
clang-format-19 --style=file -i $(git ls-files | fgrep .cpp)
git diff --exit-code || exit 1

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "3rdparty/inja"]
path = 3rdparty/inja
url = https://github.com/pantor/inja

1
3rdparty/inja vendored

Submodule 3rdparty/inja deleted from 384a6bef3f

View File

@@ -8,38 +8,14 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
find_package(QtCreator REQUIRED COMPONENTS Core) find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test REQUIRED) find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED)
find_package(GTest)
# IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
set(QODEASSIST_QT_CREATOR_VERSION_MAJOR ${CMAKE_MATCH_1})
set(QODEASSIST_QT_CREATOR_VERSION_MINOR ${CMAKE_MATCH_2})
set(QODEASSIST_QT_CREATOR_VERSION_PATCH ${CMAKE_MATCH_3})
if(NOT version_match)
message(FATAL_ERROR "Failed to parse Qt Creator version string: ${IDE_VERSION}")
endif()
message(STATUS "Qt Creator Version: ${QODEASSIST_QT_CREATOR_VERSION_MAJOR}.${QODEASSIST_QT_CREATOR_VERSION_MINOR}.${QODEASSIST_QT_CREATOR_VERSION_PATCH}")
add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
)
add_subdirectory(llmcore) add_subdirectory(llmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(ChatView) add_subdirectory(ChatView)
add_subdirectory(context)
if(GTest_FOUND)
add_subdirectory(test)
endif()
add_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
@@ -66,63 +42,31 @@ add_qtc_plugin(QodeAssist
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp templates/Templates.hpp
templates/CodeLlamaFim.hpp templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp templates/CustomFimTemplate.hpp
templates/Qwen.hpp templates/Qwen.hpp
templates/OpenAICompatible.hpp
templates/Ollama.hpp
templates/BasicChat.hpp
templates/Llama3.hpp templates/Llama3.hpp
templates/ChatML.hpp templates/ChatML.hpp
templates/Alpaca.hpp templates/Alpaca.hpp
templates/Llama2.hpp templates/Llama2.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
providers/Providers.hpp providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
QodeAssistClient.hpp QodeAssistClient.cpp QodeAssistClient.hpp QodeAssistClient.cpp
DocumentContextReader.hpp DocumentContextReader.cpp
utils/CounterTooltip.hpp utils/CounterTooltip.cpp
core/ChangesManager.h core/ChangesManager.cpp
chat/ChatOutputPane.h chat/ChatOutputPane.cpp chat/ChatOutputPane.h chat/ChatOutputPane.cpp
chat/NavigationPanel.hpp chat/NavigationPanel.cpp chat/NavigationPanel.hpp chat/NavigationPanel.cpp
ConfigurationManager.hpp ConfigurationManager.cpp ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
) )
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable
NAMES
qtcreator "Qt Creator"
PATHS
"${QtCreatorCorePath}/../../../bin"
"${QtCreatorCorePath}/../../../MacOS"
NO_DEFAULT_PATH
)
if (QtCreatorExecutable)
add_custom_target(RunQtCreator
COMMAND ${QtCreatorExecutable} -pluginpath $<TARGET_FILE_DIR:QodeAssist>
DEPENDS QodeAssist
)
set_target_properties(RunQtCreator PROPERTIES FOLDER "qtc_runnable")
endif()

View File

@@ -1,8 +1,8 @@
qt_add_library(QodeAssistChatView STATIC) qt_add_library(QodeAssistChatView STATIC)
qt_policy(SET QTP0001 NEW) qt_policy(SET QTP0001 NEW)
qt_policy(SET QTP0004 NEW)
# URI name should match the subdirectory name to suppress the warning
qt_add_qml_module(QodeAssistChatView qt_add_qml_module(QodeAssistChatView
URI ChatView URI ChatView
VERSION 1.0 VERSION 1.0
@@ -13,17 +13,6 @@ qt_add_qml_module(QodeAssistChatView
qml/Badge.qml qml/Badge.qml
qml/dialog/CodeBlock.qml qml/dialog/CodeBlock.qml
qml/dialog/TextBlock.qml qml/dialog/TextBlock.qml
qml/controls/QoAButton.qml
qml/parts/TopBar.qml
qml/parts/BottomBar.qml
qml/parts/AttachedFilesPlace.qml
RESOURCES
icons/attach-file-light.svg
icons/attach-file-dark.svg
icons/close-dark.svg
icons/close-light.svg
icons/link-file-light.svg
icons/link-file-dark.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp
ChatModel.hpp ChatModel.cpp ChatModel.hpp ChatModel.cpp
@@ -31,7 +20,6 @@ qt_add_qml_module(QodeAssistChatView
ClientInterface.hpp ClientInterface.cpp ClientInterface.hpp ClientInterface.cpp
MessagePart.hpp MessagePart.hpp
ChatUtils.h ChatUtils.cpp ChatUtils.h ChatUtils.cpp
ChatSerializer.hpp ChatSerializer.cpp
) )
target_link_libraries(QodeAssistChatView target_link_libraries(QodeAssistChatView
@@ -44,7 +32,6 @@ target_link_libraries(QodeAssistChatView
QtCreator::Utils QtCreator::Utils
LLMCore LLMCore
QodeAssistSettings QodeAssistSettings
Context
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -18,9 +18,9 @@
*/ */
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include <utils/aspects.h>
#include <QtCore/qjsonobject.h> #include <QtCore/qjsonobject.h>
#include <QtQml> #include <QtQml>
#include <utils/aspects.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
@@ -28,14 +28,14 @@ namespace QodeAssist::Chat {
ChatModel::ChatModel(QObject *parent) ChatModel::ChatModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_totalTokens(0)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
connect( connect(&settings.chatTokensThreshold,
&settings.chatTokensThreshold, &Utils::BaseAspect::changed,
&Utils::BaseAspect::changed, this,
this, &ChatModel::tokensThresholdChanged);
&ChatModel::tokensThresholdChanged);
} }
int ChatModel::rowCount(const QModelIndex &parent) const int ChatModel::rowCount(const QModelIndex &parent) const
@@ -55,13 +55,6 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
case Roles::Content: { case Roles::Content: {
return message.content; return message.content;
} }
case Roles::Attachments: {
QStringList filenames;
for (const auto &attachment : message.attachments) {
filenames << attachment.filename;
}
return filenames;
}
default: default:
return QVariant(); return QVariant();
} }
@@ -72,37 +65,29 @@ QHash<int, QByteArray> ChatModel::roleNames() const
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[Roles::RoleType] = "roleType"; roles[Roles::RoleType] = "roleType";
roles[Roles::Content] = "content"; roles[Roles::Content] = "content";
roles[Roles::Attachments] = "attachments";
return roles; return roles;
} }
void ChatModel::addMessage( void ChatModel::addMessage(const QString &content, ChatRole role, const QString &id)
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments)
{ {
QString fullContent = content; int tokenCount = estimateTokenCount(content);
if (!attachments.isEmpty()) {
fullContent += "\n\nAttached files list:";
for (const auto &attachment : attachments) {
fullContent += QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
}
}
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) { if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
Message &lastMessage = m_messages.last(); Message &lastMessage = m_messages.last();
int oldTokenCount = lastMessage.tokenCount;
lastMessage.content = content; lastMessage.content = content;
lastMessage.attachments = attachments; lastMessage.tokenCount = tokenCount;
m_totalTokens += (tokenCount - oldTokenCount);
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1)); emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else { } else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size()); beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{role, content, id}; m_messages.append({role, content, tokenCount, id});
newMessage.attachments = attachments; m_totalTokens += tokenCount;
m_messages.append(newMessage);
endInsertRows(); endInsertRows();
} }
trim();
emit totalTokensChanged();
} }
QVector<ChatModel::Message> ChatModel::getChatHistory() const QVector<ChatModel::Message> ChatModel::getChatHistory() const
@@ -110,12 +95,32 @@ QVector<ChatModel::Message> ChatModel::getChatHistory() const
return m_messages; return m_messages;
} }
void ChatModel::trim()
{
while (m_totalTokens > tokensThreshold()) {
if (!m_messages.isEmpty()) {
m_totalTokens -= m_messages.first().tokenCount;
beginRemoveRows(QModelIndex(), 0, 0);
m_messages.removeFirst();
endRemoveRows();
} else {
break;
}
}
}
int ChatModel::estimateTokenCount(const QString &text) const
{
return text.length() / 4;
}
void ChatModel::clear() void ChatModel::clear()
{ {
beginResetModel(); beginResetModel();
m_messages.clear(); m_messages.clear();
m_totalTokens = 0;
endResetModel(); endResetModel();
emit modelReseted(); emit totalTokensChanged();
} }
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
@@ -128,8 +133,7 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
while (blockMatches.hasNext()) { while (blockMatches.hasNext()) {
auto match = blockMatches.next(); auto match = blockMatches.next();
if (match.capturedStart() > lastIndex) { if (match.capturedStart() > lastIndex) {
QString textBetween QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
if (!textBetween.isEmpty()) { if (!textBetween.isEmpty()) {
parts.append({MessagePart::Text, textBetween, ""}); parts.append({MessagePart::Text, textBetween, ""});
} }
@@ -151,6 +155,7 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const
{ {
QJsonArray messages; QJsonArray messages;
messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}}); messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}});
for (const auto &message : m_messages) { for (const auto &message : m_messages) {
@@ -165,27 +170,17 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
default: default:
continue; continue;
} }
messages.append(QJsonObject{{"role", role}, {"content", message.content}});
QString content
= message.attachments.isEmpty()
? message.content
: message.content + "\n\nAttached files list:"
+ std::accumulate(
message.attachments.begin(),
message.attachments.end(),
QString(),
[](QString acc, const Context::ContentFile &attachment) {
return acc
+ QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
});
messages.append(QJsonObject{{"role", role}, {"content", content}});
} }
return messages; return messages;
} }
int ChatModel::totalTokens() const
{
return m_totalTokens;
}
int ChatModel::tokensThreshold() const int ChatModel::tokensThreshold() const
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
@@ -197,16 +192,4 @@ QString ChatModel::lastMessageId() const
return !m_messages.isEmpty() ? m_messages.last().id : ""; return !m_messages.isEmpty() ? m_messages.last().id : "";
} }
void ChatModel::resetModelTo(int index)
{
if (index < 0 || index >= m_messages.size())
return;
if (index < m_messages.size() - 1) {
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
m_messages.remove(index, m_messages.size() - index);
endRemoveRows();
}
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -26,29 +26,27 @@
#include <QJsonArray> #include <QJsonArray>
#include <QtQmlIntegration> #include <QtQmlIntegration>
#include "context/ContentFile.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel : public QAbstractListModel class ChatModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(int totalTokens READ totalTokens NOTIFY totalTokensChanged FINAL)
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL) Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
enum Roles { RoleType = Qt::UserRole, Content };
enum ChatRole { System, User, Assistant }; enum ChatRole { System, User, Assistant };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
struct Message struct Message
{ {
ChatRole role; ChatRole role;
QString content; QString content;
int tokenCount;
QString id; QString id;
QList<Context::ContentFile> attachments;
}; };
explicit ChatModel(QObject *parent = nullptr); explicit ChatModel(QObject *parent = nullptr);
@@ -57,30 +55,29 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addMessage( Q_INVOKABLE void addMessage(const QString &content, ChatRole role, const QString &id);
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {});
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const; Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() const; QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const; QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
int totalTokens() const;
int tokensThreshold() const; int tokensThreshold() const;
QString currentModel() const; QString currentModel() const;
QString lastMessageId() const; QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index);
signals: signals:
void totalTokensChanged();
void tokensThresholdChanged(); void tokensThresholdChanged();
void modelReseted();
private: private:
void trim();
int estimateTokenCount(const QString &text) const;
QVector<Message> m_messages; QVector<Message> m_messages;
int m_totalTokens = 0;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -18,92 +18,33 @@
*/ */
#include "ChatRootView.hpp" #include "ChatRootView.hpp"
#include <QtGui/qclipboard.h>
#include <QClipboard>
#include <QDesktopServices>
#include <QFileDialog>
#include <QMessageBox>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatRootView::ChatRootView(QQuickItem *parent) ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent) : QQuickItem(parent)
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_promptProvider(LLMCore::PromptTemplateManager::instance()) , m_clientInterface(new ClientInterface(m_chatModel, this))
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
{ {
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect(
&Settings::chatAssistantSettings().linkOpenFiles,
&Utils::BaseAspect::changed,
this,
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); });
auto &settings = Settings::generalSettings(); auto &settings = Settings::generalSettings();
connect( connect(&settings.caModel,
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged); &Utils::BaseAspect::changed,
this,
&ChatRootView::currentTemplateChanged);
connect( connect(&Settings::chatAssistantSettings().sharingCurrentFile,
m_clientInterface, &Utils::BaseAspect::changed,
&ClientInterface::messageReceivedCompletely, this,
this, &ChatRootView::isSharingCurrentFileChanged);
&ChatRootView::autosave);
connect( generateColors();
m_clientInterface,
&ClientInterface::messageReceivedCompletely,
this,
&ChatRootView::updateInputTokensCount);
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
connect(
&Settings::chatAssistantSettings().useSystemPrompt,
&Utils::BaseAspect::changed,
this,
&ChatRootView::updateInputTokensCount);
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&ChatRootView::updateInputTokensCount);
auto editors = Core::EditorManager::instance();
connect(editors, &Core::EditorManager::editorCreated, this, &ChatRootView::onEditorCreated);
connect(
editors,
&Core::EditorManager::editorAboutToClose,
this,
&ChatRootView::onEditorAboutToClose);
connect(editors, &Core::EditorManager::currentEditorAboutToChange, this, [this]() {
if (m_isSyncOpenFiles) {
for (auto editor : std::as_const(m_currentEditors)) {
onAppendLinkFileFromEditor(editor);
}
}
});
updateInputTokensCount();
} }
ChatModel *ChatRootView::chatModel() const ChatModel *ChatRootView::chatModel() const
@@ -111,26 +52,14 @@ ChatModel *ChatRootView::chatModel() const
return m_chatModel; return m_chatModel;
} }
void ChatRootView::sendMessage(const QString &message) QColor ChatRootView::backgroundColor() const
{ {
if (m_inputTokensCount > m_chatModel->tokensThreshold()) { return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
QMessageBox::StandardButton reply = QMessageBox::question( }
Core::ICore::dialogParent(),
tr("Token Limit Exceeded"),
tr("The chat history has exceeded the token limit.\n"
"Would you like to create new chat?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) { void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) const
autosave(); {
m_chatModel->clear(); m_clientInterface->sendMessage(message, sharingCurrentFile);
setRecentFilePath(QString{});
return;
}
}
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
clearAttachmentFiles();
} }
void ChatRootView::copyToClipboard(const QString &text) void ChatRootView::copyToClipboard(const QString &text)
@@ -143,41 +72,47 @@ void ChatRootView::cancelRequest()
m_clientInterface->cancelRequest(); m_clientInterface->cancelRequest();
} }
void ChatRootView::clearAttachmentFiles() void ChatRootView::generateColors()
{ {
if (!m_attachmentFiles.isEmpty()) { QColor baseColor = backgroundColor();
m_attachmentFiles.clear(); bool isDarkTheme = baseColor.lightness() < 128;
emit attachmentFilesChanged();
}
}
void ChatRootView::clearLinkedFiles() if (isDarkTheme) {
{ m_primaryColor = generateColor(baseColor, 0.1, 1.2, 1.4);
if (!m_linkedFiles.isEmpty()) { m_secondaryColor = generateColor(baseColor, -0.1, 1.1, 1.2);
m_linkedFiles.clear(); m_codeColor = generateColor(baseColor, 0.05, 0.8, 1.1);
emit linkedFilesChanged();
}
}
QString ChatRootView::getChatsHistoryDir() const
{
QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString();
} else { } else {
path = QString("%1/qodeassist/chat_history") m_primaryColor = generateColor(baseColor, 0.05, 1.05, 1.1);
.arg(Core::ICore::userResourcePath().toFSPathString()); m_secondaryColor = generateColor(baseColor, -0.05, 1.1, 1.2);
m_codeColor = generateColor(baseColor, 0.02, 0.95, 1.05);
}
}
QColor ChatRootView::generateColor(const QColor &baseColor,
float hueShift,
float saturationMod,
float lightnessMod)
{
float h, s, l, a;
baseColor.getHslF(&h, &s, &l, &a);
bool isDarkTheme = l < 0.5;
h = fmod(h + hueShift + 1.0, 1.0);
s = qBound(0.0f, s * saturationMod, 1.0f);
if (isDarkTheme) {
l = qBound(0.0f, l * lightnessMod, 1.0f);
} else {
l = qBound(0.0f, l / lightnessMod, 1.0f);
} }
QDir dir(path); h = qBound(0.0f, h, 1.0f);
if (!dir.exists() && !dir.mkpath(".")) { s = qBound(0.0f, s, 1.0f);
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path)); l = qBound(0.0f, l, 1.0f);
return QString(); a = qBound(0.0f, a, 1.0f);
}
return path; return QColor::fromHslF(h, s, l, a);
} }
QString ChatRootView::currentTemplate() const QString ChatRootView::currentTemplate() const
@@ -186,370 +121,24 @@ QString ChatRootView::currentTemplate() const
return settings.caModel(); return settings.caModel();
} }
void ChatRootView::saveHistory(const QString &filePath) QColor ChatRootView::primaryColor() const
{ {
auto result = ChatSerializer::saveToFile(m_chatModel, filePath); return m_primaryColor;
if (!result.success) {
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
} else {
setRecentFilePath(filePath);
}
} }
void ChatRootView::loadHistory(const QString &filePath) QColor ChatRootView::secondaryColor() const
{ {
auto result = ChatSerializer::loadFromFile(m_chatModel, filePath); return m_secondaryColor;
if (!result.success) {
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
} else {
setRecentFilePath(filePath);
}
updateInputTokensCount();
} }
void ChatRootView::showSaveDialog() QColor ChatRootView::codeColor() const
{ {
QString initialDir = getChatsHistoryDir(); return m_codeColor;
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
dialog->setAcceptMode(QFileDialog::AcceptSave);
dialog->setFileMode(QFileDialog::AnyFile);
dialog->setNameFilter(tr("JSON files (*.json)"));
dialog->setDefaultSuffix("json");
if (!initialDir.isEmpty()) {
dialog->setDirectory(initialDir);
dialog->selectFile(getSuggestedFileName() + ".json");
}
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
if (result == QFileDialog::Accepted) {
QStringList files = dialog->selectedFiles();
if (!files.isEmpty()) {
saveHistory(files.first());
}
}
dialog->deleteLater();
});
dialog->open();
} }
void ChatRootView::showLoadDialog() bool ChatRootView::isSharingCurrentFile() const
{ {
QString initialDir = getChatsHistoryDir(); return Settings::chatAssistantSettings().sharingCurrentFile();
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
dialog->setAcceptMode(QFileDialog::AcceptOpen);
dialog->setFileMode(QFileDialog::ExistingFile);
dialog->setNameFilter(tr("JSON files (*.json)"));
if (!initialDir.isEmpty()) {
dialog->setDirectory(initialDir);
}
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
if (result == QFileDialog::Accepted) {
QStringList files = dialog->selectedFiles();
if (!files.isEmpty()) {
loadHistory(files.first());
}
}
dialog->deleteLater();
});
dialog->open();
}
QString ChatRootView::getSuggestedFileName() const
{
QStringList parts;
static const QRegularExpression saitizeSymbols = QRegularExpression("[\\/:*?\"<>|\\s]");
static const QRegularExpression underSymbols = QRegularExpression("_+");
if (m_chatModel->rowCount() > 0) {
QString firstMessage
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
QString sanitizedMessage = shortMessage;
sanitizedMessage.replace(saitizeSymbols, "_");
sanitizedMessage.replace(underSymbols, "_");
sanitizedMessage = sanitizedMessage.trimmed();
if (!sanitizedMessage.isEmpty()) {
if (sanitizedMessage.startsWith('_')) {
sanitizedMessage.remove(0, 1);
}
if (sanitizedMessage.endsWith('_')) {
sanitizedMessage.chop(1);
}
QString targetDir = getChatsHistoryDir();
QString fullPath = QDir(targetDir).filePath(sanitizedMessage);
QFileInfo fileInfo(fullPath);
if (!fileInfo.exists() && QFileInfo(fileInfo.path()).isWritable()) {
parts << sanitizedMessage;
}
}
}
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
QString fileName = parts.join("_");
QString fullPath = QDir(getChatsHistoryDir()).filePath(fileName);
QFileInfo finalCheck(fullPath);
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
fileName = QString("chat_%1").arg(
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
}
return fileName;
}
void ChatRootView::autosave()
{
if (m_chatModel->rowCount() == 0 || !Settings::chatAssistantSettings().autosave()) {
return;
}
QString filePath = getAutosaveFilePath();
if (!filePath.isEmpty()) {
ChatSerializer::saveToFile(m_chatModel, filePath);
setRecentFilePath(filePath);
}
}
QString ChatRootView::getAutosaveFilePath() const
{
if (!m_recentFilePath.isEmpty()) {
return m_recentFilePath;
}
QString dir = getChatsHistoryDir();
if (dir.isEmpty()) {
return QString();
}
return QDir(dir).filePath(getSuggestedFileName() + ".json");
}
QStringList ChatRootView::attachmentFiles() const
{
return m_attachmentFiles;
}
QStringList ChatRootView::linkedFiles() const
{
return m_linkedFiles;
}
void ChatRootView::showAttachFilesDialog()
{
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toFSPathString());
}
if (dialog.exec() == QDialog::Accepted) {
QStringList newFilePaths = dialog.selectedFiles();
if (!newFilePaths.isEmpty()) {
bool filesAdded = false;
for (const QString &filePath : std::as_const(newFilePaths)) {
if (!m_attachmentFiles.contains(filePath)) {
m_attachmentFiles.append(filePath);
filesAdded = true;
}
}
if (filesAdded) {
emit attachmentFilesChanged();
}
}
}
}
void ChatRootView::removeFileFromAttachList(int index)
{
if (index >= 0 && index < m_attachmentFiles.size()) {
m_attachmentFiles.removeAt(index);
emit attachmentFilesChanged();
}
}
void ChatRootView::showLinkFilesDialog()
{
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toFSPathString());
}
if (dialog.exec() == QDialog::Accepted) {
QStringList newFilePaths = dialog.selectedFiles();
if (!newFilePaths.isEmpty()) {
bool filesAdded = false;
for (const QString &filePath : std::as_const(newFilePaths)) {
if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath);
filesAdded = true;
}
}
if (filesAdded) {
emit linkedFilesChanged();
}
}
}
}
void ChatRootView::removeFileFromLinkList(int index)
{
if (index >= 0 && index < m_linkedFiles.size()) {
m_linkedFiles.removeAt(index);
emit linkedFilesChanged();
}
}
void ChatRootView::calculateMessageTokensCount(const QString &message)
{
m_messageTokensCount = Context::TokenUtils::estimateTokens(message);
updateInputTokensCount();
}
void ChatRootView::setIsSyncOpenFiles(bool state)
{
if (m_isSyncOpenFiles != state) {
m_isSyncOpenFiles = state;
emit isSyncOpenFilesChanged();
}
if (m_isSyncOpenFiles) {
for (auto editor : std::as_const(m_currentEditors)) {
onAppendLinkFileFromEditor(editor);
}
}
}
void ChatRootView::openChatHistoryFolder()
{
QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString();
} else {
path = QString("%1/qodeassist/chat_history")
.arg(Core::ICore::userResourcePath().toFSPathString());
}
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
void ChatRootView::updateInputTokensCount()
{
int inputTokens = m_messageTokensCount;
auto &settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
}
if (!m_attachmentFiles.isEmpty()) {
auto attachFiles = m_clientInterface->contextManager()->getContentFiles(m_attachmentFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
}
if (!m_linkedFiles.isEmpty()) {
auto linkFiles = m_clientInterface->contextManager()->getContentFiles(m_linkedFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
}
const auto &history = m_chatModel->getChatHistory();
for (const auto &message : history) {
inputTokens += Context::TokenUtils::estimateTokens(message.content);
inputTokens += 4; // + role
}
m_inputTokensCount = inputTokens;
emit inputTokensCountChanged();
}
int ChatRootView::inputTokensCount() const
{
return m_inputTokensCount;
}
bool ChatRootView::isSyncOpenFiles() const
{
return m_isSyncOpenFiles;
}
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString();
m_linkedFiles.removeOne(filePath);
emit linkedFilesChanged();
}
if (editor) {
m_currentEditors.removeOne(editor);
}
}
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString();
if (!m_linkedFiles.contains(filePath) && !shouldIgnoreFileForAttach(document->filePath())) {
m_linkedFiles.append(filePath);
emit linkedFilesChanged();
}
}
}
void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath)
{
if (editor && editor->document()) {
m_currentEditors.append(editor);
}
}
QString ChatRootView::chatFileName() const
{
return QFileInfo(m_recentFilePath).baseName();
}
void ChatRootView::setRecentFilePath(const QString &filePath)
{
if (m_recentFilePath != filePath) {
m_recentFilePath = filePath;
emit chatFileNameChanged();
}
}
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
{
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString()));
return true;
}
return false;
} }
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,22 +23,24 @@
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "llmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatRootView : public QQuickItem class ChatRootView : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
// Possibly Qt bug: QTBUG-131004
// The class type name must be fully qualified
// including the namespace.
// Otherwise qmlls can't find it.
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL) Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL) Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL) Q_PROPERTY(QColor backgroundColor READ backgroundColor CONSTANT FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL) Q_PROPERTY(QColor primaryColor READ primaryColor CONSTANT FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL) Q_PROPERTY(QColor secondaryColor READ secondaryColor CONSTANT FINAL)
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL) Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL)
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL) Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY
isSharingCurrentFileChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
@@ -47,70 +49,38 @@ public:
ChatModel *chatModel() const; ChatModel *chatModel() const;
QString currentTemplate() const; QString currentTemplate() const;
void saveHistory(const QString &filePath); QColor backgroundColor() const;
void loadHistory(const QString &filePath); QColor primaryColor() const;
QColor secondaryColor() const;
Q_INVOKABLE void showSaveDialog(); QColor codeColor() const;
Q_INVOKABLE void showLoadDialog();
void autosave(); bool isSharingCurrentFile() const;
QString getAutosaveFilePath() const;
QStringList attachmentFiles() const;
QStringList linkedFiles() const;
Q_INVOKABLE void showAttachFilesDialog();
Q_INVOKABLE void removeFileFromAttachList(int index);
Q_INVOKABLE void showLinkFilesDialog();
Q_INVOKABLE void removeFileFromLinkList(int index);
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const;
bool isSyncOpenFiles() const;
void onEditorAboutToClose(Core::IEditor *editor);
void onAppendLinkFileFromEditor(Core::IEditor *editor);
void onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath);
QString chatFileName() const;
void setRecentFilePath(const QString &filePath);
bool shouldIgnoreFileForAttach(const Utils::FilePath &filePath);
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message, bool sharingCurrentFile = false) const;
void copyToClipboard(const QString &text); void copyToClipboard(const QString &text);
void cancelRequest(); void cancelRequest();
void clearAttachmentFiles();
void clearLinkedFiles();
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged(); void currentTemplateChanged();
void attachmentFilesChanged();
void linkedFilesChanged(); void isSharingCurrentFileChanged();
void inputTokensCountChanged();
void isSyncOpenFilesChanged();
void chatFileNameChanged();
private: private:
QString getChatsHistoryDir() const; void generateColors();
QString getSuggestedFileName() const; QColor generateColor(const QColor &baseColor,
float hueShift,
float saturationMod,
float lightnessMod);
ChatModel *m_chatModel; ChatModel *m_chatModel;
LLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
QString m_currentTemplate; QString m_currentTemplate;
QString m_recentFilePath; QColor m_primaryColor;
QStringList m_attachmentFiles; QColor m_secondaryColor;
QStringList m_linkedFiles; QColor m_codeColor;
int m_messageTokensCount{0};
int m_inputTokensCount{0};
bool m_isSyncOpenFiles;
QList<Core::IEditor *> m_currentEditors;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,142 +0,0 @@
/*
* 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 "ChatSerializer.hpp"
#include "Logger.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
namespace QodeAssist::Chat {
const QString ChatSerializer::VERSION = "0.1";
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
{
if (!ensureDirectoryExists(filePath)) {
return {false, "Failed to create directory structure"};
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
}
QJsonObject root = serializeChat(model);
QJsonDocument doc(root);
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
}
return {true, QString()};
}
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
return {false, QString("JSON parse error: %1").arg(error.errorString())};
}
QJsonObject root = doc.object();
QString version = root["version"].toString();
if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)};
}
if (!deserializeChat(model, root)) {
return {false, "Failed to deserialize chat data"};
}
return {true, QString()};
}
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
return messageObj;
}
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString();
message.id = json["id"].toString();
return message;
}
QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
{
QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) {
messagesArray.append(serializeMessage(message));
}
QJsonObject root;
root["version"] = VERSION;
root["messages"] = messagesArray;
return root;
}
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
{
QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
for (const auto &messageValue : messagesArray) {
messages.append(deserializeMessage(messageValue.toObject()));
}
model->clear();
for (const auto &message : messages) {
model->addMessage(message.content, message.role, message.id);
}
return true;
}
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
{
QFileInfo fileInfo(filePath);
QDir dir = fileInfo.dir();
return dir.exists() || dir.mkpath(".");
}
bool ChatSerializer::validateVersion(const QString &version)
{
return version == VERSION;
}
} // namespace QodeAssist::Chat

View File

@@ -1,56 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "ChatModel.hpp"
namespace QodeAssist::Chat {
struct SerializationResult
{
bool success{false};
QString errorMessage;
};
class ChatSerializer
{
public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
// Public for testing purposes
static QJsonObject serializeMessage(const ChatModel::Message &message);
static ChatModel::Message deserializeMessage(const QJsonObject &json);
static QJsonObject serializeChat(const ChatModel *model);
static bool deserializeChat(ChatModel *model, const QJsonObject &json);
private:
static const QString VERSION;
static constexpr int CURRENT_VERSION = 1;
static bool ensureDirectoryExists(const QString &filePath);
static bool validateVersion(const QString &version);
};
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,6 +23,7 @@
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
// Q_NAMESPACE
class ChatUtils : public QObject class ChatUtils : public QObject
{ {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -40,4 +40,4 @@ void ChatWidget::scrollToBottom()
{ {
QMetaObject::invokeMethod(rootObject(), "scrollToBottom"); QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
} }
} // namespace QodeAssist::Chat }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -38,4 +38,4 @@ signals:
void clearPressed(); void clearPressed();
}; };
} // namespace QodeAssist::Chat }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,11 +19,11 @@
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <texteditor/textdocument.h>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <texteditor/textdocument.h>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h> #include <coreplugin/editormanager/ieditor.h>
@@ -35,46 +35,40 @@
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp" #include "ProvidersManager.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ClientInterface::ClientInterface( ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent) : QObject(parent)
, m_requestHandler(new LLMCore::RequestHandler(this)) , m_requestHandler(new LLMCore::RequestHandler(this))
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, m_contextManager(new Context::ContextManager(this))
{ {
connect( connect(m_requestHandler,
m_requestHandler, &LLMCore::RequestHandler::completionReceived,
&LLMCore::RequestHandler::completionReceived, this,
this, [this](const QString &completion, const QJsonObject &request, bool isComplete) {
[this](const QString &completion, const QJsonObject &request, bool isComplete) { handleLLMResponse(completion, request, isComplete);
handleLLMResponse(completion, request, isComplete); });
});
connect( connect(m_requestHandler,
m_requestHandler, &LLMCore::RequestHandler::requestFinished,
&LLMCore::RequestHandler::requestFinished, this,
this, [this](const QString &, bool success, const QString &errorString) {
[this](const QString &, bool success, const QString &errorString) { if (!success) {
if (!success) { emit errorOccurred(errorString);
emit errorOccurred(errorString); }
} });
});
} }
ClientInterface::~ClientInterface() = default; ClientInterface::~ClientInterface() = default;
void ClientInterface::sendMessage( void ClientInterface::sendMessage(const QString &message, bool includeCurrentFile)
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
{ {
cancelRequest(); cancelRequest();
auto attachFiles = m_contextManager->getContentFiles(attachments); m_chatModel->addMessage(message, ChatModel::ChatRole::User, "");
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
auto &chatAssistantSettings = Settings::chatAssistantSettings(); auto &chatAssistantSettings = Settings::chatAssistantSettings();
@@ -87,7 +81,8 @@ void ClientInterface::sendMessage(
} }
auto templateName = Settings::generalSettings().caTemplate(); auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
if (!promptTemplate) { if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
@@ -95,47 +90,47 @@ void ClientInterface::sendMessage(
} }
LLMCore::ContextData context; LLMCore::ContextData context;
context.prefix = message;
context.suffix = "";
if (chatAssistantSettings.useSystemPrompt()) { QString systemPrompt;
QString systemPrompt = chatAssistantSettings.systemPrompt(); if (chatAssistantSettings.useSystemPrompt())
if (!linkedFiles.isEmpty()) { systemPrompt = chatAssistantSettings.systemPrompt();
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
if (includeCurrentFile) {
QString fileContext = getCurrentFileContext();
if (!fileContext.isEmpty()) {
systemPrompt = systemPrompt.append(fileContext);
} }
context.systemPrompt = systemPrompt;
} }
QVector<LLMCore::Message> messages; QJsonObject providerRequest;
for (const auto &msg : m_chatModel->getChatHistory()) { providerRequest["model"] = Settings::generalSettings().caModel();
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content}); providerRequest["stream"] = true;
} providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
context.history = messages;
if (promptTemplate)
promptTemplate->prepareRequest(providerRequest, context);
else
qWarning("No prompt template found");
if (provider)
provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
else
qWarning("No provider found");
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat; config.requestType = LLMCore::RequestType::Chat;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"} config.providerRequest = providerRequest;
: QString{"generateContent?"}; config.multiLineCompletion = false;
config.url = QUrl(QString("%1/models/%2:%3") config.apiKey = Settings::chatAssistantSettings().apiKey();
.arg(
Settings::generalSettings().caUrl(),
Settings::generalSettings().caModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().caModel()},
{"stream", chatAssistantSettings.stream()}};
}
config.apiKey = provider->apiKey(); QJsonObject request;
request["id"] = QUuid::createUuid().toString();
config.provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QJsonObject request{{"id", QUuid::createUuid().toString()}};
m_requestHandler->sendLLMRequest(config, request); m_requestHandler->sendLLMRequest(config, request);
} }
@@ -151,8 +146,9 @@ void ClientInterface::cancelRequest()
m_requestHandler->cancelRequest(id); m_requestHandler->cancelRequest(id);
} }
void ClientInterface::handleLLMResponse( void ClientInterface::handleLLMResponse(const QString &response,
const QString &response, const QJsonObject &request, bool isComplete) const QJsonObject &request,
bool isComplete)
{ {
const auto message = response.trimmed(); const auto message = response.trimmed();
@@ -163,7 +159,6 @@ void ClientInterface::handleLLMResponse(
if (isComplete) { if (isComplete) {
LOG_MESSAGE( LOG_MESSAGE(
"Message completed. Final response for message " + messageId + ": " + response); "Message completed. Final response for message " + messageId + ": " + response);
emit messageReceivedCompletely();
} }
} }
} }
@@ -183,35 +178,13 @@ QString ClientInterface::getCurrentFileContext() const
} }
QString fileInfo = QString("Language: %1\nFile: %2\n\n") QString fileInfo = QString("Language: %1\nFile: %2\n\n")
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString()); .arg(textDocument->mimeType(), textDocument->filePath().toString());
QString content = textDocument->document()->toPlainText(); QString content = textDocument->document()->toPlainText();
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toFSPathString())); LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toString()));
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content); return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
} }
QString ClientInterface::getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
}
return updatedPrompt;
}
Context::ContextManager *ClientInterface::contextManager() const
{
return m_contextManager;
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -25,8 +25,6 @@
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "RequestHandler.hpp" #include "RequestHandler.hpp"
#include "llmcore/IPromptProvider.hpp"
#include <context/ContextManager.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -35,33 +33,22 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void sendMessage( void sendMessage(const QString &message, bool includeCurrentFile = false);
const QString &message,
const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {});
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
Context::ContextManager *contextManager() const;
signals: signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely();
private: private:
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
QString getCurrentFileContext() const; QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel;
LLMCore::RequestHandler *m_requestHandler; LLMCore::RequestHandler *m_requestHandler;
Context::ContextManager *m_contextManager; ChatModel *m_chatModel;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,11 +0,0 @@
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_37_14)">
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_37_14">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

View File

@@ -1,11 +0,0 @@
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51_20)">
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_51_20">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

View File

@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_41_14)">
<path d="M0 0L24 24M0 24L24 0" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_41_14">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 353 B

View File

@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_41_14)">
<path d="M0 0L24 24M0 24L24 0" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_41_14">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 353 B

View File

@@ -1,12 +0,0 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_49_24)">
<path d="M10 12L10 32L10 12Z" fill="black"/>
<path d="M10 12L10 32" stroke="black" stroke-width="3"/>
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="black" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_49_24">
<rect width="20" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 513 B

View File

@@ -1,12 +0,0 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51_24)">
<path d="M10 12L10 32Z" fill="white"/>
<path d="M10 12L10 32" stroke="white" stroke-width="3"/>
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="white" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_51_24">
<rect width="20" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 507 B

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,18 +23,18 @@ Rectangle {
id: root id: root
property alias text: badgeText.text property alias text: badgeText.text
property alias fontColor: badgeText.color
implicitWidth: badgeText.implicitWidth + root.radius width: badgeText.implicitWidth + radius
implicitHeight: badgeText.implicitHeight + 6 height: badgeText.implicitHeight + 6
color: palette.button color: "lightgreen"
radius: root.height / 2 radius: height / 2
border.color: palette.mid
border.width: 1 border.width: 1
border.color: "gray"
Text { Text {
id: badgeText id: badgeText
anchors.centerIn: parent anchors.centerIn: parent
color: palette.buttonText
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -17,36 +17,28 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/ */
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import ChatView import ChatView
import QtQuick.Layouts
import "./dialog" import "./dialog"
Rectangle { Rectangle {
id: root id: root
property alias msgModel: msgCreator.model property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model property color fontColor
property bool isUserMessage: false property color codeBgColor
property int messageIndex: -1 property color selectionColor
signal resetChatToMessage(int index) height: msgColumn.height
height: msgColumn.implicitHeight + 10
radius: 8 radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
HoverHandler { Column {
id: mouse
}
ColumnLayout {
id: msgColumn id: msgColumn
x: 5
width: parent.width - x
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width
spacing: 5 spacing: 5
Repeater { Repeater {
@@ -57,7 +49,7 @@ Rectangle {
// why does `required property MessagePart modelData` not work? // why does `required property MessagePart modelData` not work?
required property var modelData required property var modelData
Layout.preferredWidth: root.width width: parent.width
sourceComponent: { sourceComponent: {
// If `required property MessagePart modelData` is used // If `required property MessagePart modelData` is used
// and conversion to MessagePart fails, you're left // and conversion to MessagePart fails, you're left
@@ -88,67 +80,6 @@ Rectangle {
} }
} }
} }
Flow {
id: attachmentsFlow
Layout.fillWidth: true
visible: attachmentsModel.model && attachmentsModel.model.length > 0
leftPadding: 10
rightPadding: 10
spacing: 5
Repeater {
id: attachmentsModel
delegate: Rectangle {
required property int index
required property var modelData
height: attachText.implicitHeight + 8
width: attachText.implicitWidth + 16
radius: 4
color: palette.button
border.width: 1
border.color: palette.mid
Text {
id: attachText
anchors.centerIn: parent
text: modelData
color: palette.text
}
}
}
}
}
Rectangle {
id: userMessageMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: "#92BD6C"
radius: root.radius
visible: root.isUserMessage
}
QoAButton {
id: stopButtonId
anchors {
right: parent.right
bottom: parent.bottom
bottomMargin: 2
}
text: qsTr("ResetTo")
visible: root.isUserMessage && mouse.hovered
onClicked: function() {
root.resetChatToMessage(root.messageIndex)
}
} }
component TextComponent : TextBlock { component TextComponent : TextBlock {
@@ -157,6 +88,8 @@ Rectangle {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
leftPadding: 10 leftPadding: 10
text: itemData.text text: itemData.text
color: root.fontColor
selectionColor: root.selectionColor
} }
@@ -171,5 +104,8 @@ Rectangle {
code: itemData.text code: itemData.text
language: itemData.language language: itemData.language
color: root.codeBgColor
selectionColor: root.selectionColor
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -17,66 +17,28 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/ */
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.Basic as QQC import QtQuick.Controls.Basic as QQC
import QtQuick.Layouts import QtQuick.Layouts
import ChatView import ChatView
import "./controls"
import "./parts"
ChatRootView { ChatRootView {
id: root id: root
property SystemPalette sysPalette: SystemPalette {
colorGroup: SystemPalette.Active
}
palette {
window: sysPalette.window
windowText: sysPalette.windowText
base: sysPalette.base
alternateBase: sysPalette.alternateBase
text: sysPalette.text
button: sysPalette.button
buttonText: sysPalette.buttonText
highlight: sysPalette.highlight
highlightedText: sysPalette.highlightedText
light: sysPalette.light
mid: sysPalette.mid
dark: sysPalette.dark
shadow: sysPalette.shadow
brightText: sysPalette.brightText
}
Rectangle { Rectangle {
id: bg id: bg
anchors.fill: parent anchors.fill: parent
color: palette.window color: root.backgroundColor
} }
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors {
spacing: 0 fill: parent
TopBar {
id: topBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat()
tokensBadge {
text: qsTr("tokens:%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
}
recentPath {
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
}
openChatHistory.onClicked: root.openChatHistoryFolder()
} }
spacing: 10
ListView { ListView {
id: chatListView id: chatListView
@@ -92,19 +54,14 @@ ChatRootView {
delegate: ChatItem { delegate: ChatItem {
required property var model required property var model
required property int index
width: ListView.view.width - scroll.width width: ListView.view.width - scroll.width
msgModel: root.chatModel.processMessageContent(model.content) msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor
isUserMessage: model.roleType === ChatModel.User fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
messageIndex: index codeBgColor: root.codeColor
selectionColor: root.primaryColor.hslLightness > 0.5 ? Qt.darker(root.primaryColor, 1.5)
: Qt.lighter(root.primaryColor, 1.5)
onResetChatToMessage: function(index) {
messageInput.text = model.content
messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(index)
}
} }
header: Item { header: Item {
@@ -112,7 +69,7 @@ ChatRootView {
height: 30 height: 30
} }
ScrollBar.vertical: QQC.ScrollBar { ScrollBar.vertical: ScrollBar {
id: scroll id: scroll
} }
@@ -138,28 +95,15 @@ ChatRootView {
id: messageInput id: messageInput
placeholderText: qsTr("Type your message here...") placeholderText: qsTr("Type your message here...")
placeholderTextColor: palette.mid placeholderTextColor: "#888"
color: palette.text color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
background: Rectangle { background: Rectangle {
radius: 2 radius: 2
color: palette.base color: root.primaryColor
border.color: messageInput.activeFocus ? palette.highlight : palette.button border.color: root.primaryColor.hslLightness > 0.5 ? Qt.lighter(root.primaryColor, 1.5)
: Qt.darker(root.primaryColor, 1.5)
border.width: 1 border.width: 1
Behavior on border.color {
ColorAnimation { duration: 150 }
}
Rectangle {
anchors.fill: parent
color: palette.highlight
opacity: messageInput.hovered ? 0.1 : 0
radius: parent.radius
}
} }
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
Keys.onPressed: function(event) { Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) { if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
root.sendChatMessage() root.sendChatMessage()
@@ -169,49 +113,65 @@ ChatRootView {
} }
} }
AttachedFilesPlace { RowLayout {
id: attachedFilesPlace
Layout.fillWidth: true Layout.fillWidth: true
attachedFilesModel: root.attachmentFiles spacing: 5
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg"
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
}
AttachedFilesPlace { Button {
id: linkedFilesPlace id: sendButton
Layout.fillWidth: true Layout.alignment: Qt.AlignBottom
attachedFilesModel: root.linkedFiles text: qsTr("Send")
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg" onClicked: root.sendChatMessage()
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
}
BottomBar {
id: bottomBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
sendButton.onClicked: root.sendChatMessage()
stopButton.onClicked: root.cancelRequest()
syncOpenFiles {
checked: root.isSyncOpenFiles
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
} }
attachFiles.onClicked: root.showAttachFilesDialog()
linkFiles.onClicked: root.showLinkFilesDialog() Button {
id: stopButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Stop")
onClicked: root.cancelRequest()
}
Button {
id: clearButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Clear Chat")
onClicked: root.clearChat()
}
CheckBox {
id: sharingCurrentFile
text: "Share current file with models"
checked: root.isSharingCurrentFile
}
}
}
Row {
id: bar
layoutDirection: Qt.RightToLeft
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: scroll.width
}
spacing: 10
Badge {
text: qsTr("tokens:%1/%2").arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
color: root.codeColor
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
} }
} }
function clearChat() { function clearChat() {
root.chatModel.clear() root.chatModel.clear()
root.clearAttachmentFiles()
root.updateInputTokensCount()
} }
function scrollToBottom() { function scrollToBottom() {
@@ -219,7 +179,7 @@ ChatRootView {
} }
function sendChatMessage() { function sendChatMessage() {
root.sendMessage(messageInput.text) root.sendMessage(messageInput.text, sharingCurrentFile.checked)
messageInput.text = "" messageInput.text = ""
scrollToBottom() scrollToBottom()
} }

View File

@@ -1,54 +0,0 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Controls.Basic
Button {
id: control
padding: 4
icon.width: 16
icon.height: 16
contentItem.height: 20
background: Rectangle {
id: bg
implicitHeight: 20
color: !control.enabled || !control.down ? control.palette.button : control.palette.dark
border.color: !control.enabled || (!control.hovered && !control.visualFocus) ? control.palette.mid : control.palette.highlight
border.width: 1
radius: 4
Rectangle {
anchors.fill: bg
radius: bg.radius
gradient: Gradient {
GradientStop { position: 0.0; color: Qt.alpha(control.palette.highlight, 0.4) }
GradientStop { position: 1.0; color: Qt.alpha(control.palette.highlight, 0.2) }
}
opacity: control.hovered ? 0.3 : 0.01
Behavior on opacity {NumberAnimation{duration: 250}}
}
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -26,6 +26,7 @@ Rectangle {
property string code: "" property string code: ""
property string language: "" property string language: ""
property color selectionColor
readonly property string monospaceFont: { readonly property string monospaceFont: {
switch (Qt.platform.os) { switch (Qt.platform.os) {
@@ -40,7 +41,6 @@ Rectangle {
} }
} }
color: palette.alternateBase
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3) border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
: Qt.lighter(root.color, 1.3) : Qt.lighter(root.color, 1.3)
border.width: 2 border.width: 2
@@ -62,10 +62,10 @@ Rectangle {
readOnly: true readOnly: true
selectByMouse: true selectByMouse: true
font.family: root.monospaceFont font.family: root.monospaceFont
font.pointSize: Qt.application.font.pointSize font.pointSize: 12
color: parent.color.hslLightness > 0.5 ? "black" : "white" color: parent.color.hslLightness > 0.5 ? "black" : "white"
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
selectionColor: palette.highlight selectionColor: root.selectionColor
} }
TextEdit { TextEdit {
@@ -80,7 +80,7 @@ Rectangle {
font.pointSize: 8 font.pointSize: 8
} }
QoAButton { Button {
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
anchors.margins: 5 anchors.margins: 5

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -26,6 +26,4 @@ TextEdit {
selectByMouse: true selectByMouse: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
textFormat: Text.StyledText textFormat: Text.StyledText
selectionColor: palette.highlight
color: palette.text
} }

View File

@@ -1,109 +0,0 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
Flow {
id: root
property alias attachedFilesModel: attachRepeater.model
property color accentColor: palette.mid
property string iconPath
signal removeFileFromListByIndex(index: int)
spacing: 5
leftPadding: 5
rightPadding: 5
topPadding: attachRepeater.model.length > 0 ? 2 : 0
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
Repeater {
id: attachRepeater
delegate: Rectangle {
required property int index
required property string modelData
height: 30
width: contentRow.width + 10
radius: 4
color: palette.button
border.width: 1
border.color: mouse.hovered ? palette.highlight : root.accentColor
HoverHandler {
id: mouse
}
Row {
id: contentRow
spacing: 5
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
Image {
id: icon
anchors.verticalCenter: parent.verticalCenter
source: root.iconPath
sourceSize.width: 8
sourceSize.height: 15
}
Text {
id: fileNameText
anchors.verticalCenter: parent.verticalCenter
color: palette.buttonText
text: {
const parts = modelData.split('/');
return parts[parts.length - 1];
}
}
MouseArea {
id: closeButton
anchors.verticalCenter: parent.verticalCenter
width: closeIcon.width + 5
height: closeButton.width + 5
onClicked: root.removeFileFromListByIndex(index)
Image {
id: closeIcon
anchors.centerIn: parent
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
: "qrc:/qt/qml/ChatView/icons/close-light.svg"
width: 6
height: 6
}
}
}
}
}
}

View File

@@ -1,98 +0,0 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
Rectangle {
id: root
property alias sendButton: sendButtonId
property alias stopButton: stopButtonId
property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId
property alias linkFiles: linkFilesId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
id: bottomBar
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 10
QoAButton {
id: sendButtonId
text: qsTr("Send")
}
QoAButton {
id: stopButtonId
text: qsTr("Stop")
}
QoAButton {
id: attachFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
height: 15
width: 8
}
text: qsTr("Attach files")
}
QoAButton {
id: linkFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
height: 15
width: 8
}
text: qsTr("Link files")
}
CheckBox {
id: syncOpenFilesId
text: qsTr("Sync open files")
ToolTip.visible: syncOpenFilesId.hovered
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
}
Item {
Layout.fillWidth: true
}
}
}

View File

@@ -1,88 +0,0 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Layouts
import ChatView
Rectangle {
id: root
property alias saveButton: saveButtonId
property alias loadButton: loadButtonId
property alias clearButton: clearButtonId
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 10
QoAButton {
id: saveButtonId
text: qsTr("Save")
}
QoAButton {
id: loadButtonId
text: qsTr("Load")
}
QoAButton {
id: clearButtonId
text: qsTr("Clear")
}
Text {
id: recentPathId
elide: Text.ElideMiddle
color: palette.text
}
QoAButton {
id: openChatHistoryId
text: qsTr("Show in system")
}
Item {
Layout.fillWidth: true
}
Badge {
id: tokensBadgeId
}
}
}

View File

@@ -1,234 +0,0 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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 "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp>
#include <QFileInfo>
#include <QHash>
namespace QodeAssist {
struct LanguageProperties
{
QString name;
QString commentStyle;
QVector<QString> namesFromModel;
QVector<QString> fileExtensions;
};
const QVector<LanguageProperties> customLanguagesFromSettings()
{
QVector<LanguageProperties> customLanguages;
const QStringList customLanguagesList = Settings::codeCompletionSettings().customLanguages();
for (const QString &entry : customLanguagesList) {
if (entry.trimmed().isEmpty()) {
continue;
}
QStringList parts = entry.split(',');
if (parts.size() < 4) {
continue;
}
QString name = parts[0].trimmed();
QString commentStyle = parts[1].trimmed();
QStringList modelNamesList = parts[2].trimmed().split(' ', Qt::SkipEmptyParts);
QStringList extensionsList = parts[3].trimmed().split(' ', Qt::SkipEmptyParts);
if (!name.isEmpty() && !commentStyle.isEmpty() && !modelNamesList.isEmpty()
&& !extensionsList.isEmpty()) {
QVector<QString> modelNames;
for (const auto &modelName : modelNamesList) {
modelNames.append(modelName);
}
QVector<QString> extensions;
for (const auto &ext : extensionsList) {
extensions.append(ext);
}
customLanguages.append({name, commentStyle, modelNames, extensions});
}
}
return customLanguages;
}
const QVector<LanguageProperties> &getKnownLanguages()
{
static QVector<LanguageProperties> knownLanguages = {
{"python", "#", {"python", "py"}, {"py"}},
{"lua", "--", {"lua"}, {"lua"}},
{"js", "//", {"js", "javascript"}, {"js", "jsx"}},
{"ts", "//", {"ts", "typescript"}, {"ts", "tsx"}},
{"c-like", "//", {"c", "c++", "cpp"}, {"c", "h", "cpp", "hpp"}},
{"java", "//", {"java"}, {"java"}},
{"c#", "//", {"cs", "csharp"}, {"cs"}},
{"php", "//", {"php"}, {"php"}},
{"ruby", "#", {"rb", "ruby"}, {"rb"}},
{"go", "//", {"go"}, {"go"}},
{"swift", "//", {"swift"}, {"swift"}},
{"kotlin", "//", {"kt", "kotlin"}, {"kt", "kotlin"}},
{"scala", "//", {"scala"}, {"scala"}},
{"r", "#", {"r"}, {"r"}},
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
{"perl", "#", {"pl", "perl"}, {"pl"}},
{"hs", "--", {"hs", "haskell"}, {"hs"}},
{"qml", "//", {"qml"}, {"qml"}},
};
knownLanguages.append(customLanguagesFromSettings());
return knownLanguages;
}
static QHash<QString, QString> buildLanguageToCommentPrefixMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
result[languageProps.name] = languageProps.commentStyle;
}
return result;
}
static QHash<QString, QString> buildExtensionToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &extension : languageProps.fileExtensions) {
result[extension] = languageProps.name;
}
}
return result;
}
static QHash<QString, QString> buildModelLanguageNameToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &nameFromModel : languageProps.namesFromModel) {
result[nameFromModel] = languageProps.name;
}
}
return result;
}
QString CodeHandler::processText(QString text, QString currentFilePath)
{
QString result;
QStringList lines = text.split('\n');
bool inCodeBlock = false;
QString pendingComments;
auto currentFileExtension = QFileInfo(currentFilePath).suffix();
auto currentLanguage = detectLanguageFromExtension(currentFileExtension);
auto addPendingCommentsIfAny = [&]() {
if (pendingComments.isEmpty()) {
return;
}
QStringList commentLines = pendingComments.split('\n');
QString commentPrefix = getCommentPrefix(currentLanguage);
for (const QString &commentLine : commentLines) {
if (!commentLine.trimmed().isEmpty()) {
result += commentPrefix + " " + commentLine.trimmed() + "\n";
} else {
result += "\n";
}
}
pendingComments.clear();
};
for (const QString &line : lines) {
if (line.trimmed().startsWith("```")) {
if (!inCodeBlock) {
auto lineLanguage = detectLanguageFromLine(line);
if (!lineLanguage.isEmpty()) {
currentLanguage = lineLanguage;
}
addPendingCommentsIfAny();
if (lineLanguage.isEmpty()) {
// language not detected, so add direct output from model, if any
result += line.trimmed().mid(3) + "\n"; // add the remainder of line after ```
}
}
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
result += line + "\n";
} else {
QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
pendingComments += trimmed + "\n";
} else {
pendingComments += "\n";
}
}
}
addPendingCommentsIfAny();
return result;
}
QString CodeHandler::getCommentPrefix(const QString &language)
{
static const auto commentPrefixes = buildLanguageToCommentPrefixMap();
return commentPrefixes.value(language, "//");
}
QString CodeHandler::detectLanguageFromLine(const QString &line)
{
static const auto modelNameToLanguage = buildModelLanguageNameToLanguageMap();
return modelNameToLanguage.value(line.trimmed().mid(3).trimmed(), "");
}
QString CodeHandler::detectLanguageFromExtension(const QString &extension)
{
static const auto extensionToLanguage = buildExtensionToLanguageMap();
return extensionToLanguage.value(extension.toLower(), "");
}
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
{
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
} // namespace QodeAssist

View File

@@ -1,51 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include <QObject>
#include <QRegularExpression>
#include <QString>
namespace QodeAssist {
class CodeHandler
{
public:
static QString processText(QString text, QString currentFileName);
/**
* Detects language from line, or returns empty string if this was not possible
*/
static QString detectLanguageFromLine(const QString &line);
/**
* Detects language file name, or returns empty string if this was not possible
*/
static QString detectLanguageFromExtension(const QString &extension);
private:
static QString getCommentPrefix(const QString &language);
static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex();
static const QRegularExpression &getPartialEndBlockRegex();
};
} // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,8 +19,8 @@
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
#include <settings/ButtonAspect.hpp>
#include <QTimer> #include <QTimer>
#include <settings/ButtonAspect.hpp>
#include "QodeAssisttr.h" #include "QodeAssisttr.h"
@@ -35,49 +35,6 @@ ConfigurationManager &ConfigurationManager::instance()
void ConfigurationManager::init() void ConfigurationManager::init()
{ {
setupConnections(); setupConnections();
updateAllTemplateDescriptions();
checkAllTemplate();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
}
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
}
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplate.setValue(templ->name());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplate.setValue(templ->name());
}
}
void ConfigurationManager::checkAllTemplate()
{
checkTemplate(m_generalSettings.ccTemplate);
checkTemplate(m_generalSettings.caTemplate);
} }
ConfigurationManager::ConfigurationManager(QObject *parent) ConfigurationManager::ConfigurationManager(QObject *parent)
@@ -100,21 +57,6 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate); connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.ccTemplate);
});
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.caTemplate);
});
} }
void ConfigurationManager::selectProvider() void ConfigurationManager::selectProvider()
@@ -127,13 +69,13 @@ void ConfigurationManager::selectProvider()
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider) auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
? m_generalSettings.ccProvider ? m_generalSettings.ccProvider
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
? m_generalSettings.ccPreset1Provider
: m_generalSettings.caProvider; : m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] { QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
m_generalSettings.showSelectionDialog( m_generalSettings.showSelectionDialog(providersList,
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:")); targetSettings,
Tr::tr("Select LLM Provider"),
Tr::tr("Providers:"));
}); });
} }
@@ -144,19 +86,14 @@ void ConfigurationManager::selectModel()
return; return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue() const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue() : m_generalSettings.caProvider.volatileValue();
: m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue() const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: m_generalSettings.caUrl.volatileValue(); : m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel : m_generalSettings.caModel;
: isPreset1 ? m_generalSettings.ccPreset1Model
: m_generalSettings.caModel;
if (auto provider = m_providersManager.getProviderByName(providerName)) { if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->supportsModelListing()) { if (!provider->supportsModelListing()) {
@@ -185,23 +122,18 @@ void ConfigurationManager::selectTemplate()
return; return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1 const auto templateList = isCodeCompletion ? m_templateManger.fimTemplatesNames()
? m_templateManger.getFimTemplatesForProvider(providerID) : m_templateManger.chatTemplatesNames();
: m_templateManger.getChatTemplatesForProvider(providerID);
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template
: m_generalSettings.caTemplate; : m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
m_generalSettings.showSelectionDialog( m_generalSettings.showSelectionDialog(templateList,
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:")); targetSettings,
Tr::tr("Select Template"),
Tr::tr("Templates:"));
}); });
} }
@@ -218,9 +150,8 @@ void ConfigurationManager::selectUrl()
urls.append(url); urls.append(url);
} }
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
: settingsButton == &m_generalSettings.ccPreset1SetUrl ? m_generalSettings.ccUrl
? m_generalSettings.ccPreset1Url
: m_generalSettings.caUrl; : m_generalSettings.caUrl;
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -36,11 +36,6 @@ public:
void init(); void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots: public slots:
void selectProvider(); void selectProvider();
void selectModel(); void selectModel();

249
DocumentContextReader.cpp Normal file
View File

@@ -0,0 +1,249 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "DocumentContextReader.hpp"
#include <QFileInfo>
#include <QTextBlock>
#include <languageserverprotocol/lsptypes.h>
#include "core/ChangesManager.h"
#include "settings/CodeCompletionSettings.hpp"
const QRegularExpression &getYearRegex()
{
static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b");
return yearRegex;
}
const QRegularExpression &getNameRegex()
{
static const QRegularExpression nameRegex("\\b[A-Z][a-z.]+ [A-Z][a-z.]+\\b");
return nameRegex;
}
const QRegularExpression &getCommentRegex()
{
static const QRegularExpression
commentRegex(R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))",
QRegularExpression::MultilineOption);
return commentRegex;
}
namespace QodeAssist {
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
: m_textDocument(textDocument)
, m_document(textDocument->document())
{
m_copyrightInfo = findCopyright();
}
QString DocumentContextReader::getLineText(int lineNumber, int cursorPosition) const
{
if (!m_document || lineNumber < 0)
return QString();
QTextBlock block = m_document->begin();
int currentLine = 0;
while (block.isValid()) {
if (currentLine == lineNumber) {
QString text = block.text();
if (cursorPosition >= 0 && cursorPosition <= text.length()) {
text = text.left(cursorPosition);
}
return text;
}
block = block.next();
currentLine++;
}
return QString();
}
QString DocumentContextReader::getContextBefore(int lineNumber,
int cursorPosition,
int linesCount) const
{
int effectiveStartLine;
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - linesCount);
} else {
effectiveStartLine = qMax(0, lineNumber - linesCount);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
QString DocumentContextReader::getContextAfter(int lineNumber,
int cursorPosition,
int linesCount) const
{
int endLine = qMin(m_document->blockCount() - 1, lineNumber + linesCount);
return getContextBetween(lineNumber + 1, endLine, cursorPosition);
}
QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const
{
int startLine = 0;
if (m_copyrightInfo.found) {
startLine = m_copyrightInfo.endLine + 1;
}
startLine = qMin(startLine, lineNumber);
QString result = getContextBetween(startLine, lineNumber, cursorPosition);
return result;
}
QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const
{
return getContextBetween(lineNumber, m_document->blockCount() - 1, cursorPosition);
}
QString DocumentContextReader::getLanguageAndFileInfo() const
{
if (!m_textDocument)
return QString();
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(
m_textDocument->mimeType());
QString mimeType = m_textDocument->mimeType();
QString filePath = m_textDocument->filePath().toString();
QString fileExtension = QFileInfo(filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, mimeType, filePath, fileExtension);
}
CopyrightInfo DocumentContextReader::findCopyright()
{
CopyrightInfo result = {-1, -1, false};
QString text = m_document->toPlainText();
QRegularExpressionMatchIterator matchIterator = getCommentRegex().globalMatch(text);
QList<CopyrightInfo> copyrightBlocks;
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured().toLower();
if (matchedText.contains("copyright") || matchedText.contains("(C)")
|| matchedText.contains("(c)") || matchedText.contains("©")
|| getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) {
int startPos = match.capturedStart();
int endPos = match.capturedEnd();
CopyrightInfo info;
info.startLine = m_document->findBlock(startPos).blockNumber();
info.endLine = m_document->findBlock(endPos).blockNumber();
info.found = true;
copyrightBlocks.append(info);
}
}
for (int i = 0; i < copyrightBlocks.size() - 1; ++i) {
if (copyrightBlocks[i].endLine + 1 >= copyrightBlocks[i + 1].startLine) {
copyrightBlocks[i].endLine = copyrightBlocks[i + 1].endLine;
copyrightBlocks.removeAt(i + 1);
--i;
}
}
if (!copyrightBlocks.isEmpty()) { // temproary solution, need cache
return copyrightBlocks.first();
}
return result;
}
QString DocumentContextReader::getContextBetween(int startLine,
int endLine,
int cursorPosition) const
{
QString context;
for (int i = startLine; i <= endLine; ++i) {
QTextBlock block = m_document->findBlockByNumber(i);
if (!block.isValid()) {
break;
}
if (i == endLine) {
context += block.text().left(cursorPosition);
} else {
context += block.text() + "\n";
}
}
return context;
}
CopyrightInfo DocumentContextReader::copyrightInfo() const
{
return m_copyrightInfo;
}
LLMCore::ContextData DocumentContextReader::prepareContext(int lineNumber, int cursorPosition) const
{
QString contextBefore = getContextBefore(lineNumber, cursorPosition);
QString contextAfter = getContextAfter(lineNumber, cursorPosition);
QString fileContext;
if (Settings::codeCompletionSettings().useFilePathInContext())
fileContext.append("\n ").append(getLanguageAndFileInfo());
if (Settings::codeCompletionSettings().useProjectChangesCache())
fileContext.append("\n ").append(
ChangesManager::instance().getRecentChangesContext(m_textDocument));
return {contextBefore, contextAfter, fileContext};
}
QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileBefore(lineNumber, cursorPosition);
} else {
int effectiveStartLine;
int beforeCursor = Settings::codeCompletionSettings().readStringsBeforeCursor();
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - beforeCursor);
} else {
effectiveStartLine = qMax(0, lineNumber - beforeCursor);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
}
QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileAfter(lineNumber, cursorPosition);
} else {
int endLine = qMin(m_document->blockCount() - 1,
lineNumber + Settings::codeCompletionSettings().readStringsAfterCursor());
return getContextBetween(lineNumber + 1, endLine, -1);
}
}
} // namespace QodeAssist

64
DocumentContextReader.hpp Normal file
View File

@@ -0,0 +1,64 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QTextDocument>
#include <texteditor/textdocument.h>
#include <llmcore/ContextData.hpp>
namespace QodeAssist {
struct CopyrightInfo
{
int startLine;
int endLine;
bool found;
};
class DocumentContextReader
{
public:
DocumentContextReader(TextEditor::TextDocument *textDocument);
QString getLineText(int lineNumber, int cursorPosition = -1) const;
QString getContextBefore(int lineNumber, int cursorPosition, int linesCount) const;
QString getContextAfter(int lineNumber, int cursorPosition, int linesCount) const;
QString readWholeFileBefore(int lineNumber, int cursorPosition) const;
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
QString getLanguageAndFileInfo() const;
CopyrightInfo findCopyright();
QString getContextBetween(int startLine, int endLine, int cursorPosition) const;
CopyrightInfo copyrightInfo() const;
LLMCore::ContextData prepareContext(int lineNumber, int cursorPosition) const;
private:
QString getContextBefore(int lineNumber, int cursorPosition) const;
QString getContextAfter(int lineNumber, int cursorPosition) const;
private:
TextEditor::TextDocument *m_textDocument;
QTextDocument *m_document;
CopyrightInfo m_copyrightInfo;
};
} // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,45 +23,30 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include "CodeHandler.hpp" #include <llmcore/RequestConfig.hpp>
#include "context/DocumentContextReader.hpp" #include <texteditor/textdocument.h>
#include "context/Utils.hpp"
#include "DocumentContextReader.hpp"
#include "llmcore/PromptTemplateManager.hpp" #include "llmcore/PromptTemplateManager.hpp"
#include "llmcore/ProvidersManager.hpp" #include "llmcore/ProvidersManager.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include <llmcore/RequestConfig.hpp>
namespace QodeAssist { namespace QodeAssist {
LLMClientInterface::LLMClientInterface( LLMClientInterface::LLMClientInterface()
const Settings::GeneralSettings &generalSettings, : m_requestHandler(this)
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
LLMCore::RequestHandlerBase &requestHandler,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_requestHandler(requestHandler)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this))
{ {
connect( connect(&m_requestHandler,
&m_requestHandler, &LLMCore::RequestHandler::completionReceived,
&LLMCore::RequestHandler::completionReceived, this,
this, &LLMClientInterface::sendCompletionToClient);
&LLMClientInterface::sendCompletionToClient);
} }
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{ {
return "QodeAssist"; return "Qode Assist";
} }
void LLMClientInterface::startImpl() void LLMClientInterface::startImpl()
@@ -88,7 +73,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
handleTextDocumentDidOpen(request); handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") { } else if (method == "getCompletionsCycling") {
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
m_performanceLogger.startTimeMeasurement(requestId); startTimeMeasurement(requestId);
handleCompletion(request); handleCompletion(request);
} else if (method == "$/cancelRequest") { } else if (method == "$/cancelRequest") {
handleCancelRequest(request); handleCancelRequest(request);
@@ -161,173 +146,98 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
void LLMClientInterface::handleCompletion(const QJsonObject &request) void LLMClientInterface::handleCompletion(const QJsonObject &request)
{ {
auto filePath = Context::extractFilePathFromRequest(request); auto updatedContext = prepareContext(request);
auto documentInfo = m_documentReader.readDocument(filePath); auto &completeSettings = Settings::codeCompletionSettings();
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available for" + filePath);
return;
}
auto updatedContext = prepareContext(request, documentInfo); auto providerName = Settings::generalSettings().ccProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) { if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
return; return;
} }
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() auto templateName = Settings::generalSettings().ccTemplate();
: m_generalSettings.ccPreset1Template(); auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) { if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
return; return;
} }
// TODO refactor to dynamic presets system
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::CodeCompletion; config.requestType = LLMCore::RequestType::Fim;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
// TODO refactor networking config.url = QUrl(
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"} config.apiKey = Settings::codeCompletionSettings().apiKey();
: QString{"generateContent?"};
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream)); config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, {"stream", true}};
} else {
config.url = QUrl(QString("%1%2").arg( config.multiLineCompletion = completeSettings.multiLineCompletion();
url,
promptTemplate->type() == LLMCore::TemplateType::FIM ? provider->completionEndpoint() QString systemPrompt;
: provider->chatEndpoint())); if (completeSettings.useSystemPrompt())
config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}}; systemPrompt.append(completeSettings.systemPrompt());
} if (!updatedContext.fileContext.isEmpty())
config.apiKey = provider->apiKey(); systemPrompt.append(updatedContext.fileContext);
config.multiLineCompletion = m_completeSettings.multiLineCompletion(); if (!systemPrompt.isEmpty())
config.providerRequest["system"] = systemPrompt;
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords()); const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty()) if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords; config.providerRequest["stop"] = stopWords;
QString systemPrompt; config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
if (m_completeSettings.useSystemPrompt()) config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == LLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
config.provider->prepareRequest(
config.providerRequest,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion);
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for fim request:");
LOG_MESSAGES(errors);
return;
}
m_requestHandler.sendLLMRequest(config, request); m_requestHandler.sendLLMRequest(config, request);
} }
LLMCore::ContextData LLMClientInterface::prepareContext( LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
const QJsonObject &request, const Context::DocumentInfo &documentInfo) const QStringView &accumulatedCompletion)
{ {
QJsonObject params = request["params"].toObject(); QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject(); QJsonObject doc = params["doc"].toObject();
QJsonObject position = doc["position"].toObject(); QJsonObject position = doc["position"].toObject();
QString uri = doc["uri"].toString();
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
filePath);
if (!textDocument) {
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
return LLMCore::ContextData{};
}
int cursorPosition = position["character"].toInt(); int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt(); int lineNumber = position["line"].toInt();
Context::DocumentContextReader DocumentContextReader reader(textDocument);
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath); return reader.prepareContext(lineNumber, cursorPosition);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
} }
Context::ContextManager *LLMClientInterface::contextManager() const void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QJsonObject &request,
bool isComplete)
{ {
return m_contextManager;
}
void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject(); QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response; QJsonObject response;
response["jsonrpc"] = "2.0"; response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"]; response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject result; QJsonObject result;
QJsonArray completions; QJsonArray completions;
QJsonObject completionItem; QJsonObject completionItem;
completionItem[LanguageServerProtocol::textKey] = completion;
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
QString processedCompletion
= promptTemplate->type() == LLMCore::TemplateType::Chat
&& m_completeSettings.smartProcessInstuctText()
? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request))
: completion;
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range; QJsonObject range;
range["start"] = position; range["start"] = position;
range["end"] = position; QJsonObject end = position;
end["character"] = position["character"].toInt() + completion.length();
range["end"] = end;
completionItem[LanguageServerProtocol::rangeKey] = range; completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position; completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem); completions.append(completionItem);
@@ -339,13 +249,37 @@ void LLMClientInterface::sendCompletionToClient(
QString("Completions: \n%1") QString("Completions: \n%1")
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
LOG_MESSAGE( LOG_MESSAGE(QString("Full response: \n%1")
QString("Full response: \n%1") .arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId); endTimeMeasurement(requestId);
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
} }
void LLMClientInterface::startTimeMeasurement(const QString &requestId)
{
m_requestStartTimes[requestId] = QDateTime::currentMSecsSinceEpoch();
}
void LLMClientInterface::endTimeMeasurement(const QString &requestId)
{
if (m_requestStartTimes.contains(requestId)) {
qint64 startTime = m_requestStartTimes[requestId];
qint64 endTime = QDateTime::currentMSecsSinceEpoch();
qint64 totalTime = endTime - startTime;
logPerformance(requestId, "TotalCompletionTime", totalTime);
m_requestStartTimes.remove(requestId);
}
}
void LLMClientInterface::logPerformance(const QString &requestId,
const QString &operation,
qint64 elapsedMs)
{
LOG_MESSAGE(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
}
void LLMClientInterface::parseCurrentMessage() {}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -22,16 +22,8 @@
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp> #include <llmcore/ContextData.hpp>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
#include <llmcore/RequestHandler.hpp> #include <llmcore/RequestHandler.hpp>
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
class QNetworkReply; class QNetworkReply;
class QNetworkAccessManager; class QNetworkAccessManager;
@@ -43,29 +35,20 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
Q_OBJECT Q_OBJECT
public: public:
LLMClientInterface( LLMClientInterface();
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
LLMCore::RequestHandlerBase &requestHandler,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
Utils::FilePath serverDeviceTemplate() const override; Utils::FilePath serverDeviceTemplate() const override;
void sendCompletionToClient( void sendCompletionToClient(const QString &completion,
const QString &completion, const QJsonObject &request, bool isComplete); const QJsonObject &request,
bool isComplete);
void handleCompletion(const QJsonObject &request); void handleCompletion(const QJsonObject &request);
// exposed for tests
void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const;
protected: protected:
void startImpl() override; void startImpl() override;
void sendData(const QByteArray &data) override;
void parseCurrentMessage() override;
private: private:
void handleInitialize(const QJsonObject &request); void handleInitialize(const QJsonObject &request);
@@ -75,18 +58,16 @@ private:
void handleExit(const QJsonObject &request); void handleExit(const QJsonObject &request);
void handleCancelRequest(const QJsonObject &request); void handleCancelRequest(const QJsonObject &request);
LLMCore::ContextData prepareContext( LLMCore::ContextData prepareContext(const QJsonObject &request,
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QStringView &accumulatedCompletion = QString{});
const Settings::CodeCompletionSettings &m_completeSettings; LLMCore::RequestHandler m_requestHandler;
const Settings::GeneralSettings &m_generalSettings;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
LLMCore::IProviderRegistry &m_providerRegistry;
LLMCore::RequestHandlerBase &m_requestHandler;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;
Context::ContextManager *m_contextManager; QMap<QString, qint64> m_requestStartTimes;
void startTimeMeasurement(const QString &requestId);
void endTimeMeasurement(const QString &requestId);
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,13 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
* *
* This file is part of QodeAssist. * 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 * QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@@ -23,25 +18,30 @@
*/ */
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include <QTextCursor>
#include <QtWidgets/qtoolbar.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <utils/stringutils.h> #include <utils/stringutils.h>
#include <utils/tooltip/tooltip.h> #include <utils/tooltip/tooltip.h>
namespace QodeAssist { namespace QodeAssist {
LLMSuggestion::LLMSuggestion( LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin)
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion) : m_completion(completion)
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion) , m_linesCount(0)
{ {
const auto &data = suggestions[currentCompletion]; int startPos = completion.range().start().toPositionInDocument(origin);
int endPos = completion.range().end().toPositionInDocument(origin);
int startPos = data.range.begin.toPositionInDocument(sourceDocument); startPos = qBound(0, startPos, origin->characterCount() - 1);
int endPos = data.range.end.toPositionInDocument(sourceDocument); endPos = qBound(startPos, endPos, origin->characterCount() - 1);
startPos = qBound(0, startPos, sourceDocument->characterCount()); m_start = QTextCursor(origin);
endPos = qBound(startPos, endPos, sourceDocument->characterCount()); m_start.setPosition(startPos);
m_start.setKeepPositionOnInsert(true);
QTextCursor cursor(sourceDocument); QTextCursor cursor(origin);
cursor.setPosition(startPos); cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor); cursor.setPosition(endPos, QTextCursor::KeepAnchor);
@@ -51,57 +51,74 @@ LLMSuggestion::LLMSuggestion(
int startPosInBlock = startPos - block.position(); int startPosInBlock = startPos - block.position();
int endPosInBlock = endPos - block.position(); int endPosInBlock = endPos - block.position();
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text); blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, completion.text());
replacementDocument()->setPlainText(blockText);
document()->setPlainText(blockText);
setCurrentPosition(m_start.position());
}
bool LLMSuggestion::apply()
{
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
cursor.beginEditBlock();
cursor.removeSelectedText();
cursor.insertText(m_completion.text());
cursor.endEditBlock();
return true;
} }
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
{ {
return applyPart(Word, widget); return applyNextLine(widget);
} }
bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyNextLine(TextEditor::TextEditorWidget *widget)
{ {
return applyPart(Line, 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();
} }
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget) void LLMSuggestion::onCounterFinished(int count)
{ {
const Utils::Text::Range range = suggestions()[currentSuggestion()].range; Utils::ToolTip::hide();
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument()); m_linesCount = 0;
QTextCursor currentCursor = widget->textCursor(); QTextCursor cursor = m_completion.range().toSelection(m_start.document());
const QString text = suggestions()[currentSuggestion()].text; cursor.beginEditBlock();
cursor.removeSelectedText();
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock() QStringList lines = m_completion.text().split('\n');
+ (cursor.selectionEnd() - cursor.selectionStart()); QString textToInsert = lines.mid(0, count).join('\n');
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos); cursor.insertText(textToInsert);
cursor.endEditBlock();
}
if (next == -1) void LLMSuggestion::reset()
return apply(); {
m_start.removeSelectedText();
}
if (part == Line) int LLMSuggestion::position()
++next; {
return m_start.position();
}
QString subText = text.mid(startPos, next - startPos); void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count)
if (subText.isEmpty()) {
return false; Utils::ToolTip::hide();
QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight());
currentCursor.insertText(subText); pos += QPoint(-10, -50);
m_counterTooltip = new CounterTooltip(count);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) { Utils::ToolTip::show(pos, m_counterTooltip, widget);
const QString newCompletionText = text.mid(startPos + seperatorPos + 1); connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished);
if (!newCompletionText.isEmpty()) {
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
const Utils::Text::Position
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
}
}
return false;
} }
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,13 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
* *
* This file is part of QodeAssist. * 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 * QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@@ -24,21 +19,37 @@
#pragma once #pragma once
#include <texteditor/texteditor.h> #include <QObject>
#include <texteditor/textsuggestion.h> #include "LSPCompletion.hpp"
#include <texteditor/textdocumentlayout.h>
#include "utils/CounterTooltip.hpp"
namespace QodeAssist { namespace QodeAssist {
class LLMSuggestion : public TextEditor::CyclicSuggestion class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion
{ {
Q_OBJECT
public: public:
enum Part { Word, Line }; LLMSuggestion(const Completion &completion, QTextDocument *origin);
LLMSuggestion( bool apply() final;
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0); bool applyWord(TextEditor::TextEditorWidget *widget) final;
bool applyNextLine(TextEditor::TextEditorWidget *widget);
void reset() final;
int position() final;
bool applyWord(TextEditor::TextEditorWidget *widget) override; const Completion &completion() const { return m_completion; }
bool applyLine(TextEditor::TextEditorWidget *widget) override;
bool applyPart(Part part, TextEditor::TextEditorWidget *widget); 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 } // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -68,10 +68,9 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
public: public:
static constexpr LanguageServerProtocol::Key docKey{"doc"}; static constexpr LanguageServerProtocol::Key docKey{"doc"};
GetCompletionParams( GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
const LanguageServerProtocol::TextDocumentIdentifier &document, int version,
int version, const LanguageServerProtocol::Position &position)
const LanguageServerProtocol::Position &position)
{ {
setTextDocument(document); setTextDocument(document);
setVersion(version); setVersion(version);

View File

@@ -1,13 +1,16 @@
{ {
"Id" : "qodeassist",
"Name" : "QodeAssist", "Name" : "QodeAssist",
"Version" : "0.5.9", "Version" : "0.3.10",
"CompatVersion" : "${IDE_VERSION_COMPAT}",
"Vendor" : "Petr Mironychev", "Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd", "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : "GPLv3", "License" : "GNU General Public License Usage
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
Alternatively, this file may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this file. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html.",
"Description" : ["QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code",
"Prerequisites:",
"- One of the supported LLM providers installed (e.g., Ollama or LM Studio)",
"- A compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2)"],
"Url" : "https://github.com/Palm1r/QodeAssist", "Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
${IDE_PLUGIN_DEPENDENCIES} ${IDE_PLUGIN_DEPENDENCIES}
} }

View File

@@ -2,11 +2,5 @@
<qresource prefix="/"> <qresource prefix="/">
<file>resources/images/qoderassist-icon@2x.png</file> <file>resources/images/qoderassist-icon@2x.png</file>
<file>resources/images/qoderassist-icon.png</file> <file>resources/images/qoderassist-icon.png</file>
<file>resources/images/repeat-last-instruct-icon@2x.png</file>
<file>resources/images/repeat-last-instruct-icon.png</file>
<file>resources/images/improve-current-code-icon@2x.png</file>
<file>resources/images/improve-current-code-icon.png</file>
<file>resources/images/suggest-new-icon.png</file>
<file>resources/images/suggest-new-icon@2x.png</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@@ -1,8 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of Qode Assist.
* *
* The Qt Company portions: * The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@@ -24,20 +24,16 @@
#include "QodeAssistClient.hpp" #include "QodeAssistClient.hpp"
#include <QInputDialog>
#include <QTimer> #include <QTimer>
#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h> #include <languageclient/languageclientsettings.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include "core/ChangesManager.h"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include <context/ChangesManager.h>
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol; using namespace LanguageServerProtocol;
using namespace TextEditor; using namespace TextEditor;
@@ -47,12 +43,11 @@ using namespace Core;
namespace QodeAssist { namespace QodeAssist {
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface) QodeAssistClient::QodeAssistClient()
: LanguageClient::Client(clientInterface) : LanguageClient::Client(new LLMClientInterface())
, m_llmClient(clientInterface)
, m_recentCharCount(0) , m_recentCharCount(0)
{ {
setName("QodeAssist"); setName("Qode Assist");
LanguageClient::LanguageFilter filter; LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*"; filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter); setSupportedLanguage(filter);
@@ -75,70 +70,48 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
return; return;
Client::openDocument(document); Client::openDocument(document);
connect( connect(document,
document, &TextDocument::contentsChangedWithPosition,
&TextDocument::contentsChangedWithPosition, this,
this, [this, document](int position, int charsRemoved, int charsAdded) {
[this, document](int position, int charsRemoved, int charsAdded) { Q_UNUSED(charsRemoved)
if (!Settings::codeCompletionSettings().autoCompletion()) if (!Settings::codeCompletionSettings().autoCompletion())
return; return;
auto project = ProjectManager::projectForFile(document->filePath()); auto project = ProjectManager::projectForFile(document->filePath());
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
auto textEditor = BaseTextEditor::currentTextEditor(); auto textEditor = BaseTextEditor::currentTextEditor();
if (!textEditor || textEditor->document() != document) if (!textEditor || textEditor->document() != document)
return; return;
if (Settings::codeCompletionSettings().useProjectChangesCache()) if (Settings::codeCompletionSettings().useProjectChangesCache())
Context::ChangesManager::instance() ChangesManager::instance().addChange(document,
.addChange(document, position, charsRemoved, charsAdded); position,
charsRemoved,
charsAdded);
TextEditorWidget *widget = textEditor->editorWidget(); TextEditorWidget *widget = textEditor->editorWidget();
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors()) if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
return; return;
const int cursorPosition = widget->textCursor().position();
if (cursorPosition < position || cursorPosition > position + charsAdded)
return;
const int cursorPosition = widget->textCursor().position(); m_recentCharCount += charsAdded;
if (cursorPosition < position || cursorPosition > position + charsAdded)
return;
if (charsRemoved > 0 || charsAdded <= 0) { if (m_typingTimer.elapsed()
m_recentCharCount = 0; > Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
m_typingTimer.restart(); m_recentCharCount = charsAdded;
return; m_typingTimer.restart();
} }
QTextCursor cursor = widget->textCursor(); if (m_recentCharCount
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1); > Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
QString lastChar = cursor.selectedText(); scheduleRequest(widget);
}
if (lastChar.isEmpty() || lastChar[0].isPunct()) { });
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
m_recentCharCount += charsAdded;
if (m_typingTimer.elapsed()
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
m_recentCharCount = charsAdded;
m_typingTimer.restart();
}
if (m_recentCharCount
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
scheduleRequest(widget);
}
});
// auto editors = BaseTextEditor::textEditorsForDocument(document);
// connect(
// editors.first()->editorWidget(),
// &TextEditorWidget::selectionChanged,
// this,
// [this, editors]() { m_chatButtonHandler.showButton(editors.first()->editorWidget()); });
} }
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project) bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
@@ -153,26 +126,14 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
MultiTextCursor cursor = editor->multiTextCursor(); MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible()) if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return; return;
const FilePath filePath = editor->textDocument()->filePath(); const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{ GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
{TextDocumentIdentifier(hostPathToServerUri(filePath)), documentVersion(filePath),
documentVersion(filePath), Position(cursor.mainCursor())}};
Position(cursor.mainCursor())}};
if (Settings::codeCompletionSettings().showProgressWidget()) {
m_progressHandler.showProgress(editor);
}
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)]( request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) { const GetCompletionRequest::Response &response) {
QTC_ASSERT(editor, return); QTC_ASSERT(editor, return);
@@ -182,35 +143,6 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
sendMessage(request); sendMessage(request);
} }
void QodeAssistClient::requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this);
connect(
m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted,
this,
&QodeAssistClient::handleRefactoringResult);
}
m_progressHandler.showProgress(editor);
m_refactorHandler->sendRefactorRequest(editor, instructions);
}
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor) void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
{ {
cancelRunningRequest(editor); cancelRunningRequest(editor);
@@ -240,8 +172,8 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position()); it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer()); it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
} }
void QodeAssistClient::handleCompletions( void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor) TextEditor::TextEditorWidget *editor)
{ {
if (response.error()) if (response.error())
log(*response.error()); log(*response.error());
@@ -261,8 +193,8 @@ void QodeAssistClient::handleCompletions(
auto isValidCompletion = [](const Completion &completion) { auto isValidCompletion = [](const Completion &completion) {
return completion.isValid() && !completion.text().trimmed().isEmpty(); return completion.isValid() && !completion.text().trimmed().isEmpty();
}; };
QList<Completion> completions QList<Completion> completions = Utils::filtered(result->completions().toListOrEmpty(),
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion); isValidCompletion);
// remove trailing whitespaces from the end of the completions // remove trailing whitespaces from the end of the completions
for (Completion &completion : completions) { for (Completion &completion : completions) {
@@ -279,19 +211,10 @@ void QodeAssistClient::handleCompletions(
if (delta > 0) if (delta > 0)
completion.setText(completionText.chopped(delta)); completion.setText(completionText.chopped(delta));
} }
auto suggestions = Utils::transform(completions, [](const Completion &c) {
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
return Text::Position{pos.line() + 1, pos.character()};
};
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
m_progressHandler.hideProgress();
if (completions.isEmpty()) if (completions.isEmpty())
return; return;
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document())); editor->insertSuggestion(
std::make_unique<LLMSuggestion>(completions.first(), editor->document()));
} }
} }
@@ -300,18 +223,13 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
const auto it = m_runningRequests.constFind(editor); const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd()) if (it == m_runningRequests.constEnd())
return; return;
m_progressHandler.hideProgress();
cancelRequest(it->id()); cancelRequest(it->id());
m_runningRequests.erase(it); m_runningRequests.erase(it);
} }
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
{ {
if (!project) return Settings::generalSettings().enableQodeAssist();
return Settings::generalSettings().enableQodeAssist();
Settings::ProjectSettings settings(project);
return settings.isEnabled();
} }
void QodeAssistClient::setupConnections() void QodeAssistClient::setupConnections()
@@ -321,13 +239,18 @@ void QodeAssistClient::setupConnections()
openDocument(textDocument); openDocument(textDocument);
}; };
m_documentOpenedConnection m_documentOpenedConnection = connect(EditorManager::instance(),
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc); &EditorManager::documentOpened,
m_documentClosedConnection = connect( this,
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) { openDoc);
if (auto textDocument = qobject_cast<TextDocument *>(document)) m_documentClosedConnection = connect(EditorManager::instance(),
closeDocument(textDocument); &EditorManager::documentClosed,
}); this,
[this](IDocument *document) {
if (auto textDocument = qobject_cast<TextDocument *>(
document))
closeDocument(textDocument);
});
for (IDocument *doc : DocumentModel::openedDocuments()) for (IDocument *doc : DocumentModel::openedDocuments())
openDoc(doc); openDoc(doc);
@@ -342,32 +265,4 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear(); m_scheduledRequests.clear();
} }
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{
if (!result.success) {
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
return;
}
auto editor = BaseTextEditor::currentTextEditor();
if (!editor) {
LOG_MESSAGE("Refactoring failed: No active editor found");
return;
}
auto editorWidget = editor->editorWidget();
QTextCursor cursor = editorWidget->textCursor();
cursor.beginEditBlock();
int startPos = result.insertRange.begin.toPositionInDocument(editorWidget->document());
int endPos = result.insertRange.end.toPositionInDocument(editorWidget->document());
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
cursor.insertText(result.newText);
cursor.endEditBlock();
m_progressHandler.hideProgress();
}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,8 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of Qode Assist.
* *
* The Qt Company portions: * The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@@ -24,40 +24,32 @@
#pragma once #pragma once
#include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp"
#include "QuickRefactorHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include <languageclient/client.h> #include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp> #include "LSPCompletion.hpp"
namespace QodeAssist { namespace QodeAssist {
class QodeAssistClient : public LanguageClient::Client class QodeAssistClient : public LanguageClient::Client
{ {
public: public:
explicit QodeAssistClient(LLMClientInterface *clientInterface); explicit QodeAssistClient();
~QodeAssistClient() override; ~QodeAssistClient() override;
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override; bool canOpenProject(ProjectExplorer::Project *project) override;
void requestCompletions(TextEditor::TextEditorWidget *editor); void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
private: private:
void scheduleRequest(TextEditor::TextEditorWidget *editor); void scheduleRequest(TextEditor::TextEditorWidget *editor);
void handleCompletions( void handleCompletions(const GetCompletionRequest::Response &response,
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor); TextEditor::TextEditorWidget *editor);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor); void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
bool isEnabled(ProjectExplorer::Project *project) const; bool isEnabled(ProjectExplorer::Project *project) const;
void setupConnections(); void setupConnections();
void cleanupConnections(); void cleanupConnections();
void handleRefactoringResult(const RefactorResult &result);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests; QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests; QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
@@ -66,10 +58,6 @@ private:
QElapsedTimer m_typingTimer; QElapsedTimer m_typingTimer;
int m_recentCharCount; int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
LLMClientInterface *m_llmClient;
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,293 +0,0 @@
/*
* Copyright (C) 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 "QuickRefactorHandler.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <context/DocumentContextReader.hpp>
#include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp>
#include <llmcore/PromptTemplateManager.hpp>
#include <llmcore/ProvidersManager.hpp>
#include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp>
namespace QodeAssist {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
: QObject(parent)
, m_requestHandler(new LLMCore::RequestHandler(this))
, m_currentEditor(nullptr)
, m_isRefactoringInProgress(false)
, m_contextManager(this)
{
connect(
m_requestHandler,
&LLMCore::RequestHandler::completionReceived,
this,
&QuickRefactorHandler::handleLLMResponse);
connect(
m_requestHandler,
&LLMCore::RequestHandler::requestFinished,
this,
[this](const QString &requestId, bool success, const QString &errorString) {
if (!success && requestId == m_lastRequestId) {
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = errorString;
emit refactoringCompleted(result);
}
});
}
QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
if (m_isRefactoringInProgress) {
cancelRequest();
}
m_currentEditor = editor;
Utils::Text::Range range;
if (editor->textCursor().hasSelection()) {
QTextCursor cursor = editor->textCursor();
int startPos = cursor.selectionStart();
int endPos = cursor.selectionEnd();
QTextBlock startBlock = editor->document()->findBlock(startPos);
int startLine = startBlock.blockNumber() + 1;
int startColumn = startPos - startBlock.position();
QTextBlock endBlock = editor->document()->findBlock(endPos);
int endLine = endBlock.blockNumber() + 1;
int endColumn = endPos - endBlock.position();
Utils::Text::Position startPosition;
startPosition.line = startLine;
startPosition.column = startColumn;
Utils::Text::Position endPosition;
endPosition.line = endLine;
endPosition.column = endColumn;
range = Utils::Text::Range();
range.begin = startPosition;
range.end = endPosition;
} else {
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
QTextBlock block = editor->document()->findBlock(cursorPos);
int line = block.blockNumber() + 1;
int column = cursorPos - block.position();
Utils::Text::Position cursorPosition;
cursorPosition.line = line;
cursorPosition.column = column;
range = Utils::Text::Range();
range.begin = cursorPosition;
range.end = cursorPosition;
}
m_currentRange = range;
prepareAndSendRequest(editor, instructions, range);
}
void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range)
{
auto &settings = Settings::generalSettings();
auto &providerRegistry = LLMCore::ProvidersManager::instance();
auto &promptManager = LLMCore::PromptTemplateManager::instance();
const auto providerName = settings.caProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
RefactorResult result;
result.success = false;
result.errorMessage = QString("No provider found with name: %1").arg(providerName);
emit refactoringCompleted(result);
return;
}
const auto templateName = settings.caTemplate();
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
RefactorResult result;
result.success = false;
result.errorMessage = QString("No template found with name: %1").arg(templateName);
emit refactoringCompleted(result);
return;
}
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", settings.caModel()}, {"stream", Settings::chatAssistantSettings().stream()}};
config.apiKey = provider->apiKey();
LLMCore::ContextData context = prepareContext(editor, range, instructions);
provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QString requestId = QUuid::createUuid().toString();
m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}};
m_isRefactoringInProgress = true;
m_requestHandler->sendLLMRequest(config, request);
}
LLMCore::ContextData QuickRefactorHandler::prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions)
{
LLMCore::ContextData context;
auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader;
auto documentInfo = documentReader.readDocument(textDocument->filePath().toUrlishString());
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available");
return context;
}
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
// TODO add selecting content before and after cursor/selection
QString fullContent = documentInfo.document->toPlainText();
QString taggedContent = fullContent;
if (cursor.hasSelection()) {
int selEnd = cursor.selectionEnd();
int selStart = cursor.selectionStart();
taggedContent
.insert(selEnd, selEnd == cursorPos ? "<selection_end><cursor>" : "<selection_end>");
taggedContent.insert(
selStart, selStart == cursorPos ? "<cursor><selection_start>" : "<selection_start>");
} else {
taggedContent.insert(cursorPos, "<cursor>");
}
QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt();
systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath;
systemPrompt += "\n\nCode context with position markers:";
systemPrompt += taggedContent;
systemPrompt += "\n\nOutput format:";
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
"between<selection_start><selection_end> or be "
"inserted at cursor position<cursor>";
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
"code block markers";
systemPrompt += "\n- The output should be ready to insert directly into the editor";
systemPrompt += "\n- Follow the existing code style and indentation patterns";
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
}
context.systemPrompt = systemPrompt;
QVector<LLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
}
void QuickRefactorHandler::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
if (request["id"].toString() != m_lastRequestId) {
return;
}
if (isComplete) {
QString cleanedResponse = response.trimmed();
if (cleanedResponse.startsWith("```")) {
int firstNewLine = cleanedResponse.indexOf('\n');
int lastFence = cleanedResponse.lastIndexOf("```");
if (firstNewLine != -1 && lastFence > firstNewLine) {
cleanedResponse
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
} else if (lastFence != -1) {
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
}
}
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
}
}
void QuickRefactorHandler::cancelRequest()
{
if (m_isRefactoringInProgress) {
m_requestHandler->cancelRequest(m_lastRequestId);
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result);
}
}
} // namespace QodeAssist

View File

@@ -1,77 +0,0 @@
/*
* Copyright (C) 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/>.
*/
#pragma once
#include <QJsonObject>
#include <QObject>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <llmcore/RequestHandler.hpp>
namespace QodeAssist {
struct RefactorResult
{
QString newText;
Utils::Text::Range insertRange;
bool success;
QString errorMessage;
};
class QuickRefactorHandler : public QObject
{
Q_OBJECT
public:
explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override;
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest();
signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result);
private:
void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
LLMCore::ContextData prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions);
LLMCore::RequestHandler *m_requestHandler;
TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress;
QString m_lastRequestId;
Context::ContextManager m_contextManager;
};
} // namespace QodeAssist

280
README.md
View File

@@ -1,77 +1,45 @@
# QodeAssist - AI-powered coding assistant plugin for Qt Creator # QodeAssist - AI-powered coding assistant plugin for Qt Creator
[![Discord](https://dcbadge.limes.pink/api/server/https://discord.gg/DGgMtTteAD?style=flat?theme=discord)](https://discord.gg/DGgMtTteAD)
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml) [![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist) ![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
![Static Badge](https://img.shields.io/badge/QtCreator-16.0.1-brightgreen) ![Static Badge](https://img.shields.io/badge/QtCreator-14.0.2-brightgreen)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment. QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
> - These services will consume API tokens which may result in charges to your account
> - The QodeAssist developer bears no responsibility for any charges incurred
> - Please carefully review the provider's pricing and your account settings before use
## Table of Contents ## Table of Contents
1. [Overview](#overview) 1. [Overview](#overview)
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator) 2. [Installation](#installation)
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude) 3. [Configure Plugin](#configure-plugin)
4. [Configure for OpenAI](#configure-for-openai) 4. [Supported LLM Providers](#supported-llm-providers)
5. [Configure for Mistral AI](#configure-for-mistral-ai) 5. [Recommended Models](#recommended-models)
6. [Configure for Google AI](#configure-for-google-ai) - [Ollama](#ollama)
7. [Configure for Ollama](#configure-for-ollama) - [LM Studio](#lm-studio)
8. [Configure for llama.cpp](#configure-for-llamacpp) 6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
9. [System Prompt Configuration](#system-prompt-configuration) 7. [Development Progress](#development-progress)
10. [File Context Features](#file-context-features) 8. [Hotkeys](#hotkeys)
11. [QtCreator Version Compatibility](#qtcreator-version-compatibility) 9. [Troubleshooting](#troubleshooting)
12. [Development Progress](#development-progress) 10. [Support the Development](#support-the-development-of-qodeassist)
13. [Hotkeys](#hotkeys) 11. [How to Build](#how-to-build)
14. [Troubleshooting](#troubleshooting)
15. [Support the Development](#support-the-development-of-qodeassist)
16. [How to Build](#how-to-build)
## Overview ## Overview
- AI-powered code completion - AI-powered code completion
- Sharing IDE opened files with model context (disabled by default, need enable in settings)
- Quick refactor code via fast chat command and opened files
- Chat functionality: - Chat functionality:
- Side and Bottom panels - Side and Bottom panels
- Chat history autosave and restore
- Token usage monitoring and management
- Attach files for one-time code analysis
- Link files for persistent context with auto update in conversations
- Automatic syncing with open editor files (optional)
- Support for multiple LLM providers: - Support for multiple LLM providers:
- Ollama - Ollama
- llama.cpp
- OpenAI
- Anthropic Claude
- LM Studio - LM Studio
- Mistral AI - OpenAI-compatible providers(eg. https://openrouter.ai)
- Google AI
- OpenAI-compatible providers(eg. llama.cpp, https://openrouter.ai)
- Extensive library of model-specific templates - Extensive library of model-specific templates
- Custom template support
- Easy configuration and model selection - Easy configuration and model selection
Join our Discord Community: Have questions or want to discuss QodeAssist? Join our [Discord server](https://discord.gg/BGMkUsXUgf) to connect with other users and get support!
<details> <details>
<summary>Code completion: (click to expand)</summary> <summary>Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
</details> </details>
<details>
<summary>Quick refactor in code: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details>
<details>
<summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
</details>
<details> <details>
<summary>Chat with LLM models in side panels: (click to expand)</summary> <summary>Chat with LLM models in side panels: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/ead5a5d9-b40a-4f17-af05-77fa2bcb3a61" width="600" alt="QodeAssistChat"> <img src="https://github.com/user-attachments/assets/ead5a5d9-b40a-4f17-af05-77fa2bcb3a61" width="600" alt="QodeAssistChat">
@@ -82,80 +50,11 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b"> <img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
</details> </details>
<details> ## Installation
<summary>Automatic syncing with open editor files: (click to expand)</summary>
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
</details>
## Install plugin to QtCreator 1. Install Latest QtCreator
1. Install Latest Qt Creator 2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Download the QodeAssist plugin for your Qt Creator 3. Install a language models in Ollama via terminal. For example, you can run:
- Remove old version plugin if already was installed
- on macOS for QtCreator 16: ~/Library/Application Support/QtProject/Qt Creator/plugins/16.0.0/petrmironychev.qodeassist
- on windows for QtCreator 16: C:\Users\<user>\AppData\Local\QtProject\qtcreator\plugins\16.0.0\petrmironychev.qodeassist\lib\qtcreator\plugins
3. Launch Qt Creator and install the plugin:
- Go to:
- MacOS: Qt Creator -> About Plugins...
- Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Configure for Anthropic Claude
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Claude api key
3. Return to General tab and configure:
- Set "Claude" as the provider for code completion or/and chat assistant
- Set the Claude URL (https://api.anthropic.com)
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
- Choose the Claude template for code completion or/and chat
<details>
<summary>Example of Claude settings: (click to expand)</summary>
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
</details>
## Configure for OpenAI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure OpenAI api key
3. Return to General tab and configure:
- Set "OpenAI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.openai.com)
- Select your preferred model (e.g., gpt-4o)
- Choose the OpenAI template for code completion or/and chat
<details>
<summary>Example of OpenAI settings: (click to expand)</summary>
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
</details>
## Configure for Mistral AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Mistral AI api key
3. Return to General tab and configure:
- Set "Mistral AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.mistral.ai)
- Select your preferred model (e.g., mistral-large-latest)
- Choose the Mistral AI template for code completion or/and chat
<details>
<summary>Example of Mistral AI settings: (click to expand)</summary>
<img width="829" alt="Mistral AI Settings" src="https://github.com/user-attachments/assets/1c5ed13b-a29b-43f7-b33f-2e05fdea540c" />
</details>
## Configure for Google AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Google AI api key
3. Return to General tab and configure:
- Set "Google AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://generativelanguage.googleapis.com/v1beta)
- Select your preferred model (e.g., gemini-2.0-flash)
- Choose the Google AI template
<details>
<summary>Example of Google AI settings: (click to expand)</summary>
<img width="829" alt="Google AI Settings" src="https://github.com/user-attachments/assets/046ede65-a94d-496c-bc6c-41f3750be12a" />
</details>
## Configure for Ollama
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Install a language models in Ollama via terminal. For example, you can run:
For standard computers (minimum 8GB RAM): For standard computers (minimum 8GB RAM):
``` ```
@@ -169,77 +68,82 @@ For high-end systems (32GB+ RAM):
``` ```
ollama run qwen2.5-coder:32b ollama run qwen2.5-coder:32b
``` ```
4. Download the QodeAssist plugin for your QtCreator.
5. Launch Qt Creator and install the plugin:
- Go to MacOS: Qt Creator -> About Plugins...
Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Configure Plugin
QodeAssist comes with default settings that should work immediately after installing a language model. The plugin is pre-configured to use Ollama with standard templates, so you may only need to verify the settings.
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS) 1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
2. Navigate to the "QodeAssist" tab 2. Navigate to the "Qode Assist" tab
3. On the "General" page, verify: 3. On the "General" page, verify:
- Ollama is selected as your LLM provider - Ollama is selected as your LLM provider
- The URL is set to http://localhost:11434 - The URL is set to http://localhost:11434
- Your installed model appears in the model selection - Your installed model appears in the model selection
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct - The prompt template is Ollama Auto FIM
4. Click Apply if you made any changes 4. Click Apply if you made any changes
You're all set! QodeAssist is now ready to use in Qt Creator. You're all set! QodeAssist is now ready to use in Qt Creator.
<details>
<summary>Example of Ollama settings: (click to expand)</summary>
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
</details>
## Configure for llama.cpp ## Supported LLM Providers
1. Open Qt Creator settings and navigate to the QodeAssist section QodeAssist currently supports the following LLM (Large Language Model) providers:
2. Go to General tab and configure: - [Ollama](https://ollama.com)
- Set "llama.cpp" as the provider for code completion or/and chat assistant - [LM Studio](https://lmstudio.ai) (experimental)
- Set the llama.cpp URL (e.g. http://localhost:8080) - OpenAI compatible providers (experimental)
- Fill in model name
- Choose template for model(e.g. llama.cpp FIM for any model with FIM support)
<details>
<summary>Example of llama.cpp settings: (click to expand)</summary>
<img width="829" alt="llama.cpp Settings" src="https://github.com/user-attachments/assets/8c75602c-60f3-49ed-a7a9-d3c972061ea2" />
</details>
## System Prompt Configuration ## Recommended Models:
QodeAssist has been thoroughly tested and optimized for use with the following language models:
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings. - Qwen2.5-coder
- CodeLlama
- StarCoder2
- DeepSeek-Coder-V2
## File Context Features ### Ollama:
### For autocomplete(FIM)
```
ollama run codellama:7b-code
ollama run starcoder2:7b
ollama run qwen2.5-coder:7b-base
ollama run deepseek-coder-v2:16b-lite-base-q3_K_M
```
### For chat
```
ollama run codellama:7b-instruct
ollama run starcoder2:instruct
ollama run qwen2.5-coder:7b-instruct
ollama run deepseek-coder-v2
```
QodeAssist provides two powerful ways to include source code files in your chat conversations: Attachments and Linked Files. Each serves a distinct purpose and helps provide better context for the AI assistant. ### Template-Model Compatibility
### Attached Files | Template | Compatible Models | Purpose |
|----------|------------------|----------|
| CodeLlama FIM | `codellama:code` | Code completion |
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
| Ollama Auto FIM | `Any Ollama base model` | Code completion |
| Qwen FIM | `Qwen 2.5 models` | Code completion |
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
| Alpaca | `starcoder2:instruct` | Chat assistance |
| Basic Chat| `Messages without tokens` | Chat assistance |
| ChatML | `Qwen 2.5 models` | Chat assistance |
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
| Llama3 | `llama3 model family` | Chat assistance |
| Ollama Auto Chat | `Any Ollama chat model` | Chat assistance |
Attachments are designed for one-time code analysis and specific queries: > Note:
- Files are included only in the current message > - FIM (Fill-in-Middle) templates are optimized for code completion
- Content is discarded after the message is processed > - Chat templates are designed for interactive dialogue
- Ideal for: > - The Ollama Auto templates automatically adapt to most Ollama models
- Getting specific feedback on code changes > - Custom Template allows you to define your own prompt format
- Code review requests
- Analyzing isolated code segments
- Quick implementation questions
- Files can be attached using the paperclip icon in the chat interface
- Multiple files can be attached to a single message
### Linked Files
Linked files provide persistent context throughout the conversation:
- Files remain accessible for the entire chat session
- Content is included in every message exchange
- Files are automatically refreshed - always using latest content from disk
- Perfect for:
- Long-term refactoring discussions
- Complex architectural changes
- Multi-file implementations
- Maintaining context across related questions
- Can be managed using the link icon in the chat interface
- Supports automatic syncing with open editor files (can be enabled in settings)
- Files can be added/removed at any time during the conversation
## QtCreator Version Compatibility ## QtCreator Version Compatibility
- QtCreator 16.0.1 - 0.5.7 - 0.x.x
- QtCreator 16.0.0 - 0.5.2 - 0.5.6
- QtCreator 15.0.1 - 0.4.8 - 0.5.1
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
- QtCreator 14.0.2 - 0.2.3 - 0.3.x - QtCreator 14.0.2 - 0.2.3 - 0.3.x
- QtCreator 14.0.1 - 0.2.2 plugin version and below - QtCreator 14.0.1 - 0.2.2 plugin version and below
@@ -257,13 +161,10 @@ Linked files provide persistent context throughout the conversation:
- To call manual request to suggestion, you can use or change it in settings - To call manual request to suggestion, you can use or change it in settings
- on Mac: Option + Command + Q - on Mac: Option + Command + Q
- on Windows: Ctrl + Alt + Q - on Windows: Ctrl + Alt + Q
- on Linux with KDE Plasma: Ctrl + Alt + Q
- To insert the full suggestion, you can use the TAB key - To insert the full suggestion, you can use the TAB key
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac - To insert line by line, you can use the "Move cursor word right" shortcut:
- To call Quick Refactor dialog, select some code or place cursor and press - On Mac: Option + Right Arrow
- on Mac: Option + Command + R - On Windows: Alt + Right Arrow
- on Windows: Ctrl + Alt + R
- on Linux with KDE Plasma: Ctrl + Alt + R
## Troubleshooting ## Troubleshooting
@@ -274,17 +175,20 @@ If QodeAssist is having problems connecting to the LLM provider, please check th
- For Ollama, the default is usually http://localhost:11434 - For Ollama, the default is usually http://localhost:11434
- For LM Studio, the default is usually http://localhost:1234 - For LM Studio, the default is usually http://localhost:1234
2. Confirm that the selected model and template are compatible: 2. Check the endpoint:
Ensure you've chosen the correct model in the "Select Models" option Make sure the endpoint in the settings matches the one required by your provider
Verify that the selected prompt template matches the model you're using - For Ollama, it should be /api/generate
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
3. On Linux the prebuilt binaries support only ubuntu 22.04+ or simililliar os. 3. Confirm that the selected model and template are compatible:
If you need compatiblity with another os, you have to build manualy. our experiments and resolution you can check here: https://github.com/Palm1r/QodeAssist/issues/48
Ensure you've chosen the correct model in the "Select Models" option
Verify that the selected prompt template matches the model you're using
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values: If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
1. Open Qt Creator settings 1. Open Qt Creator settings
2. Navigate to the "QodeAssist" tab 2. Navigate to the "Qode Assist" tab
3. Pick settings page for reset 3. Pick settings page for reset
4. Click on the "Reset Page to Defaults" button 4. Click on the "Reset Page to Defaults" button
- The API key will not reset - The API key will not reset
@@ -323,7 +227,3 @@ relative or absolute path to this plugin directory.
QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them
C++ code style: check use .clang-fortmat in project C++ code style: check use .clang-fortmat in project
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d)
![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41)

View File

@@ -1,72 +0,0 @@
/*
* 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 "UpdateStatusWidget.hpp"
namespace QodeAssist {
UpdateStatusWidget::UpdateStatusWidget(QWidget *parent)
: QFrame(parent)
{
setFrameStyle(QFrame::NoFrame);
auto layout = new QHBoxLayout(this);
layout->setContentsMargins(4, 0, 4, 0);
layout->setSpacing(4);
m_actionButton = new QToolButton(this);
m_actionButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_versionLabel = new QLabel(this);
m_versionLabel->setVisible(false);
m_updateButton = new QPushButton(tr("Update"), this);
m_updateButton->setVisible(false);
m_updateButton->setStyleSheet("QPushButton { padding: 2px 8px; }");
layout->addWidget(m_actionButton);
layout->addWidget(m_versionLabel);
layout->addWidget(m_updateButton);
}
void UpdateStatusWidget::setDefaultAction(QAction *action)
{
m_actionButton->setDefaultAction(action);
}
void UpdateStatusWidget::showUpdateAvailable(const QString &version)
{
m_versionLabel->setText(tr("New version: v%1").arg(version));
m_versionLabel->setVisible(true);
m_updateButton->setVisible(true);
m_updateButton->setToolTip(tr("Check update information"));
}
void UpdateStatusWidget::hideUpdateInfo()
{
m_versionLabel->setVisible(false);
m_updateButton->setVisible(false);
}
QPushButton *UpdateStatusWidget::updateButton() const
{
return m_updateButton;
}
} // namespace QodeAssist

View File

@@ -1,28 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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/>.
*/
#pragma once
#include <QtGlobal>
#define QODEASSIST_QT_CREATOR_VERSION \
QT_VERSION_CHECK( \
QODEASSIST_QT_CREATOR_VERSION_MAJOR, \
QODEASSIST_QT_CREATOR_VERSION_MINOR, \
QODEASSIST_QT_CREATOR_VERSION_PATCH)

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -23,15 +23,16 @@
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
NavigationPanel::NavigationPanel() NavigationPanel::NavigationPanel() {
{
setDisplayName(tr("QodeAssist Chat")); setDisplayName(tr("QodeAssist Chat"));
setPriority(500); setPriority(500);
setId("QodeAssistChat"); setId("QodeAssistChat");
setActivationSequence(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C)); setActivationSequence(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C));
} }
NavigationPanel::~NavigationPanel() {} NavigationPanel::~NavigationPanel()
{
}
Core::NavigationView NavigationPanel::createWidget() Core::NavigationView NavigationPanel::createWidget()
{ {
@@ -41,4 +42,4 @@ Core::NavigationView NavigationPanel::createWidget()
return view; return view;
} }
} // namespace QodeAssist::Chat }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,8 +19,8 @@
#pragma once #pragma once
#include <coreplugin/inavigationwidgetfactory.h>
#include <QObject> #include <QObject>
#include <coreplugin/inavigationwidgetfactory.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -34,4 +34,4 @@ public:
Core::NavigationView createWidget() override; Core::NavigationView createWidget() override;
}; };
} // namespace QodeAssist::Chat }

View File

@@ -1,26 +0,0 @@
add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp
ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp
ContentFile.hpp
DocumentReaderQtCreator.hpp
IDocumentReader.hpp
TokenUtils.hpp TokenUtils.cpp
ProgrammingLanguage.hpp ProgrammingLanguage.cpp
IContextManager.hpp
IgnoreManager.hpp IgnoreManager.cpp
)
target_link_libraries(Context
PUBLIC
Qt::Core
QtCreator::Core
QtCreator::TextEditor
QtCreator::Utils
QtCreator::ProjectExplorer
PRIVATE
LLMCore
QodeAssistSettings
)
target_include_directories(Context PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR})

View File

@@ -1,32 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include <QString>
namespace QodeAssist::Context {
struct ContentFile
{
QString filename;
QString content;
};
} // namespace QodeAssist::Context

View File

@@ -1,185 +0,0 @@
/*
* 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 "ContextManager.hpp"
#include <QFile>
#include <QFileInfo>
#include <QJsonObject>
#include <QTextStream>
#include "settings/GeneralSettings.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>
#include <texteditor/textdocument.h>
#include "Logger.hpp"
namespace QodeAssist::Context {
ContextManager::ContextManager(QObject *parent)
: QObject(parent)
, m_ignoreManager(new IgnoreManager(this))
{}
QString ContextManager::readFile(const QString &filePath) const
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return QString();
QTextStream in(&file);
return in.readAll();
}
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
{
QList<ContentFile> files;
for (const QString &path : filePaths) {
auto project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(path));
if (project && m_ignoreManager->shouldIgnore(path, project)) {
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
continue;
}
ContentFile contentFile = createContentFile(path);
files.append(contentFile);
}
return files;
}
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
{
QStringList sourceFiles;
if (!project)
return sourceFiles;
auto projectNode = project->rootProjectNode();
if (!projectNode)
return sourceFiles;
projectNode->forEachNode(
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) {
sourceFiles.append(fileNode->filePath().toUrlishString());
}
},
nullptr);
return sourceFiles;
}
ContentFile ContextManager::createContentFile(const QString &filePath) const
{
ContentFile contentFile;
QFileInfo fileInfo(filePath);
contentFile.filename = fileInfo.fileName();
contentFile.content = readFile(filePath);
return contentFile;
}
ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &documentInfo) const
{
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available for" + documentInfo.filePath);
return Context::ProgrammingLanguage::Unknown;
}
return Context::ProgrammingLanguageUtils::fromMimeType(documentInfo.mimeType);
}
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
{
const auto &generalSettings = Settings::generalSettings();
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
}
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const
{
auto documents = Core::DocumentModel::openedDocuments();
QList<QPair<QString, QString>> files;
for (const auto *document : std::as_const(documents)) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
if (!excludeFiles.contains(filePath)) {
files.append({filePath, textDocument->plainText()});
}
}
return files;
}
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
{
QString context = "User files context:\n";
auto documents = Core::DocumentModel::openedDocuments();
for (const auto *document : documents) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
if (excludeFiles.contains(filePath))
continue;
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
context += QString("File: %1\n").arg(filePath);
context += textDocument->plainText();
context += "\n";
}
return context;
}
IgnoreManager *ContextManager::ignoreManager() const
{
return m_ignoreManager;
}
} // namespace QodeAssist::Context

View File

@@ -1,60 +0,0 @@
/*
* Copyright (C) 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/>.
*/
#pragma once
#include <QObject>
#include <QString>
#include "ContentFile.hpp"
#include "IContextManager.hpp"
#include "IgnoreManager.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class ContextManager : public QObject, public IContextManager
{
Q_OBJECT
public:
explicit ContextManager(QObject *parent = nullptr);
~ContextManager() override = default;
QString readFile(const QString &filePath) const override;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
ContentFile createContentFile(const QString &filePath) const override;
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
IgnoreManager *ignoreManager() const;
private:
IgnoreManager *m_ignoreManager;
};
} // namespace QodeAssist::Context

View File

@@ -1,299 +0,0 @@
/*
* 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 "DocumentContextReader.hpp"
#include <languageserverprotocol/lsptypes.h>
#include <QFileInfo>
#include <QTextBlock>
#include "CodeCompletionSettings.hpp"
#include "ChangesManager.h"
const QRegularExpression &getYearRegex()
{
static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b");
return yearRegex;
}
const QRegularExpression &getNameRegex()
{
static const QRegularExpression nameRegex("\\b[A-Z][a-z.]+ [A-Z][a-z.]+\\b");
return nameRegex;
}
const QRegularExpression &getCommentRegex()
{
static const QRegularExpression commentRegex(
R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))", QRegularExpression::MultilineOption);
return commentRegex;
}
namespace QodeAssist::Context {
DocumentContextReader::DocumentContextReader(
QTextDocument *document, const QString &mimeType, const QString &filePath)
: m_document(document)
, m_mimeType(mimeType)
, m_filePath(filePath)
{
m_copyrightInfo = findCopyright();
}
QString DocumentContextReader::getLineText(int lineNumber, int cursorPosition) const
{
if (!m_document || lineNumber < 0)
return QString();
QTextBlock block = m_document->begin();
int currentLine = 0;
while (block.isValid()) {
if (currentLine == lineNumber) {
QString text = block.text();
if (cursorPosition >= 0 && cursorPosition <= text.length()) {
text = text.left(cursorPosition);
}
return text;
}
block = block.next();
currentLine++;
}
return QString();
}
QString DocumentContextReader::getContextBefore(
int lineNumber, int cursorPosition, int linesCount) const
{
int startLine = lineNumber - linesCount + 1;
if (m_copyrightInfo.found) {
startLine = qMax(m_copyrightInfo.endLine + 1, startLine);
}
return getContextBetween(startLine, -1, lineNumber, cursorPosition);
}
QString DocumentContextReader::getContextAfter(
int lineNumber, int cursorPosition, int linesCount) const
{
int endLine = lineNumber + linesCount - 1;
if (m_copyrightInfo.found && m_copyrightInfo.endLine >= lineNumber) {
lineNumber = m_copyrightInfo.endLine + 1;
cursorPosition = -1;
}
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
}
QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const
{
int startLine = 0;
if (m_copyrightInfo.found) {
startLine = m_copyrightInfo.endLine + 1;
}
return getContextBetween(startLine, -1, lineNumber, cursorPosition);
}
QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const
{
int endLine = m_document->blockCount() - 1;
if (m_copyrightInfo.found && m_copyrightInfo.endLine >= lineNumber) {
lineNumber = m_copyrightInfo.endLine + 1;
cursorPosition = -1;
}
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
}
QString DocumentContextReader::getLanguageAndFileInfo() const
{
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_mimeType);
QString fileExtension = QFileInfo(m_filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, m_mimeType, m_filePath, fileExtension);
}
CopyrightInfo DocumentContextReader::findCopyright()
{
CopyrightInfo result = {-1, -1, false};
QString text = m_document->toPlainText();
QRegularExpressionMatchIterator matchIterator = getCommentRegex().globalMatch(text);
QList<CopyrightInfo> copyrightBlocks;
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured().toLower();
bool hasCopyrightIndicator = matchedText.contains("copyright")
|| matchedText.contains("(c)") || matchedText.contains("©")
|| matchedText.contains("copr.")
|| matchedText.contains("all rights reserved")
|| matchedText.contains("proprietary")
|| matchedText.contains("licensed under")
|| matchedText.contains("license:")
|| matchedText.contains("gpl") || matchedText.contains("lgpl")
|| matchedText.contains("mit license")
|| matchedText.contains("apache license")
|| matchedText.contains("bsd license")
|| matchedText.contains("mozilla public license")
|| matchedText.contains("copyleft");
bool hasYear = getYearRegex().match(matchedText).hasMatch();
bool hasName = getNameRegex().match(matchedText).hasMatch();
if ((hasCopyrightIndicator && (hasYear || hasName)) || (hasYear && hasName)) {
int startPos = match.capturedStart();
int endPos = match.capturedEnd();
CopyrightInfo info;
info.startLine = m_document->findBlock(startPos).blockNumber();
info.endLine = m_document->findBlock(endPos).blockNumber();
info.found = true;
copyrightBlocks.append(info);
}
}
for (int i = 0; i < copyrightBlocks.size() - 1; ++i) {
if (copyrightBlocks[i].endLine + 1 >= copyrightBlocks[i + 1].startLine) {
copyrightBlocks[i].endLine = copyrightBlocks[i + 1].endLine;
copyrightBlocks.removeAt(i + 1);
--i;
}
}
if (!copyrightBlocks.isEmpty()) { // temproary solution, need cache
return copyrightBlocks.first();
}
return result;
}
QString DocumentContextReader::getContextBetween(
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const
{
QString context;
startLine = qMax(startLine, 0);
endLine = qMin(endLine, m_document->blockCount() - 1);
if (startLine > endLine) {
return context;
}
if (startLine == endLine) {
auto block = m_document->findBlockByNumber(startLine);
if (!block.isValid()) {
return context;
}
auto text = block.text();
if (startCursorPosition < 0) {
startCursorPosition = 0;
}
if (endCursorPosition < 0) {
endCursorPosition = text.size();
}
if (startCursorPosition >= endCursorPosition) {
return context;
}
return text.mid(startCursorPosition, endCursorPosition - startCursorPosition);
}
// first line
{
auto block = m_document->findBlockByNumber(startLine);
if (!block.isValid()) {
return context;
}
auto text = block.text();
if (startCursorPosition < 0) {
context += text + "\n";
} else {
context += text.right(text.size() - startCursorPosition) + "\n";
}
}
// intermediate lines, if any
for (int i = startLine + 1; i <= endLine - 1; ++i) {
auto block = m_document->findBlockByNumber(i);
if (!block.isValid()) {
return context;
}
context += block.text() + "\n";
}
// last line
{
auto block = m_document->findBlockByNumber(endLine);
if (!block.isValid()) {
return context;
}
auto text = block.text();
if (endCursorPosition < 0) {
context += text;
} else {
context += text.left(endCursorPosition);
}
}
return context;
}
CopyrightInfo DocumentContextReader::copyrightInfo() const
{
return m_copyrightInfo;
}
LLMCore::ContextData DocumentContextReader::prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
{
QString contextBefore;
QString contextAfter;
if (settings.readFullFile()) {
contextBefore = readWholeFileBefore(lineNumber, cursorPosition);
contextAfter = readWholeFileAfter(lineNumber, cursorPosition);
} else {
// Note that readStrings{After,Before}Cursor include current line, but linesCount argument of
// getContext{After,Before} do not
contextBefore
= getContextBefore(lineNumber, cursorPosition, settings.readStringsBeforeCursor() + 1);
contextAfter
= getContextAfter(lineNumber, cursorPosition, settings.readStringsAfterCursor() + 1);
}
QString fileContext;
fileContext.append("\n ").append(getLanguageAndFileInfo());
if (settings.useProjectChangesCache())
fileContext.append("Recent Project Changes Context:\n ")
.append(ChangesManager::instance().getRecentChangesContext(m_textDocument));
return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext};
}
} // namespace QodeAssist::Context

View File

@@ -1,92 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include <texteditor/textdocument.h>
#include <QTextDocument>
#include <llmcore/ContextData.hpp>
#include <settings/CodeCompletionSettings.hpp>
namespace QodeAssist::Context {
struct CopyrightInfo
{
int startLine;
int endLine;
bool found;
};
class DocumentContextReader
{
public:
DocumentContextReader(
QTextDocument *m_document, const QString &mimeType, const QString &filePath);
QString getLineText(int lineNumber, int cursorPosition = -1) const;
/**
* @brief Retrieves @c linesCount lines of context ending at @c lineNumber at
* @c cursorPosition in that line. The line at @c lineNumber is inclusive regardless of
* @c cursorPosition.
*/
QString getContextBefore(int lineNumber, int cursorPosition, int linesCount) const;
/**
* @brief Retrieves @c linesCount lines of context starting at @c lineNumber at
* @c cursorPosition in that line. The line at @c lineNumber is inclusive regardless of
* @c cursorPosition.
*/
QString getContextAfter(int lineNumber, int cursorPosition, int linesCount) const;
/**
* @brief Retrieves whole file ending at @c lineNumber at @c cursorPosition in that line.
*/
QString readWholeFileBefore(int lineNumber, int cursorPosition) const;
/**
* @brief Retrieves whole file starting at @c lineNumber at @c cursorPosition in that line.
*/
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
QString getLanguageAndFileInfo() const;
CopyrightInfo findCopyright();
QString getContextBetween(
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const;
CopyrightInfo copyrightInfo() const;
LLMCore::ContextData prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
private:
TextEditor::TextDocument *m_textDocument;
QTextDocument *m_document;
QString m_mimeType;
QString m_filePath;
// Used to omit copyright headers from context. If context would otherwise include copyright
// header it is excluded by deleting it from the returned context. This means, that the
// returned context may contain less information than requested. If the cursor is within copyright
// header, then the context may be empty if the context window is small.
CopyrightInfo m_copyrightInfo;
};
} // namespace QodeAssist::Context

View File

@@ -1,45 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas
*
* 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/>.
*/
#pragma once
#include "IDocumentReader.hpp"
#include <texteditor/textdocument.h>
namespace QodeAssist::Context {
class DocumentReaderQtCreator : public IDocumentReader
{
public:
DocumentInfo readDocument(const QString &path) const override
{
auto *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
Utils::FilePath::fromString(path));
if (!textDocument) {
return {};
}
return {
.document = textDocument->document(),
.mimeType = textDocument->mimeType(),
.filePath = path};
}
};
} // namespace QodeAssist::Context

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) 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/>.
*/
#pragma once
#include <QObject>
#include <QString>
#include "ContentFile.hpp"
#include "IDocumentReader.hpp"
#include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class IContextManager
{
public:
virtual ~IContextManager() = default;
virtual QString readFile(const QString &filePath) const = 0;
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
virtual QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const = 0;
virtual ContentFile createContentFile(const QString &filePath) const = 0;
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;
virtual bool isSpecifyCompletion(const DocumentInfo &documentInfo) const = 0;
};
} // namespace QodeAssist::Context

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas
*
* 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/>.
*/
#pragma once
#include <QTextDocument>
namespace QodeAssist::Context {
struct DocumentInfo
{
QTextDocument *document = nullptr; // not owned
QString mimeType;
QString filePath;
};
class IDocumentReader
{
public:
virtual ~IDocumentReader() = default;
virtual DocumentInfo readDocument(const QString &path) const = 0;
};
} // namespace QodeAssist::Context

View File

@@ -1,274 +0,0 @@
/*
* Copyright (C) 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 "IgnoreManager.hpp"
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QTextStream>
#include "logger/Logger.hpp"
namespace QodeAssist::Context {
IgnoreManager::IgnoreManager(QObject *parent)
: QObject(parent)
{
auto projectManager = ProjectExplorer::ProjectManager::instance();
if (projectManager) {
connect(
projectManager,
&ProjectExplorer::ProjectManager::projectRemoved,
this,
&IgnoreManager::removeIgnorePatterns);
}
connect(
QCoreApplication::instance(),
&QCoreApplication::aboutToQuit,
this,
&IgnoreManager::cleanupConnections);
}
IgnoreManager::~IgnoreManager()
{
cleanupConnections();
}
void IgnoreManager::cleanupConnections()
{
QList<ProjectExplorer::Project *> projects = m_projectConnections.keys();
for (ProjectExplorer::Project *project : projects) {
if (project) {
disconnect(m_projectConnections.take(project));
}
}
m_projectConnections.clear();
m_projectIgnorePatterns.clear();
m_ignoreCache.clear();
}
bool IgnoreManager::shouldIgnore(const QString &filePath, ProjectExplorer::Project *project) const
{
if (!project)
return false;
if (!m_projectIgnorePatterns.contains(project)) {
const_cast<IgnoreManager *>(this)->reloadIgnorePatterns(project);
}
const QStringList &patterns = m_projectIgnorePatterns[project];
if (patterns.isEmpty())
return false;
QDir projectDir(project->projectDirectory().toUrlishString());
QString relativePath = projectDir.relativeFilePath(filePath);
return matchesIgnorePatterns(relativePath, patterns);
}
bool IgnoreManager::matchesIgnorePatterns(const QString &path, const QStringList &patterns) const
{
QString cacheKey = path + ":" + patterns.join("|");
if (m_ignoreCache.contains(cacheKey))
return m_ignoreCache[cacheKey];
bool result = isPathExcluded(path, patterns);
m_ignoreCache.insert(cacheKey, result);
return result;
}
bool IgnoreManager::isPathExcluded(const QString &path, const QStringList &patterns) const
{
bool excluded = false;
for (const QString &pattern : patterns) {
if (pattern.isEmpty() || pattern.startsWith('#'))
continue;
bool isNegative = pattern.startsWith('!');
QString actualPattern = isNegative ? pattern.mid(1) : pattern;
bool matches = matchPathWithPattern(path, actualPattern);
if (matches) {
excluded = !isNegative;
}
}
return excluded;
}
bool IgnoreManager::matchPathWithPattern(const QString &path, const QString &pattern) const
{
QString adjustedPattern = pattern.trimmed();
bool matchFromRoot = adjustedPattern.startsWith('/');
if (matchFromRoot)
adjustedPattern = adjustedPattern.mid(1);
bool matchDirOnly = adjustedPattern.endsWith('/');
if (matchDirOnly)
adjustedPattern.chop(1);
QString regexPattern = QRegularExpression::escape(adjustedPattern);
regexPattern.replace("\\*\\*", ".*");
regexPattern.replace("\\*", "[^/]*");
regexPattern.replace("\\?", ".");
if (matchFromRoot)
regexPattern = QString("^%1").arg(regexPattern);
else
regexPattern = QString("(^|/)%1").arg(regexPattern);
if (matchDirOnly)
regexPattern = QString("%1$").arg(regexPattern);
else
regexPattern = QString("%1($|/)").arg(regexPattern);
QRegularExpression regex(regexPattern);
QRegularExpressionMatch match = regex.match(path);
return match.hasMatch();
}
QStringList IgnoreManager::loadIgnorePatterns(ProjectExplorer::Project *project)
{
QStringList patterns;
if (!project)
return patterns;
QString ignoreFile = ignoreFilePath(project);
if (ignoreFile.isEmpty() || !QFile::exists(ignoreFile)) {
// LOG_MESSAGE(
// QString("No .qodeassistignore file found for project: %1").arg(project->displayName()));
return patterns;
}
QFile file(ignoreFile);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
LOG_MESSAGE(QString("Could not open .qodeassistignore file: %1").arg(ignoreFile));
return patterns;
}
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (!line.isEmpty() && !line.startsWith('#'))
patterns << line;
}
LOG_MESSAGE(QString("Successfully loaded .qodeassistignore file: %1 with %2 patterns")
.arg(ignoreFile)
.arg(patterns.size()));
return patterns;
}
void IgnoreManager::reloadIgnorePatterns(ProjectExplorer::Project *project)
{
if (!project)
return;
QStringList patterns = loadIgnorePatterns(project);
m_projectIgnorePatterns[project] = patterns;
QStringList keysToRemove;
QString projectPath = project->projectDirectory().toUrlishString();
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(projectPath))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
if (!m_projectConnections.contains(project)) {
QPointer<ProjectExplorer::Project> projectPtr(project);
auto connection = connect(project, &QObject::destroyed, this, [this, projectPtr]() {
if (projectPtr) {
m_projectIgnorePatterns.remove(projectPtr);
m_projectConnections.remove(projectPtr);
QStringList keysToRemove;
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(projectPtr->projectDirectory().toUrlishString()))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
}
});
m_projectConnections[project] = connection;
}
}
void IgnoreManager::removeIgnorePatterns(ProjectExplorer::Project *project)
{
m_projectIgnorePatterns.remove(project);
QStringList keysToRemove;
for (auto it = m_ignoreCache.begin(); it != m_ignoreCache.end(); ++it) {
if (it.key().contains(project->projectDirectory().toUrlishString()))
keysToRemove << it.key();
}
for (const QString &key : keysToRemove)
m_ignoreCache.remove(key);
if (m_projectConnections.contains(project)) {
disconnect(m_projectConnections[project]);
m_projectConnections.remove(project);
}
LOG_MESSAGE(QString("Removed ignore patterns for project: %1").arg(project->displayName()));
}
void IgnoreManager::reloadAllPatterns()
{
QList<ProjectExplorer::Project *> projects = m_projectIgnorePatterns.keys();
for (ProjectExplorer::Project *project : projects) {
if (project) {
reloadIgnorePatterns(project);
}
}
m_ignoreCache.clear();
}
QString IgnoreManager::ignoreFilePath(ProjectExplorer::Project *project) const
{
if (!project) {
return QString();
}
return project->projectDirectory().toUrlishString() + "/.qodeassistignore";
}
} // namespace QodeAssist::Context

View File

@@ -1,62 +0,0 @@
/*
* Copyright (C) 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/>.
*/
#pragma once
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QStringList>
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context {
class IgnoreManager : public QObject
{
Q_OBJECT
public:
explicit IgnoreManager(QObject *parent = nullptr);
~IgnoreManager() override;
bool shouldIgnore(const QString &filePath, ProjectExplorer::Project *project = nullptr) const;
void reloadIgnorePatterns(ProjectExplorer::Project *project);
void removeIgnorePatterns(ProjectExplorer::Project *project);
void reloadAllPatterns();
private slots:
void cleanupConnections();
private:
bool matchesIgnorePatterns(const QString &path, const QStringList &patterns) const;
bool isPathExcluded(const QString &path, const QStringList &patterns) const;
bool matchPathWithPattern(const QString &path, const QString &pattern) const;
QStringList loadIgnorePatterns(ProjectExplorer::Project *project);
QString ignoreFilePath(ProjectExplorer::Project *project) const;
QHash<ProjectExplorer::Project *, QStringList> m_projectIgnorePatterns;
mutable QHash<QString, bool> m_ignoreCache;
QHash<ProjectExplorer::Project *, QMetaObject::Connection> m_projectConnections;
};
} // namespace QodeAssist::Context

View File

@@ -1,70 +0,0 @@
/*
* 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 "ProgrammingLanguage.hpp"
namespace QodeAssist::Context {
ProgrammingLanguage ProgrammingLanguageUtils::fromMimeType(const QString &mimeType)
{
if (mimeType == "text/x-qml" || mimeType == "application/javascript"
|| mimeType == "text/javascript" || mimeType == "text/x-javascript") {
return ProgrammingLanguage::QML;
}
if (mimeType == "text/x-c++src" || mimeType == "text/x-c++hdr" || mimeType == "text/x-csrc"
|| mimeType == "text/x-chdr") {
return ProgrammingLanguage::Cpp;
}
if (mimeType == "text/x-python") {
return ProgrammingLanguage::Python;
}
return ProgrammingLanguage::Unknown;
}
QString ProgrammingLanguageUtils::toString(ProgrammingLanguage language)
{
switch (language) {
case ProgrammingLanguage::Cpp:
return "c/c++";
case ProgrammingLanguage::QML:
return "qml";
case ProgrammingLanguage::Python:
return "python";
case ProgrammingLanguage::Unknown:
default:
return QString();
}
}
ProgrammingLanguage ProgrammingLanguageUtils::fromString(const QString &str)
{
QString lower = str.toLower();
if (lower == "c/c++") {
return ProgrammingLanguage::Cpp;
}
if (lower == "qml") {
return ProgrammingLanguage::QML;
}
if (lower == "python") {
return ProgrammingLanguage::Python;
}
return ProgrammingLanguage::Unknown;
}
} // namespace QodeAssist::Context

View File

@@ -1,43 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include <QString>
namespace QodeAssist::Context {
enum class ProgrammingLanguage {
QML, // QML/JavaScript
Cpp, // C/C++
Python,
Unknown,
};
namespace ProgrammingLanguageUtils {
ProgrammingLanguage fromMimeType(const QString &mimeType);
QString toString(ProgrammingLanguage language);
ProgrammingLanguage fromString(const QString &str);
} // namespace ProgrammingLanguageUtils
} // namespace QodeAssist::Context

View File

@@ -1,54 +0,0 @@
/*
* 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 "TokenUtils.hpp"
namespace QodeAssist::Context {
int TokenUtils::estimateTokens(const QString &text)
{
if (text.isEmpty()) {
return 0;
}
// TODO: need to improve
return text.length() / 4;
}
int TokenUtils::estimateFileTokens(const Context::ContentFile &file)
{
int total = 0;
total += estimateTokens(file.filename);
total += estimateTokens(file.content);
total += 5;
return total;
}
int TokenUtils::estimateFilesTokens(const QList<Context::ContentFile> &files)
{
int total = 0;
for (const auto &file : files) {
total += estimateFileTokens(file);
}
return total;
}
} // namespace QodeAssist::Context

View File

@@ -1,36 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include "ContentFile.hpp"
#include <QList>
#include <QString>
namespace QodeAssist::Context {
class TokenUtils
{
public:
static int estimateTokens(const QString &text);
static int estimateFileTokens(const Context::ContentFile &file);
static int estimateFilesTokens(const QList<Context::ContentFile> &files);
};
} // namespace QodeAssist::Context

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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/>.
*/
#pragma once
#include <QJsonObject>
namespace QodeAssist::Context {
inline QString extractFilePathFromRequest(const QJsonObject &request)
{
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QString uri = doc["uri"].toString();
return QUrl(uri).toLocalFile();
}
} // namespace QodeAssist::Context

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -18,9 +18,9 @@
*/ */
#include "ChangesManager.h" #include "ChangesManager.h"
#include "CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
namespace QodeAssist::Context { namespace QodeAssist {
ChangesManager &ChangesManager::instance() ChangesManager &ChangesManager::instance()
{ {
@@ -30,12 +30,17 @@ ChangesManager &ChangesManager::instance()
ChangesManager::ChangesManager() ChangesManager::ChangesManager()
: QObject(nullptr) : QObject(nullptr)
{} {
}
ChangesManager::~ChangesManager() {} ChangesManager::~ChangesManager()
{
}
void ChangesManager::addChange( void ChangesManager::addChange(TextEditor::TextDocument *document,
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded) int position,
int charsRemoved,
int charsAdded)
{ {
auto &documentQueue = m_documentChanges[document]; auto &documentQueue = m_documentChanges[document];
@@ -46,10 +51,9 @@ void ChangesManager::addChange(
ChangeInfo change{fileName, lineNumber, lineContent}; ChangeInfo change{fileName, lineNumber, lineContent};
auto it auto it = std::find_if(documentQueue.begin(),
= std::find_if(documentQueue.begin(), documentQueue.end(), [lineNumber](const ChangeInfo &c) { documentQueue.end(),
return c.lineNumber == lineNumber; [lineNumber](const ChangeInfo &c) { return c.lineNumber == lineNumber; });
});
if (it != documentQueue.end()) { if (it != documentQueue.end()) {
it->lineContent = lineContent; it->lineContent = lineContent;
@@ -75,4 +79,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *
return context; return context;
} }
} // namespace QodeAssist::Context } // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,13 +19,13 @@
#pragma once #pragma once
#include <texteditor/textdocument.h>
#include <QDateTime> #include <QDateTime>
#include <QHash> #include <QHash>
#include <QQueue> #include <QQueue>
#include <QTimer> #include <QTimer>
#include <texteditor/textdocument.h>
namespace QodeAssist::Context { namespace QodeAssist {
class ChangesManager : public QObject class ChangesManager : public QObject
{ {
@@ -41,8 +41,10 @@ public:
static ChangesManager &instance(); static ChangesManager &instance();
void addChange( void addChange(TextEditor::TextDocument *document,
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded); int position,
int charsRemoved,
int charsAdded);
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const; QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
private: private:
@@ -56,4 +58,4 @@ private:
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges; QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist

View File

@@ -3,19 +3,10 @@ add_library(LLMCore STATIC
Provider.hpp Provider.hpp
ProvidersManager.hpp ProvidersManager.cpp ProvidersManager.hpp ProvidersManager.cpp
ContextData.hpp ContextData.hpp
IPromptProvider.hpp
IProviderRegistry.hpp
PromptProviderChat.hpp
PromptProviderFim.hpp
PromptTemplate.hpp PromptTemplate.hpp
PromptTemplateManager.hpp PromptTemplateManager.cpp PromptTemplateManager.hpp PromptTemplateManager.cpp
RequestConfig.hpp RequestConfig.hpp
RequestHandlerBase.hpp RequestHandlerBase.cpp
RequestHandler.hpp RequestHandler.cpp RequestHandler.hpp RequestHandler.cpp
OllamaMessage.hpp OllamaMessage.cpp
OpenAIMessage.hpp OpenAIMessage.cpp
ValidationUtils.hpp ValidationUtils.cpp
ProviderID.hpp
) )
target_link_libraries(LLMCore target_link_libraries(LLMCore

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -20,37 +20,14 @@
#pragma once #pragma once
#include <QString> #include <QString>
#include <QVector>
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
struct Message
{
QString role;
QString content;
// clang-format off
bool operator==(const Message&) const = default;
// clang-format on
};
struct FileMetadata
{
QString filePath;
QString content;
bool operator==(const FileMetadata &) const = default;
};
struct ContextData struct ContextData
{ {
std::optional<QString> systemPrompt; QString prefix;
std::optional<QString> prefix; QString suffix;
std::optional<QString> suffix; QString fileContext;
std::optional<QString> fileContext;
std::optional<QVector<Message>> history;
std::optional<QList<FileMetadata>> filesMetadata;
bool operator==(const ContextData &) const = default;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,39 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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/>.
*/
#pragma once
#include "PromptTemplate.hpp"
#include <QString>
namespace QodeAssist::LLMCore {
class IPromptProvider
{
public:
virtual ~IPromptProvider() = default;
virtual PromptTemplate *getTemplateByName(const QString &templateName) const = 0;
virtual QStringList templatesNames() const = 0;
virtual QStringList getTemplatesForProvider(ProviderID id) const = 0;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,36 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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/>.
*/
#pragma once
#include "Provider.hpp"
namespace QodeAssist::LLMCore {
class IProviderRegistry
{
public:
virtual ~IProviderRegistry() = default;
virtual Provider *getProviderByName(const QString &providerName) = 0;
virtual QStringList providersNames() const = 0;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,102 +0,0 @@
/*
* 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 "OllamaMessage.hpp"
#include <QJsonArray>
#include <QJsonDocument>
namespace QodeAssist::LLMCore {
QJsonObject OllamaMessage::parseJsonFromData(const QByteArray &data)
{
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(line, &error);
if (!doc.isNull() && error.error == QJsonParseError::NoError) {
return doc.object();
}
}
return QJsonObject();
}
OllamaMessage OllamaMessage::fromJson(const QByteArray &data, Type type)
{
OllamaMessage msg;
QJsonObject obj = parseJsonFromData(data);
if (obj.isEmpty()) {
msg.error = "Invalid JSON response";
return msg;
}
msg.model = obj["model"].toString();
msg.createdAt = QDateTime::fromString(obj["created_at"].toString(), Qt::ISODate);
msg.done = obj["done"].toBool();
msg.doneReason = obj["done_reason"].toString();
msg.error = obj["error"].toString();
if (type == Type::Generate) {
auto &genResponse = msg.response.emplace<GenerateResponse>();
genResponse.response = obj["response"].toString();
if (msg.done && obj.contains("context")) {
const auto array = obj["context"].toArray();
genResponse.context.reserve(array.size());
for (const auto &val : array) {
genResponse.context.append(val.toInt());
}
}
} else {
auto &chatResponse = msg.response.emplace<ChatResponse>();
const auto msgObj = obj["message"].toObject();
chatResponse.role = msgObj["role"].toString();
chatResponse.content = msgObj["content"].toString();
}
if (msg.done) {
msg.metrics
= {obj["total_duration"].toVariant().toLongLong(),
obj["load_duration"].toVariant().toLongLong(),
obj["prompt_eval_count"].toVariant().toLongLong(),
obj["prompt_eval_duration"].toVariant().toLongLong(),
obj["eval_count"].toVariant().toLongLong(),
obj["eval_duration"].toVariant().toLongLong()};
}
return msg;
}
QString OllamaMessage::getContent() const
{
if (std::holds_alternative<GenerateResponse>(response)) {
return std::get<GenerateResponse>(response).response;
}
return std::get<ChatResponse>(response).content;
}
bool OllamaMessage::hasError() const
{
return !error.isEmpty();
}
} // namespace QodeAssist::LLMCore

View File

@@ -1,71 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include <QDateTime>
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::LLMCore {
class OllamaMessage
{
public:
enum class Type { Generate, Chat };
struct Metrics
{
qint64 totalDuration{0};
qint64 loadDuration{0};
qint64 promptEvalCount{0};
qint64 promptEvalDuration{0};
qint64 evalCount{0};
qint64 evalDuration{0};
};
struct GenerateResponse
{
QString response;
QVector<int> context;
};
struct ChatResponse
{
QString role;
QString content;
};
QString model;
QDateTime createdAt;
std::variant<GenerateResponse, ChatResponse> response;
bool done{false};
QString doneReason;
QString error;
Metrics metrics;
static OllamaMessage fromJson(const QByteArray &data, Type type);
QString getContent() const;
bool hasError() const;
private:
static QJsonObject parseJsonFromData(const QByteArray &data);
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,82 +0,0 @@
/*
* 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 "OpenAIMessage.hpp"
#include <QJsonArray>
#include <QJsonDocument>
namespace QodeAssist::LLMCore {
OpenAIMessage OpenAIMessage::fromJson(const QJsonObject &obj)
{
OpenAIMessage msg;
if (obj.contains("error")) {
msg.error = obj["error"].toObject()["message"].toString();
return msg;
}
if (obj.contains("choices")) {
auto choices = obj["choices"].toArray();
if (!choices.isEmpty()) {
auto choiceObj = choices[0].toObject();
if (choiceObj.contains("delta")) {
QJsonObject delta = choiceObj["delta"].toObject();
msg.choice.content = delta["content"].toString();
} else if (choiceObj.contains("message")) {
QJsonObject message = choiceObj["message"].toObject();
msg.choice.content = message["content"].toString();
}
msg.choice.finishReason = choiceObj["finish_reason"].toString();
if (!msg.choice.finishReason.isEmpty()) {
msg.done = true;
}
}
}
if (obj.contains("usage")) {
QJsonObject usage = obj["usage"].toObject();
msg.usage.promptTokens = usage["prompt_tokens"].toInt();
msg.usage.completionTokens = usage["completion_tokens"].toInt();
msg.usage.totalTokens = usage["total_tokens"].toInt();
}
return msg;
}
QString OpenAIMessage::getContent() const
{
return choice.content;
}
bool OpenAIMessage::hasError() const
{
return !error.isEmpty();
}
bool OpenAIMessage::isDone() const
{
return done
|| (!choice.finishReason.isEmpty()
&& (choice.finishReason == "stop" || choice.finishReason == "length"));
}
} // namespace QodeAssist::LLMCore

View File

@@ -1,56 +0,0 @@
/*
* 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/>.
*/
#pragma once
#include <QByteArray>
#include <QJsonObject>
#include <QString>
namespace QodeAssist::LLMCore {
class OpenAIMessage
{
public:
struct Choice
{
QString content;
QString finishReason;
};
struct Usage
{
int promptTokens{0};
int completionTokens{0};
int totalTokens{0};
};
Choice choice;
QString error;
bool done{false};
Usage usage;
QString getContent() const;
bool hasError() const;
bool isDone() const;
static OpenAIMessage fromJson(const QJsonObject &obj);
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,53 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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/>.
*/
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplate.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::LLMCore {
class PromptProviderChat : public IPromptProvider
{
public:
explicit PromptProviderChat(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderChat() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getChatTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.chatTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getChatTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,52 +0,0 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* 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/>.
*/
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::LLMCore {
class PromptProviderFim : public IPromptProvider
{
public:
explicit PromptProviderFim(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderFim() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getFimTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.fimTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getFimTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::LLMCore

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -24,11 +24,10 @@
#include <QString> #include <QString>
#include "ContextData.hpp" #include "ContextData.hpp"
#include "ProviderID.hpp"
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
enum class TemplateType { Chat, FIM }; enum class TemplateType { Chat, Fim };
class PromptTemplate class PromptTemplate
{ {
@@ -36,9 +35,9 @@ public:
virtual ~PromptTemplate() = default; virtual ~PromptTemplate() = default;
virtual TemplateType type() const = 0; virtual TemplateType type() const = 0;
virtual QString name() const = 0; virtual QString name() const = 0;
virtual QString promptTemplate() const = 0;
virtual QStringList stopWords() const = 0; virtual QStringList stopWords() const = 0;
virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0; virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0;
virtual QString description() const = 0; virtual QString description() const = 0;
virtual bool isSupportProvider(ProviderID id) const = 0;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -37,48 +37,19 @@ QStringList PromptTemplateManager::chatTemplatesNames() const
return m_chatTemplates.keys(); return m_chatTemplates.keys();
} }
QStringList PromptTemplateManager::getFimTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_fimTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
QStringList PromptTemplateManager::getChatTemplatesForProvider(ProviderID id)
{
QStringList templateList;
for (const auto tmpl : m_chatTemplates) {
if (tmpl->isSupportProvider(id)) {
templateList.append(tmpl->name());
}
}
return templateList;
}
PromptTemplateManager::~PromptTemplateManager() PromptTemplateManager::~PromptTemplateManager()
{ {
qDeleteAll(m_fimTemplates); qDeleteAll(m_fimTemplates);
qDeleteAll(m_chatTemplates);
} }
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName) PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
{ {
if (!m_fimTemplates.contains(templateName))
return m_fimTemplates.first();
return m_fimTemplates[templateName]; return m_fimTemplates[templateName];
} }
PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName) PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName)
{ {
if (!m_chatTemplates.contains(templateName))
return m_chatTemplates.first();
return m_chatTemplates[templateName]; return m_chatTemplates[templateName];
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -35,11 +35,13 @@ public:
template<typename T> template<typename T>
void registerTemplate() void registerTemplate()
{ {
static_assert(std::is_base_of<PromptTemplate, T>::value, "T must inherit from PromptTemplate"); static_assert(std::is_base_of<PromptTemplate, T>::value,
"T must inherit from PromptTemplate");
T *template_ptr = new T(); T *template_ptr = new T();
QString name = template_ptr->name(); QString name = template_ptr->name();
m_fimTemplates[name] = template_ptr; if (template_ptr->type() == TemplateType::Fim) {
if (template_ptr->type() == TemplateType::Chat) { m_fimTemplates[name] = template_ptr;
} else if (template_ptr->type() == TemplateType::Chat) {
m_chatTemplates[name] = template_ptr; m_chatTemplates[name] = template_ptr;
} }
} }
@@ -50,9 +52,6 @@ public:
QStringList fimTemplatesNames() const; QStringList fimTemplatesNames() const;
QStringList chatTemplatesNames() const; QStringList chatTemplatesNames() const;
QStringList getFimTemplatesForProvider(ProviderID id);
QStringList getChatTemplatesForProvider(ProviderID id);
private: private:
PromptTemplateManager() = default; PromptTemplateManager() = default;
PromptTemplateManager(const PromptTemplateManager &) = delete; PromptTemplateManager(const PromptTemplateManager &) = delete;

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,13 +19,9 @@
#pragma once #pragma once
#include <utils/environment.h>
#include <QNetworkRequest>
#include <QString> #include <QString>
#include "ContextData.hpp"
#include "PromptTemplate.hpp"
#include "RequestType.hpp" #include "RequestType.hpp"
#include <utils/environment.h>
class QNetworkReply; class QNetworkReply;
class QJsonObject; class QJsonObject;
@@ -42,18 +38,10 @@ public:
virtual QString completionEndpoint() const = 0; virtual QString completionEndpoint() const = 0;
virtual QString chatEndpoint() const = 0; virtual QString chatEndpoint() const = 0;
virtual bool supportsModelListing() const = 0; virtual bool supportsModelListing() const = 0;
virtual void prepareRequest(
QJsonObject &request, virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type)
= 0;
virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0; virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0;
virtual QList<QString> getInstalledModels(const QString &url) = 0; virtual QList<QString> getInstalledModels(const QString &url) = 0;
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
virtual QString apiKey() const = 0;
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
virtual ProviderID providerID() const = 0;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,34 +0,0 @@
/*
* 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/>.
*/
namespace QodeAssist::LLMCore {
enum class ProviderID {
Any,
Ollama,
LMStudio,
Claude,
OpenAI,
OpenAICompatible,
MistralAI,
OpenRouter,
GoogleAI,
LlamaCpp
};
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -39,8 +39,6 @@ ProvidersManager::~ProvidersManager()
Provider *ProvidersManager::getProviderByName(const QString &providerName) Provider *ProvidersManager::getProviderByName(const QString &providerName)
{ {
if (!m_providers.contains(providerName))
return m_providers.first();
return m_providers[providerName]; return m_providers[providerName];
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -21,12 +21,12 @@
#include <QString> #include <QString>
#include "IProviderRegistry.hpp"
#include <QMap> #include <QMap>
#include "Provider.hpp"
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
class ProvidersManager : public IProviderRegistry class ProvidersManager
{ {
public: public:
static ProvidersManager &instance(); static ProvidersManager &instance();
@@ -41,9 +41,9 @@ public:
m_providers[name] = provider; m_providers[name] = provider;
} }
Provider *getProviderByName(const QString &providerName) override; Provider *getProviderByName(const QString &providerName);
QStringList providersNames() const override; QStringList providersNames() const;
private: private:
ProvidersManager() = default; ProvidersManager() = default;

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,11 +19,11 @@
#pragma once #pragma once
#include <QJsonObject>
#include <QUrl>
#include "PromptTemplate.hpp" #include "PromptTemplate.hpp"
#include "Provider.hpp" #include "Provider.hpp"
#include "RequestType.hpp" #include "RequestType.hpp"
#include <QJsonObject>
#include <QUrl>
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -26,23 +26,22 @@
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
RequestHandler::RequestHandler(QObject *parent) RequestHandler::RequestHandler(QObject *parent)
: RequestHandlerBase(parent) : QObject(parent)
, m_manager(new QNetworkAccessManager(this))
{} {}
void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request) void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request)
{ {
LOG_MESSAGE(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2") LOG_MESSAGE(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
.arg( .arg(config.url.toString(),
config.url.toString(),
QString::fromUtf8( QString::fromUtf8(
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented)))); QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
QNetworkAccessManager *manager = new QNetworkAccessManager();
QNetworkRequest networkRequest(config.url); QNetworkRequest networkRequest(config.url);
config.provider->prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest, config.apiKey);
QNetworkReply *reply QNetworkReply *reply = m_manager->post(networkRequest,
= manager->post(networkRequest, QJsonDocument(config.providerRequest).toJson()); QJsonDocument(config.providerRequest).toJson());
if (!reply) { if (!reply) {
LOG_MESSAGE("Error: Failed to create network reply"); LOG_MESSAGE("Error: Failed to create network reply");
return; return;
@@ -55,42 +54,37 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
handleLLMResponse(reply, request, config); handleLLMResponse(reply, request, config);
}); });
connect(reply, &QNetworkReply::finished, this, [this, reply, requestId, manager]() { connect(reply, &QNetworkReply::finished, this, [this, reply, requestId]() {
reply->deleteLater();
m_activeRequests.remove(requestId); m_activeRequests.remove(requestId);
if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::NoError) {
QString errorMessage = reply->errorString(); LOG_MESSAGE(QString("Error in QodeAssist request: %1").arg(reply->errorString()));
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); emit requestFinished(requestId, false, reply->errorString());
LOG_MESSAGE(
QString("Error details: %1\nStatus code: %2").arg(errorMessage).arg(statusCode));
emit requestFinished(requestId, false, errorMessage);
} else { } else {
LOG_MESSAGE("Request finished successfully"); LOG_MESSAGE("Request finished successfully");
emit requestFinished(requestId, true, QString()); emit requestFinished(requestId, true, QString());
} }
reply->deleteLater();
manager->deleteLater();
}); });
} }
void RequestHandler::handleLLMResponse( void RequestHandler::handleLLMResponse(QNetworkReply *reply,
QNetworkReply *reply, const QJsonObject &request, const LLMConfig &config) const QJsonObject &request,
const LLMConfig &config)
{ {
QString &accumulatedResponse = m_accumulatedResponses[reply]; QString &accumulatedResponse = m_accumulatedResponses[reply];
bool isComplete = config.provider->handleResponse(reply, accumulatedResponse); bool isComplete = config.provider->handleResponse(reply, accumulatedResponse);
if (config.requestType == RequestType::CodeCompletion) { if (config.requestType == RequestType::Fim) {
if (!config.multiLineCompletion if (!config.multiLineCompletion
&& processSingleLineCompletion(reply, request, accumulatedResponse, config)) { && processSingleLineCompletion(reply, request, accumulatedResponse, config)) {
return; return;
} }
if (isComplete) { if (isComplete) {
auto cleanedCompletion auto cleanedCompletion = removeStopWords(accumulatedResponse,
= removeStopWords(accumulatedResponse, config.promptTemplate->stopWords()); config.promptTemplate->stopWords());
removeCodeBlockWrappers(cleanedCompletion);
emit completionReceived(cleanedCompletion, request, true); emit completionReceived(cleanedCompletion, request, true);
} }
@@ -115,6 +109,16 @@ bool RequestHandler::cancelRequest(const QString &id)
return false; return false;
} }
void RequestHandler::prepareNetworkRequest(
QNetworkRequest &networkRequest, const QString &apiKey) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey.isEmpty()) {
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8());
}
}
bool RequestHandler::processSingleLineCompletion( bool RequestHandler::processSingleLineCompletion(
QNetworkReply *reply, QNetworkReply *reply,
const QJsonObject &request, const QJsonObject &request,
@@ -122,6 +126,7 @@ bool RequestHandler::processSingleLineCompletion(
const LLMConfig &config) const LLMConfig &config)
{ {
QString cleanedResponse = accumulatedResponse; QString cleanedResponse = accumulatedResponse;
removeCodeBlockWrappers(cleanedResponse);
int newlinePos = cleanedResponse.indexOf('\n'); int newlinePos = cleanedResponse.indexOf('\n');
if (newlinePos != -1) { if (newlinePos != -1) {
@@ -147,4 +152,36 @@ QString RequestHandler::removeStopWords(const QStringView &completion, const QSt
return filteredCompletion; return filteredCompletion;
} }
void RequestHandler::removeCodeBlockWrappers(QString &response)
{
static const QRegularExpression
fullCodeBlockRegex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
static const QRegularExpression
partialStartBlockRegex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
static const QRegularExpression
partialEndBlockRegex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
QRegularExpressionMatchIterator matchIterator = fullCodeBlockRegex.globalMatch(response);
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString codeBlock = match.captured(0);
QString codeContent = match.captured(1).trimmed();
response.replace(codeBlock, codeContent);
}
QRegularExpressionMatch startMatch = partialStartBlockRegex.match(response);
if (startMatch.hasMatch()) {
QString partialBlock = startMatch.captured(0);
QString codeContent = startMatch.captured(1).trimmed();
response.replace(partialBlock, codeContent);
}
QRegularExpressionMatch endMatch = partialEndBlockRegex.match(response);
if (endMatch.hasMatch()) {
QString partialBlock = endMatch.captured(0);
QString codeContent = endMatch.captured(1).trimmed();
response.replace(partialBlock, codeContent);
}
}
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

Some files were not shown because too many files have changed in this diff Show More