mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-06-04 01:28:58 -04:00
Compare commits
230 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
af3fdb58ff | ||
|
637a4d9d4c | ||
|
7e2345773f | ||
|
14a5ddbdd8 | ||
|
e178b7daa7 | ||
|
4b353d5091 | ||
|
f7ba7b95be | ||
|
6ae95fec45 | ||
|
dad8ab2bf3 | ||
|
25a6983de0 | ||
|
4e05abc7d2 | ||
|
784529e344 | ||
|
155153a763 | ||
|
9225c0c1a9 | ||
|
43adc95857 | ||
|
ee672f2cda | ||
|
a3edb8a577 | ||
|
407d3b11c0 | ||
|
285e739074 | ||
|
f7e748ba7e | ||
|
acb1306321 | ||
|
8b38ecc29b | ||
|
cfb364f033 | ||
|
2fe6850a06 | ||
|
3e9506ca92 | ||
|
d24adff0f5 | ||
|
447324eb07 | ||
|
4ca494cc51 | ||
|
8a80dbe8f5 | ||
|
2b539bbdeb | ||
|
3f2c146df1 | ||
|
9a54f04a0d | ||
|
7a33425d1a | ||
|
711aa672f2 | ||
|
8cb6a2f6d2 | ||
|
2f9622e23e | ||
|
674b1fecde | ||
|
b36d01d2c7 | ||
|
615175bea8 | ||
|
7515599acb | ||
|
3652d4d5d9 | ||
|
75677770b2 | ||
|
329a1efd5d | ||
|
27760a3b99 | ||
|
a93b3cd7f5 | ||
|
bacde51d71 | ||
|
418578743a | ||
|
56e5ef22f1 | ||
|
e90933d713 | ||
|
5b9c67c2d8 | ||
|
fe84a2a303 | ||
|
62de53c306 | ||
|
7c6a10936c | ||
|
032c9bbbf3 | ||
|
8906f98038 | ||
|
5126092449 | ||
|
9d2d70fc63 | ||
|
ffaf6bd61b | ||
|
79218d8412 | ||
|
7e6e526ac8 | ||
|
80646e2af0 | ||
|
5808a892c1 | ||
|
d58ff90458 | ||
|
7d06ab04dc | ||
|
9d40e8ca25 | ||
|
5b16c5403a | ||
|
4ddbe0b8b9 | ||
|
f41e063c02 | ||
|
9d7d084448 | ||
|
1ca1ffc629 | ||
|
8419577ae5 | ||
|
91a6a88130 | ||
|
be38abc505 | ||
|
f2e0afb6b8 | ||
|
3cf07238fd | ||
|
b98f85a997 | ||
|
085659483f | ||
|
8a1fd5438e | ||
|
78f69e82a5 | ||
|
3d770f91c7 | ||
|
c724bace06 | ||
|
719065ebfc | ||
|
a218064a4f | ||
|
13cd12b00a | ||
|
ed59be4199 | ||
|
7dd8b3d085 | ||
|
3839d6896c | ||
|
6b86637dcb | ||
|
58c3e26e7f | ||
|
98e1047bf1 | ||
|
b6f36d61ae | ||
|
f2f453ccc8 | ||
|
1bcfd749d5 | ||
|
e66f467214 | ||
|
c9a3cdaf25 | ||
|
7c483f89cd | ||
|
6c323642e4 | ||
|
3a494d5254 | ||
|
44b3b0cc0c | ||
|
3aae923d43 | ||
|
f94c79a5ff | ||
|
9a5047618d | ||
|
90beebf2ee | ||
|
521261e0a3 | ||
|
5536de146c | ||
|
81ac3c71fb | ||
|
61ca5c9a1b | ||
|
8a167bf248 | ||
|
ab97f39ea4 | ||
|
0d3493e7f6 | ||
|
1d062e1fe4 | ||
|
5dceb5cd19 | ||
|
69a8aa80d9 | ||
|
e218699e64 | ||
|
3dc0d910bf | ||
|
f9f2a86cea | ||
|
247256d4a4 | ||
|
bcf7b6c226 | ||
|
29a3939c64 | ||
|
cb3464170e | ||
|
ca0fb5efbb | ||
|
d8a01504a3 | ||
|
3b188740e8 | ||
|
0d22e1866e | ||
|
61196cae90 | ||
|
102bb114a1 | ||
|
e507d7ee17 | ||
|
ed55c829af | ||
|
d651a246de | ||
|
c8e0f3268e | ||
|
84025ec843 | ||
|
f6fd411b2d | ||
|
903eb50e7a | ||
|
e8f7f031b6 | ||
|
90ae6cd1c0 | ||
|
2ad0117498 | ||
|
9d58565de3 | ||
|
8dba9b4baa | ||
|
7ba615a72d | ||
|
1b06a651f0 | ||
|
912c3d8c04 | ||
|
e924029ec2 | ||
|
d96f44d42c | ||
|
bd25736a55 | ||
|
60936f6d84 | ||
|
7d23d0323f | ||
|
1fa6a225a4 | ||
|
31133e3378 | ||
|
a2f15fc843 | ||
|
2a0beb6c4c | ||
|
e836b86569 | ||
|
288fefebe5 | ||
|
528badbf1e | ||
|
b789e42602 | ||
|
4bf955462f | ||
|
5b99e68e53 | ||
|
0f1b277ef7 | ||
|
56995c9edf | ||
|
45aba6b6be | ||
|
1dfb3feb96 | ||
|
2c49d45297 | ||
|
31145f191b | ||
|
9096adde6f | ||
|
b8e578d2d7 | ||
|
4e45774bce | ||
|
928490d31f | ||
|
97163cf6c9 | ||
|
f85c162692 | ||
|
258053d826 | ||
|
bf63ae5714 | ||
|
ae76850e78 | ||
|
bf3c0b3aa0 | ||
|
9add61c805 | ||
|
add86d2e67 | ||
|
a6c909d34d | ||
|
2814dec3e5 | ||
|
1b86b60de8 | ||
|
4b7f638731 | ||
|
de046f0529 | ||
|
e975e143b1 | ||
|
c97c0f62e8 | ||
|
61fded34ea | ||
|
289a19ac1a | ||
|
43ac662671 | ||
|
1d64d2afc9 | ||
|
9db61119aa | ||
|
70481b3116 | ||
|
511f5b36eb | ||
|
35012865c7 | ||
|
f27429aa66 | ||
|
113d5adcf4 | ||
|
30ea89cdc2 | ||
|
13469edce6 | ||
|
ee2c3950e8 | ||
|
d04e5bc967 | ||
|
d8ef9d0120 | ||
|
e544e46d76 | ||
|
63f0900511 | ||
|
7dee6f62c0 | ||
|
dc06ea2ed5 | ||
|
fc5e1adc0d | ||
|
93e59fb2dc | ||
|
cd2a56cde0 | ||
|
09cde8fd3d | ||
|
ac8080542d | ||
|
7376a11a05 | ||
|
10e8b16caf | ||
|
a38debb140 | ||
|
844ac35a59 | ||
|
16b77a5722 | ||
|
c070fd5cfd | ||
|
882047d7b2 | ||
|
b692402897 | ||
|
8102ba95f9 | ||
|
f8bb9998ab | ||
|
6dab055ca2 | ||
|
7b31fff9f2 | ||
|
be9156fd0e | ||
|
657413344d | ||
|
5f3deb44b9 | ||
|
55e2b24b8d | ||
|
76c17f03dd | ||
|
19c25043fb | ||
|
56b5ea8e68 | ||
|
b475f15e3d | ||
|
31f4516e7b | ||
|
bfdbc755e3 | ||
|
30964d90d5 | ||
|
1261f913bb | ||
|
36d5242a1f |
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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: ''
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -23,16 +23,6 @@ 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.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Log**
|
||||||
- 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.
|
||||||
|
109
.github/scripts/plugin.json
vendored
Normal file
109
.github/scripts/plugin.json
vendored
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"name": "QodeAssist",
|
||||||
|
"vendor": "Petr Mironychev",
|
||||||
|
"tags": [
|
||||||
|
"code assistant",
|
||||||
|
"llm",
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"compatibility": "Qt 6.8.3",
|
||||||
|
"platforms": [
|
||||||
|
"Windows",
|
||||||
|
"macOS",
|
||||||
|
"Linux"
|
||||||
|
],
|
||||||
|
"license": "GPLv3",
|
||||||
|
"version": "0.5.11",
|
||||||
|
"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": false,
|
||||||
|
"released_at": "2025-04-17T10:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.9",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-21T10:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.10",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-24T10:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.11",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-24T21:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.12",
|
||||||
|
"is_latest": true,
|
||||||
|
"released_at": "2025-05-01T17: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": []
|
||||||
|
}
|
147
.github/scripts/registerPlugin.js
vendored
Normal file
147
.github/scripts/registerPlugin.js
vendored
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
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'));
|
163
.github/workflows/build_cmake.yml
vendored
163
.github/workflows/build_cmake.yml
vendored
@ -9,18 +9,16 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PLUGIN_NAME: QodeAssist
|
PLUGIN_NAME: QodeAssist
|
||||||
QT_VERSION: 6.7.3
|
|
||||||
QT_CREATOR_VERSION: 14.0.2
|
|
||||||
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"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: ${{ matrix.config.name }}
|
name: ${{ matrix.config.name }} (Qt ${{ matrix.qt_config.qt_version }}, QtC ${{ matrix.qt_config.qt_creator_version }})
|
||||||
runs-on: ${{ matrix.config.os }}
|
runs-on: ${{ matrix.config.os }}
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.git.outputs.tag }}
|
tag: ${{ steps.git.outputs.tag }}
|
||||||
@ -30,76 +28,57 @@ 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 Latest GCC", artifact: "Linux-x64",
|
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64",
|
||||||
os: ubuntu-latest,
|
os: ubuntu-22.04,
|
||||||
|
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++"
|
||||||
}
|
}
|
||||||
|
qt_config:
|
||||||
|
- {
|
||||||
|
qt_version: "6.8.3",
|
||||||
|
qt_creator_version: "16.0.2"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
qt_version: "6.8.3",
|
||||||
|
qt_creator_version: "16.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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}\n")
|
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
|
||||||
else()
|
else()
|
||||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}\n")
|
execute_process(
|
||||||
|
COMMAND git rev-parse --short HEAD
|
||||||
|
OUTPUT_VARIABLE short_sha
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${short_sha}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
- name: Download Ninja and CMake
|
- name: Download Ninja and CMake
|
||||||
shell: cmake -P {0}
|
uses: lukka/get-cmake@latest
|
||||||
run: |
|
with:
|
||||||
set(cmake_version "$ENV{CMAKE_VERSION}")
|
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
||||||
set(ninja_version "$ENV{NINJA_VERSION}")
|
ninjaVersion: ${{ env.NINJA_VERSION }}
|
||||||
|
|
||||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
- name: Install dependencies
|
||||||
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")
|
||||||
@ -107,7 +86,13 @@ jobs:
|
|||||||
COMMAND sudo apt update
|
COMMAND sudo apt update
|
||||||
)
|
)
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND sudo apt install libgl1-mesa-dev libcups2-dev
|
COMMAND sudo apt install
|
||||||
|
# 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)
|
||||||
@ -119,14 +104,14 @@ jobs:
|
|||||||
id: qt
|
id: qt
|
||||||
shell: cmake -P {0}
|
shell: cmake -P {0}
|
||||||
run: |
|
run: |
|
||||||
set(qt_version "$ENV{QT_VERSION}")
|
set(qt_version "${{ matrix.qt_config.qt_version }}")
|
||||||
|
|
||||||
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_msvc2019_64")
|
set(qt_package_arch_suffix "win64_msvc2022_64")
|
||||||
set(qt_dir_prefix "${qt_version}/msvc2019_64")
|
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
||||||
set(qt_package_suffix "-Windows-Windows_10_22H2-MSVC2019-Windows-Windows_10_22H2-X86_64")
|
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-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")
|
||||||
@ -135,15 +120,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_8-GCC-Linux-RHEL_8_8-X86_64")
|
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-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_13-Clang-MacOS-MacOS_13-X86_64-ARM64")
|
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}")
|
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/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)
|
||||||
@ -153,7 +138,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_prefix}" qt_dir)
|
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6" 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}")
|
||||||
@ -172,11 +157,17 @@ jobs:
|
|||||||
|
|
||||||
foreach(package qt5compat qtshadertools)
|
foreach(package qt5compat qtshadertools)
|
||||||
downloadAndExtract(
|
downloadAndExtract(
|
||||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${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")
|
||||||
@ -184,47 +175,26 @@ jobs:
|
|||||||
else()
|
else()
|
||||||
set(uic_suffix "Rhel8.6-x86_64")
|
set(uic_suffix "Rhel8.6-x86_64")
|
||||||
endif()
|
endif()
|
||||||
downloadAndExtract(
|
downloadAndExtractLibicu(
|
||||||
"${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@v2.0
|
||||||
|
with:
|
||||||
|
version: ${{ matrix.qt_config.qt_creator_version }}
|
||||||
|
unzip-to: 'qtcreator'
|
||||||
|
platform: ${{ matrix.config.platform }}
|
||||||
|
|
||||||
|
- 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: |
|
||||||
@ -262,7 +232,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}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
|
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.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 }}"
|
||||||
@ -280,13 +250,18 @@ jobs:
|
|||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||||
name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
if: startsWith(matrix.config.os, 'ubuntu')
|
||||||
|
run: |
|
||||||
|
xvfb-run ./build/build/test/QodeAssistTest
|
||||||
|
|
||||||
release:
|
release:
|
||||||
if: contains(github.ref, 'tags/v')
|
if: contains(github.ref, 'tags/v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
needs: build
|
needs: [build]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
|
24
.github/workflows/check_formatting.yml
vendored
Normal file
24
.github/workflows/check_formatting.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "3rdparty/inja"]
|
||||||
|
path = 3rdparty/inja
|
||||||
|
url = https://github.com/pantor/inja
|
1
3rdparty/inja
vendored
Submodule
1
3rdparty/inja
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 384a6bef3f1b69f8e429b28ee206df832cb2a6c8
|
@ -8,14 +8,38 @@ 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 REQUIRED)
|
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test 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
|
||||||
@ -40,27 +64,65 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssistConstants.hpp
|
QodeAssistConstants.hpp
|
||||||
QodeAssisttr.h
|
QodeAssisttr.h
|
||||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||||
|
templates/Templates.hpp
|
||||||
templates/CodeLlamaFim.hpp
|
templates/CodeLlamaFim.hpp
|
||||||
templates/StarCoder2Fim.hpp
|
|
||||||
templates/DeepSeekCoderFim.hpp
|
|
||||||
templates/CustomFimTemplate.hpp
|
|
||||||
templates/DeepSeekCoderChat.hpp
|
|
||||||
templates/CodeLlamaChat.hpp
|
|
||||||
templates/Qwen.hpp
|
|
||||||
templates/StarCoderChat.hpp
|
|
||||||
templates/Ollama.hpp
|
templates/Ollama.hpp
|
||||||
templates/BasicChat.hpp
|
templates/Claude.hpp
|
||||||
|
templates/OpenAI.hpp
|
||||||
|
templates/MistralAI.hpp
|
||||||
|
templates/StarCoder2Fim.hpp
|
||||||
|
# templates/DeepSeekCoderFim.hpp
|
||||||
|
# templates/CustomFimTemplate.hpp
|
||||||
|
templates/Qwen.hpp
|
||||||
|
templates/OpenAICompatible.hpp
|
||||||
|
templates/Llama3.hpp
|
||||||
|
templates/ChatML.hpp
|
||||||
|
templates/Alpaca.hpp
|
||||||
|
templates/Llama2.hpp
|
||||||
|
templates/CodeLlamaQMLFim.hpp
|
||||||
|
templates/GoogleAI.hpp
|
||||||
|
templates/LlamaCppFim.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()
|
||||||
|
@ -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,6 +13,18 @@ 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
|
||||||
@ -20,6 +32,7 @@ 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
|
||||||
@ -32,6 +45,7 @@ target_link_libraries(QodeAssistChatView
|
|||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
LLMCore
|
LLMCore
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
|
Context
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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(&settings.chatTokensThreshold,
|
connect(
|
||||||
&Utils::BaseAspect::changed,
|
&settings.chatTokensThreshold,
|
||||||
this,
|
&Utils::BaseAspect::changed,
|
||||||
&ChatModel::tokensThresholdChanged);
|
this,
|
||||||
|
&ChatModel::tokensThresholdChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatModel::rowCount(const QModelIndex &parent) const
|
int ChatModel::rowCount(const QModelIndex &parent) const
|
||||||
@ -55,6 +55,13 @@ 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();
|
||||||
}
|
}
|
||||||
@ -65,29 +72,37 @@ 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(const QString &content, ChatRole role, const QString &id)
|
void ChatModel::addMessage(
|
||||||
|
const QString &content,
|
||||||
|
ChatRole role,
|
||||||
|
const QString &id,
|
||||||
|
const QList<Context::ContentFile> &attachments)
|
||||||
{
|
{
|
||||||
int tokenCount = estimateTokenCount(content);
|
QString fullContent = 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.tokenCount = tokenCount;
|
lastMessage.attachments = attachments;
|
||||||
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());
|
||||||
m_messages.append({role, content, tokenCount, id});
|
Message newMessage{role, content, id};
|
||||||
m_totalTokens += tokenCount;
|
newMessage.attachments = attachments;
|
||||||
|
m_messages.append(newMessage);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
trim();
|
|
||||||
emit totalTokensChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
||||||
@ -95,32 +110,12 @@ 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 totalTokensChanged();
|
emit modelReseted();
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||||
@ -129,11 +124,13 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
|||||||
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
||||||
int lastIndex = 0;
|
int lastIndex = 0;
|
||||||
auto blockMatches = codeBlockRegex.globalMatch(content);
|
auto blockMatches = codeBlockRegex.globalMatch(content);
|
||||||
|
bool foundCodeBlock = blockMatches.hasNext();
|
||||||
|
|
||||||
while (blockMatches.hasNext()) {
|
while (blockMatches.hasNext()) {
|
||||||
auto match = blockMatches.next();
|
auto match = blockMatches.next();
|
||||||
if (match.capturedStart() > lastIndex) {
|
if (match.capturedStart() > lastIndex) {
|
||||||
QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
QString textBetween
|
||||||
|
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||||
if (!textBetween.isEmpty()) {
|
if (!textBetween.isEmpty()) {
|
||||||
parts.append({MessagePart::Text, textBetween, ""});
|
parts.append({MessagePart::Text, textBetween, ""});
|
||||||
}
|
}
|
||||||
@ -144,7 +141,19 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
|||||||
|
|
||||||
if (lastIndex < content.length()) {
|
if (lastIndex < content.length()) {
|
||||||
QString remainingText = content.mid(lastIndex).trimmed();
|
QString remainingText = content.mid(lastIndex).trimmed();
|
||||||
if (!remainingText.isEmpty()) {
|
|
||||||
|
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
|
||||||
|
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
|
||||||
|
|
||||||
|
if (unclosedMatch.hasMatch()) {
|
||||||
|
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
|
||||||
|
if (!beforeCodeBlock.isEmpty()) {
|
||||||
|
parts.append({MessagePart::Text, beforeCodeBlock, ""});
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.append(
|
||||||
|
{MessagePart::Code, unclosedMatch.captured(2).trimmed(), unclosedMatch.captured(1)});
|
||||||
|
} else if (!remainingText.isEmpty()) {
|
||||||
parts.append({MessagePart::Text, remainingText, ""});
|
parts.append({MessagePart::Text, remainingText, ""});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,7 +164,6 @@ 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) {
|
||||||
@ -170,17 +178,27 @@ 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();
|
||||||
@ -192,4 +210,16 @@ 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()) {
|
||||||
|
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
|
||||||
|
m_messages.remove(index, m_messages.size() - index);
|
||||||
|
endRemoveRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -26,27 +26,29 @@
|
|||||||
#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);
|
||||||
@ -55,29 +57,30 @@ 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(const QString &content, ChatRole role, const QString &id);
|
Q_INVOKABLE void addMessage(
|
||||||
|
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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -18,33 +18,117 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#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_clientInterface(new ClientInterface(m_chatModel, this))
|
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
|
||||||
|
, 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(&settings.caModel,
|
connect(
|
||||||
&Utils::BaseAspect::changed,
|
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
||||||
this,
|
|
||||||
&ChatRootView::currentTemplateChanged);
|
|
||||||
|
|
||||||
connect(&Settings::chatAssistantSettings().sharingCurrentFile,
|
connect(
|
||||||
&Utils::BaseAspect::changed,
|
m_clientInterface,
|
||||||
this,
|
&ClientInterface::messageReceivedCompletely,
|
||||||
&ChatRootView::isSharingCurrentFileChanged);
|
this,
|
||||||
|
&ChatRootView::autosave);
|
||||||
|
|
||||||
generateColors();
|
connect(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
&Settings::chatAssistantSettings().textFontFamily,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatRootView::textFamilyChanged);
|
||||||
|
connect(
|
||||||
|
&Settings::chatAssistantSettings().codeFontFamily,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatRootView::codeFamilyChanged);
|
||||||
|
connect(
|
||||||
|
&Settings::chatAssistantSettings().textFontSize,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatRootView::textFontSizeChanged);
|
||||||
|
connect(
|
||||||
|
&Settings::chatAssistantSettings().codeFontSize,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatRootView::codeFontSizeChanged);
|
||||||
|
connect(
|
||||||
|
&Settings::chatAssistantSettings().textFormat,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatRootView::textFormatChanged);
|
||||||
|
|
||||||
|
updateInputTokensCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel *ChatRootView::chatModel() const
|
ChatModel *ChatRootView::chatModel() const
|
||||||
@ -52,14 +136,26 @@ ChatModel *ChatRootView::chatModel() const
|
|||||||
return m_chatModel;
|
return m_chatModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::backgroundColor() const
|
void ChatRootView::sendMessage(const QString &message)
|
||||||
{
|
{
|
||||||
return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
if (m_inputTokensCount > m_chatModel->tokensThreshold()) {
|
||||||
}
|
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);
|
||||||
|
|
||||||
void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) const
|
if (reply == QMessageBox::Yes) {
|
||||||
{
|
autosave();
|
||||||
m_clientInterface->sendMessage(message, sharingCurrentFile);
|
m_chatModel->clear();
|
||||||
|
setRecentFilePath(QString{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
|
||||||
|
clearAttachmentFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::copyToClipboard(const QString &text)
|
void ChatRootView::copyToClipboard(const QString &text)
|
||||||
@ -72,47 +168,41 @@ void ChatRootView::cancelRequest()
|
|||||||
m_clientInterface->cancelRequest();
|
m_clientInterface->cancelRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::generateColors()
|
void ChatRootView::clearAttachmentFiles()
|
||||||
{
|
{
|
||||||
QColor baseColor = backgroundColor();
|
if (!m_attachmentFiles.isEmpty()) {
|
||||||
bool isDarkTheme = baseColor.lightness() < 128;
|
m_attachmentFiles.clear();
|
||||||
|
emit attachmentFilesChanged();
|
||||||
if (isDarkTheme) {
|
|
||||||
m_primaryColor = generateColor(baseColor, 0.1, 1.2, 1.4);
|
|
||||||
m_secondaryColor = generateColor(baseColor, -0.1, 1.1, 1.2);
|
|
||||||
m_codeColor = generateColor(baseColor, 0.05, 0.8, 1.1);
|
|
||||||
} else {
|
|
||||||
m_primaryColor = generateColor(baseColor, 0.05, 1.05, 1.1);
|
|
||||||
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,
|
void ChatRootView::clearLinkedFiles()
|
||||||
float hueShift,
|
|
||||||
float saturationMod,
|
|
||||||
float lightnessMod)
|
|
||||||
{
|
{
|
||||||
float h, s, l, a;
|
if (!m_linkedFiles.isEmpty()) {
|
||||||
baseColor.getHslF(&h, &s, &l, &a);
|
m_linkedFiles.clear();
|
||||||
bool isDarkTheme = l < 0.5;
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h = fmod(h + hueShift + 1.0, 1.0);
|
QString ChatRootView::getChatsHistoryDir() const
|
||||||
|
{
|
||||||
|
QString path;
|
||||||
|
|
||||||
s = qBound(0.0f, s * saturationMod, 1.0f);
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
if (isDarkTheme) {
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
l = qBound(0.0f, l * lightnessMod, 1.0f);
|
|
||||||
} else {
|
} else {
|
||||||
l = qBound(0.0f, l / lightnessMod, 1.0f);
|
path = QString("%1/qodeassist/chat_history")
|
||||||
|
.arg(Core::ICore::userResourcePath().toFSPathString());
|
||||||
}
|
}
|
||||||
|
|
||||||
h = qBound(0.0f, h, 1.0f);
|
QDir dir(path);
|
||||||
s = qBound(0.0f, s, 1.0f);
|
if (!dir.exists() && !dir.mkpath(".")) {
|
||||||
l = qBound(0.0f, l, 1.0f);
|
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
|
||||||
a = qBound(0.0f, a, 1.0f);
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
return QColor::fromHslF(h, s, l, a);
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::currentTemplate() const
|
QString ChatRootView::currentTemplate() const
|
||||||
@ -121,24 +211,395 @@ QString ChatRootView::currentTemplate() const
|
|||||||
return settings.caModel();
|
return settings.caModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::primaryColor() const
|
void ChatRootView::saveHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
return m_primaryColor;
|
auto result = ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||||
|
if (!result.success) {
|
||||||
|
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
|
||||||
|
} else {
|
||||||
|
setRecentFilePath(filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::secondaryColor() const
|
void ChatRootView::loadHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
return m_secondaryColor;
|
auto result = ChatSerializer::loadFromFile(m_chatModel, filePath);
|
||||||
|
if (!result.success) {
|
||||||
|
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
|
||||||
|
} else {
|
||||||
|
setRecentFilePath(filePath);
|
||||||
|
}
|
||||||
|
updateInputTokensCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor ChatRootView::codeColor() const
|
void ChatRootView::showSaveDialog()
|
||||||
{
|
{
|
||||||
return m_codeColor;
|
QString initialDir = getChatsHistoryDir();
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::isSharingCurrentFile() const
|
void ChatRootView::showLoadDialog()
|
||||||
{
|
{
|
||||||
return Settings::chatAssistantSettings().sharingCurrentFile();
|
QString initialDir = getChatsHistoryDir();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::textFontFamily() const
|
||||||
|
{
|
||||||
|
return Settings::chatAssistantSettings().textFontFamily.stringValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::codeFontFamily() const
|
||||||
|
{
|
||||||
|
return Settings::chatAssistantSettings().codeFontFamily.stringValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatRootView::codeFontSize() const
|
||||||
|
{
|
||||||
|
return Settings::chatAssistantSettings().codeFontSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatRootView::textFontSize() const
|
||||||
|
{
|
||||||
|
return Settings::chatAssistantSettings().textFontSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatRootView::textFormat() const
|
||||||
|
{
|
||||||
|
return Settings::chatAssistantSettings().textFormat();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -23,24 +23,27 @@
|
|||||||
|
|
||||||
#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(QColor backgroundColor READ backgroundColor CONSTANT FINAL)
|
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||||
Q_PROPERTY(QColor primaryColor READ primaryColor CONSTANT FINAL)
|
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||||
Q_PROPERTY(QColor secondaryColor READ secondaryColor CONSTANT FINAL)
|
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||||
Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL)
|
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
||||||
Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY
|
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
|
||||||
isSharingCurrentFileChanged FINAL)
|
Q_PROPERTY(QString textFontFamily READ textFontFamily NOTIFY textFamilyChanged FINAL)
|
||||||
|
Q_PROPERTY(QString codeFontFamily READ codeFontFamily NOTIFY codeFamilyChanged FINAL)
|
||||||
|
Q_PROPERTY(int codeFontSize READ codeFontSize NOTIFY codeFontSizeChanged FINAL)
|
||||||
|
Q_PROPERTY(int textFontSize READ textFontSize NOTIFY textFontSizeChanged FINAL)
|
||||||
|
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@ -49,38 +52,82 @@ public:
|
|||||||
ChatModel *chatModel() const;
|
ChatModel *chatModel() const;
|
||||||
QString currentTemplate() const;
|
QString currentTemplate() const;
|
||||||
|
|
||||||
QColor backgroundColor() const;
|
void saveHistory(const QString &filePath);
|
||||||
QColor primaryColor() const;
|
void loadHistory(const QString &filePath);
|
||||||
QColor secondaryColor() const;
|
|
||||||
|
|
||||||
QColor codeColor() const;
|
Q_INVOKABLE void showSaveDialog();
|
||||||
|
Q_INVOKABLE void showLoadDialog();
|
||||||
|
|
||||||
bool isSharingCurrentFile() const;
|
void autosave();
|
||||||
|
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);
|
||||||
|
|
||||||
|
QString textFontFamily() const;
|
||||||
|
QString codeFontFamily() const;
|
||||||
|
|
||||||
|
int codeFontSize() const;
|
||||||
|
int textFontSize() const;
|
||||||
|
int textFormat() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message, bool sharingCurrentFile = false) const;
|
void sendMessage(const QString &message);
|
||||||
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 isSharingCurrentFileChanged();
|
void linkedFilesChanged();
|
||||||
|
void inputTokensCountChanged();
|
||||||
|
void isSyncOpenFilesChanged();
|
||||||
|
void chatFileNameChanged();
|
||||||
|
void textFamilyChanged();
|
||||||
|
void codeFamilyChanged();
|
||||||
|
void codeFontSizeChanged();
|
||||||
|
void textFontSizeChanged();
|
||||||
|
void textFormatChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void generateColors();
|
QString getChatsHistoryDir() const;
|
||||||
QColor generateColor(const QColor &baseColor,
|
QString getSuggestedFileName() const;
|
||||||
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;
|
||||||
QColor m_primaryColor;
|
QString m_recentFilePath;
|
||||||
QColor m_secondaryColor;
|
QStringList m_attachmentFiles;
|
||||||
QColor m_codeColor;
|
QStringList m_linkedFiles;
|
||||||
|
int m_messageTokensCount{0};
|
||||||
|
int m_inputTokensCount{0};
|
||||||
|
bool m_isSyncOpenFiles;
|
||||||
|
QList<Core::IEditor *> m_currentEditors;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
142
ChatView/ChatSerializer.cpp
Normal file
142
ChatView/ChatSerializer.cpp
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
56
ChatView/ChatSerializer.hpp
Normal file
56
ChatView/ChatSerializer.hpp
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -29,4 +29,40 @@ void ChatUtils::copyToClipboard(const QString &text)
|
|||||||
QGuiApplication::clipboard()->setText(text);
|
QGuiApplication::clipboard()->setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ChatUtils::getSafeMarkdownText(const QString &text) const
|
||||||
|
{
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool needsSanitization = false;
|
||||||
|
for (const QChar &ch : text) {
|
||||||
|
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
|
||||||
|
needsSanitization = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsSanitization) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString safeText;
|
||||||
|
safeText.reserve(text.size());
|
||||||
|
|
||||||
|
for (QChar ch : text) {
|
||||||
|
if (ch.isNull()) {
|
||||||
|
safeText.append(' ');
|
||||||
|
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
|
||||||
|
safeText.append(ch);
|
||||||
|
} else if (ch.isPrint()) {
|
||||||
|
safeText.append(ch);
|
||||||
|
} else {
|
||||||
|
safeText.append(QChar(0xFFFD));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeText;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -23,7 +23,6 @@
|
|||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
// Q_NAMESPACE
|
|
||||||
|
|
||||||
class ChatUtils : public QObject
|
class ChatUtils : public QObject
|
||||||
{
|
{
|
||||||
@ -35,6 +34,7 @@ public:
|
|||||||
: QObject(parent) {};
|
: QObject(parent) {};
|
||||||
|
|
||||||
Q_INVOKABLE void copyToClipboard(const QString &text);
|
Q_INVOKABLE void copyToClipboard(const QString &text);
|
||||||
|
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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,39 +35,47 @@
|
|||||||
#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(ChatModel *chatModel, QObject *parent)
|
ClientInterface::ClientInterface(
|
||||||
|
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(m_requestHandler,
|
connect(
|
||||||
&LLMCore::RequestHandler::completionReceived,
|
m_requestHandler,
|
||||||
this,
|
&LLMCore::RequestHandler::completionReceived,
|
||||||
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
|
this,
|
||||||
handleLLMResponse(completion, request, isComplete);
|
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
|
||||||
});
|
handleLLMResponse(completion, request, isComplete);
|
||||||
|
});
|
||||||
|
|
||||||
connect(m_requestHandler,
|
connect(
|
||||||
&LLMCore::RequestHandler::requestFinished,
|
m_requestHandler,
|
||||||
this,
|
&LLMCore::RequestHandler::requestFinished,
|
||||||
[this](const QString &, bool success, const QString &errorString) {
|
this,
|
||||||
if (!success) {
|
[this](const QString &, bool success, const QString &errorString) {
|
||||||
emit errorOccurred(errorString);
|
if (!success) {
|
||||||
}
|
emit errorOccurred(errorString);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientInterface::~ClientInterface() = default;
|
ClientInterface::~ClientInterface() = default;
|
||||||
|
|
||||||
void ClientInterface::sendMessage(const QString &message, bool includeCurrentFile)
|
void ClientInterface::sendMessage(
|
||||||
|
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, "", attachFiles);
|
||||||
|
|
||||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
auto providerName = Settings::generalSettings().caProvider();
|
||||||
@ -79,8 +87,7 @@ void ClientInterface::sendMessage(const QString &message, bool includeCurrentFil
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = Settings::generalSettings().caTemplate();
|
auto templateName = Settings::generalSettings().caTemplate();
|
||||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||||
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));
|
||||||
@ -88,47 +95,47 @@ void ClientInterface::sendMessage(const QString &message, bool includeCurrentFil
|
|||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData context;
|
LLMCore::ContextData context;
|
||||||
context.prefix = message;
|
|
||||||
context.suffix = "";
|
|
||||||
|
|
||||||
QString systemPrompt;
|
if (chatAssistantSettings.useSystemPrompt()) {
|
||||||
if (chatAssistantSettings.useSystemPrompt())
|
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||||
systemPrompt = chatAssistantSettings.systemPrompt();
|
if (!linkedFiles.isEmpty()) {
|
||||||
|
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||||
if (includeCurrentFile) {
|
|
||||||
QString fileContext = getCurrentFileContext();
|
|
||||||
if (!fileContext.isEmpty()) {
|
|
||||||
systemPrompt = systemPrompt.append(fileContext);
|
|
||||||
}
|
}
|
||||||
|
context.systemPrompt = systemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject providerRequest;
|
QVector<LLMCore::Message> messages;
|
||||||
providerRequest["model"] = Settings::generalSettings().caModel();
|
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||||
providerRequest["stream"] = true;
|
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content});
|
||||||
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;
|
||||||
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||||
config.providerRequest = providerRequest;
|
QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
|
||||||
config.multiLineCompletion = false;
|
: QString{"generateContent?"};
|
||||||
|
config.url = QUrl(QString("%1/models/%2:%3")
|
||||||
|
.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()}};
|
||||||
|
}
|
||||||
|
|
||||||
QJsonObject request;
|
config.apiKey = provider->apiKey();
|
||||||
request["id"] = QUuid::createUuid().toString();
|
|
||||||
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "");
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,15 +151,20 @@ void ClientInterface::cancelRequest()
|
|||||||
m_requestHandler->cancelRequest(id);
|
m_requestHandler->cancelRequest(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleLLMResponse(const QString &response,
|
void ClientInterface::handleLLMResponse(
|
||||||
const QJsonObject &request,
|
const QString &response, const QJsonObject &request, bool isComplete)
|
||||||
bool isComplete)
|
|
||||||
{
|
{
|
||||||
QString messageId = request["id"].toString();
|
const auto message = response.trimmed();
|
||||||
m_chatModel->addMessage(response.trimmed(), ChatModel::ChatRole::Assistant, messageId);
|
|
||||||
|
|
||||||
if (isComplete) {
|
if (!message.isEmpty()) {
|
||||||
LOG_MESSAGE("Message completed. Final response for message " + messageId + ": " + response);
|
QString messageId = request["id"].toString();
|
||||||
|
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
|
||||||
|
|
||||||
|
if (isComplete) {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
"Message completed. Final response for message " + messageId + ": " + response);
|
||||||
|
emit messageReceivedCompletely();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,13 +183,35 @@ 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().toString());
|
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString());
|
||||||
|
|
||||||
QString content = textDocument->document()->toPlainText();
|
QString content = textDocument->document()->toPlainText();
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toString()));
|
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toFSPathString()));
|
||||||
|
|
||||||
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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -25,6 +25,8 @@
|
|||||||
|
|
||||||
#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 {
|
||||||
|
|
||||||
@ -33,22 +35,33 @@ class ClientInterface : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit ClientInterface(
|
||||||
|
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||||
~ClientInterface();
|
~ClientInterface();
|
||||||
|
|
||||||
void sendMessage(const QString &message, bool includeCurrentFile = false);
|
void sendMessage(
|
||||||
|
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::RequestHandler *m_requestHandler;
|
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
|
LLMCore::RequestHandler *m_requestHandler;
|
||||||
|
Context::ContextManager *m_contextManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
11
ChatView/icons/attach-file-dark.svg
Normal file
11
ChatView/icons/attach-file-dark.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 869 B |
11
ChatView/icons/attach-file-light.svg
Normal file
11
ChatView/icons/attach-file-light.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 869 B |
10
ChatView/icons/close-dark.svg
Normal file
10
ChatView/icons/close-dark.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 353 B |
10
ChatView/icons/close-light.svg
Normal file
10
ChatView/icons/close-light.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 353 B |
12
ChatView/icons/link-file-dark.svg
Normal file
12
ChatView/icons/link-file-dark.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 513 B |
12
ChatView/icons/link-file-light.svg
Normal file
12
ChatView/icons/link-file-light.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 507 B |
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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
|
|
||||||
|
|
||||||
width: badgeText.implicitWidth + radius
|
implicitWidth: badgeText.implicitWidth + root.radius
|
||||||
height: badgeText.implicitHeight + 6
|
implicitHeight: badgeText.implicitHeight + 6
|
||||||
color: "lightgreen"
|
color: palette.button
|
||||||
radius: height / 2
|
radius: root.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -17,28 +17,54 @@
|
|||||||
* 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 color fontColor
|
property alias messageAttachments: attachmentsModel.model
|
||||||
property color codeBgColor
|
property string textFontFamily: Qt.application.font.family
|
||||||
property color selectionColor
|
property string codeFontFamily: {
|
||||||
|
switch (Qt.platform.os) {
|
||||||
|
case "windows":
|
||||||
|
return "Consolas";
|
||||||
|
case "osx":
|
||||||
|
return "Menlo";
|
||||||
|
case "linux":
|
||||||
|
return "DejaVu Sans Mono";
|
||||||
|
default:
|
||||||
|
return "monospace";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
property int textFontSize: Qt.application.font.pointSize
|
||||||
|
property int codeFontSize: Qt.application.font.pointSize
|
||||||
|
property int textFormat: 0
|
||||||
|
|
||||||
height: msgColumn.height
|
property bool isUserMessage: false
|
||||||
|
property int messageIndex: -1
|
||||||
|
property real listViewContentY: 0
|
||||||
|
|
||||||
|
signal resetChatToMessage(int index)
|
||||||
|
|
||||||
|
height: msgColumn.implicitHeight + 10
|
||||||
radius: 8
|
radius: 8
|
||||||
|
color: isUserMessage ? palette.alternateBase
|
||||||
|
: palette.base
|
||||||
|
|
||||||
Column {
|
HoverHandler {
|
||||||
|
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 {
|
||||||
@ -49,7 +75,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
|
||||||
|
|
||||||
width: parent.width
|
Layout.preferredWidth: root.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
|
||||||
@ -76,10 +102,72 @@ Rectangle {
|
|||||||
id: codeBlockComponent
|
id: codeBlockComponent
|
||||||
CodeBlockComponent {
|
CodeBlockComponent {
|
||||||
itemData: msgCreatorDelegate.modelData
|
itemData: msgCreatorDelegate.modelData
|
||||||
|
blockStart: root.y + msgCreatorDelegate.y
|
||||||
|
currentContentY: root.listViewContentY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
top: parent.top
|
||||||
|
}
|
||||||
|
|
||||||
|
text: qsTr("ResetTo")
|
||||||
|
visible: root.isUserMessage && mouse.hovered
|
||||||
|
onClicked: function() {
|
||||||
|
root.resetChatToMessage(root.messageIndex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component TextComponent : TextBlock {
|
component TextComponent : TextBlock {
|
||||||
@ -87,13 +175,28 @@ Rectangle {
|
|||||||
height: implicitHeight + 10
|
height: implicitHeight + 10
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
leftPadding: 10
|
leftPadding: 10
|
||||||
text: itemData.text
|
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
|
||||||
color: root.fontColor
|
: itemData.text
|
||||||
selectionColor: root.selectionColor
|
font.family: root.textFontFamily
|
||||||
|
font.pointSize: root.textFontSize
|
||||||
|
textFormat: {
|
||||||
|
if (root.textFormat == 0) {
|
||||||
|
return Text.MarkdownText
|
||||||
|
} else if (root.textFormat == 1) {
|
||||||
|
return Text.RichText
|
||||||
|
} else {
|
||||||
|
return Text.PlainText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUtils {
|
||||||
|
id: utils
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
component CodeBlockComponent : CodeBlock {
|
component CodeBlockComponent : CodeBlock {
|
||||||
|
id: codeblock
|
||||||
|
|
||||||
required property var itemData
|
required property var itemData
|
||||||
anchors {
|
anchors {
|
||||||
left: parent.left
|
left: parent.left
|
||||||
@ -104,8 +207,7 @@ Rectangle {
|
|||||||
|
|
||||||
code: itemData.text
|
code: itemData.text
|
||||||
language: itemData.language
|
language: itemData.language
|
||||||
color: root.codeBgColor
|
codeFontFamily: root.codeFontFamily
|
||||||
selectionColor: root.selectionColor
|
codeFontSize: root.codeFontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -17,28 +17,66 @@
|
|||||||
* 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: root.backgroundColor
|
color: palette.window
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors {
|
anchors.fill: parent
|
||||||
fill: parent
|
spacing: 0
|
||||||
|
|
||||||
|
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
|
||||||
@ -54,14 +92,25 @@ 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)
|
||||||
color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor
|
messageAttachments: model.attachments
|
||||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
isUserMessage: model.roleType === ChatModel.User
|
||||||
codeBgColor: root.codeColor
|
messageIndex: index
|
||||||
selectionColor: root.primaryColor.hslLightness > 0.5 ? Qt.darker(root.primaryColor, 1.5)
|
listViewContentY: chatListView.contentY
|
||||||
: Qt.lighter(root.primaryColor, 1.5)
|
textFontFamily: root.textFontFamily
|
||||||
|
codeFontFamily: root.codeFontFamily
|
||||||
|
codeFontSize: root.codeFontSize
|
||||||
|
textFontSize: root.textFontSize
|
||||||
|
textFormat: root.textFormat
|
||||||
|
|
||||||
|
onResetChatToMessage: function(index) {
|
||||||
|
messageInput.text = model.content
|
||||||
|
messageInput.cursorPosition = model.content.length
|
||||||
|
root.chatModel.resetModelTo(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header: Item {
|
header: Item {
|
||||||
@ -69,7 +118,7 @@ ChatRootView {
|
|||||||
height: 30
|
height: 30
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: QQC.ScrollBar {
|
||||||
id: scroll
|
id: scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,15 +144,28 @@ ChatRootView {
|
|||||||
id: messageInput
|
id: messageInput
|
||||||
|
|
||||||
placeholderText: qsTr("Type your message here...")
|
placeholderText: qsTr("Type your message here...")
|
||||||
placeholderTextColor: "#888"
|
placeholderTextColor: palette.mid
|
||||||
color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
color: palette.text
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
radius: 2
|
radius: 2
|
||||||
color: root.primaryColor
|
color: palette.base
|
||||||
border.color: root.primaryColor.hslLightness > 0.5 ? Qt.lighter(root.primaryColor, 1.5)
|
border.color: messageInput.activeFocus ? palette.highlight : palette.button
|
||||||
: 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()
|
||||||
@ -113,65 +175,49 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
AttachedFilesPlace {
|
||||||
|
id: attachedFilesPlace
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 5
|
attachedFilesModel: root.attachmentFiles
|
||||||
|
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||||
Button {
|
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg"
|
||||||
id: sendButton
|
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
|
||||||
|
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
text: qsTr("Send")
|
|
||||||
onClicked: root.sendChatMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
AttachedFilesPlace {
|
||||||
id: bar
|
id: linkedFilesPlace
|
||||||
|
|
||||||
layoutDirection: Qt.RightToLeft
|
Layout.fillWidth: true
|
||||||
|
attachedFilesModel: root.linkedFiles
|
||||||
anchors {
|
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||||
left: parent.left
|
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
|
||||||
leftMargin: 5
|
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
|
||||||
right: parent.right
|
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
||||||
rightMargin: scroll.width
|
|
||||||
}
|
}
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Badge {
|
BottomBar {
|
||||||
text: qsTr("tokens:%1/%2").arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
|
id: bottomBar
|
||||||
color: root.codeColor
|
|
||||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
root.chatModel.clear()
|
root.chatModel.clear()
|
||||||
|
root.clearAttachmentFiles()
|
||||||
|
root.updateInputTokensCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
@ -179,7 +225,7 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
root.sendMessage(messageInput.text, sharingCurrentFile.checked)
|
root.sendMessage(messageInput.text)
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
54
ChatView/qml/controls/QoAButton.qml
Normal file
54
ChatView/qml/controls/QoAButton.qml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -26,26 +26,33 @@ Rectangle {
|
|||||||
|
|
||||||
property string code: ""
|
property string code: ""
|
||||||
property string language: ""
|
property string language: ""
|
||||||
property color selectionColor
|
|
||||||
|
|
||||||
readonly property string monospaceFont: {
|
property real currentContentY: 0
|
||||||
switch (Qt.platform.os) {
|
property real blockStart: 0
|
||||||
case "windows":
|
|
||||||
return "Consolas";
|
property alias codeFontFamily: codeText.font.family
|
||||||
case "osx":
|
property alias codeFontSize: codeText.font.pointSize
|
||||||
return "Menlo";
|
|
||||||
case "linux":
|
readonly property real buttonTopMargin: 5
|
||||||
return "DejaVu Sans Mono";
|
readonly property real blockEnd: blockStart + root.height
|
||||||
default:
|
readonly property real maxButtonOffset: Math.max(0, root.height - copyButton.height - buttonTopMargin)
|
||||||
return "monospace";
|
|
||||||
|
readonly property real buttonPosition: {
|
||||||
|
if (currentContentY > blockEnd) {
|
||||||
|
return buttonTopMargin;
|
||||||
}
|
}
|
||||||
|
else if (currentContentY > blockStart) {
|
||||||
|
let offset = currentContentY - blockStart;
|
||||||
|
return Math.min(offset, maxButtonOffset);
|
||||||
|
}
|
||||||
|
return buttonTopMargin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
radius: 4
|
radius: 4
|
||||||
|
|
||||||
implicitWidth: parent.width
|
implicitWidth: parent.width
|
||||||
implicitHeight: codeText.implicitHeight + 20
|
implicitHeight: codeText.implicitHeight + 20
|
||||||
|
|
||||||
@ -55,17 +62,14 @@ Rectangle {
|
|||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: codeText
|
id: codeText
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 10
|
anchors.margins: 10
|
||||||
text: root.code
|
text: root.code
|
||||||
readOnly: true
|
readOnly: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
font.family: root.monospaceFont
|
|
||||||
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: root.selectionColor
|
selectionColor: palette.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
@ -77,14 +81,20 @@ Rectangle {
|
|||||||
text: root.language
|
text: root.language
|
||||||
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
||||||
: Qt.lighter(root.color, 1.1)
|
: Qt.lighter(root.color, 1.1)
|
||||||
font.pointSize: 8
|
font.pointSize: codeText.font.pointSize - 4
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
QoAButton {
|
||||||
anchors.top: parent.top
|
id: copyButton
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: 5
|
anchors {
|
||||||
text: "Copy"
|
top: parent.top
|
||||||
|
topMargin: root.buttonPosition
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: root.buttonTopMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
text: qsTr("Copy")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
utils.copyToClipboard(root.code)
|
utils.copyToClipboard(root.code)
|
||||||
text = qsTr("Copied")
|
text = qsTr("Copied")
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -25,5 +25,6 @@ TextEdit {
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
textFormat: Text.StyledText
|
selectionColor: palette.highlight
|
||||||
|
color: palette.text
|
||||||
}
|
}
|
||||||
|
109
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
109
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
98
ChatView/qml/parts/BottomBar.qml
Normal file
98
ChatView/qml/parts/BottomBar.qml
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
ChatView/qml/parts/TopBar.qml
Normal file
88
ChatView/qml/parts/TopBar.qml
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
234
CodeHandler.cpp
Normal file
234
CodeHandler.cpp
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
51
CodeHandler.hpp
Normal file
51
CodeHandler.hpp
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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 <QTimer>
|
|
||||||
#include <settings/ButtonAspect.hpp>
|
#include <settings/ButtonAspect.hpp>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include "QodeAssisttr.h"
|
#include "QodeAssisttr.h"
|
||||||
|
|
||||||
@ -35,6 +35,49 @@ 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)
|
||||||
@ -57,6 +100,21 @@ 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()
|
||||||
@ -69,13 +127,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(providersList,
|
m_generalSettings.showSelectionDialog(
|
||||||
targetSettings,
|
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
|
||||||
Tr::tr("Select LLM Provider"),
|
|
||||||
Tr::tr("Providers:"));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,14 +144,19 @@ 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()
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
: isPreset1 ? m_generalSettings.ccPreset1Provider.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 : m_generalSettings.caModel;
|
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
||||||
|
: 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()) {
|
||||||
@ -122,18 +185,23 @@ 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 ? m_templateManger.fimTemplatesNames()
|
const auto templateList = isCodeCompletion || isPreset1
|
||||||
: m_templateManger.chatTemplatesNames();
|
? m_templateManger.getFimTemplatesForProvider(providerID)
|
||||||
|
: 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(templateList,
|
m_generalSettings.showSelectionDialog(
|
||||||
targetSettings,
|
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
|
||||||
Tr::tr("Select Template"),
|
|
||||||
Tr::tr("Templates:"));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,8 +218,9 @@ void ConfigurationManager::selectUrl()
|
|||||||
urls.append(url);
|
urls.append(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
|
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
||||||
? m_generalSettings.ccUrl
|
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
||||||
|
? 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]() {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -36,6 +36,11 @@ 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();
|
||||||
|
@ -1,248 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 += getLanguageAndFileInfo();
|
|
||||||
|
|
||||||
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
|
||||||
fileContext += 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
|
|
@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -23,30 +23,56 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
#include <llmcore/RequestConfig.hpp>
|
#include "CodeHandler.hpp"
|
||||||
#include <texteditor/textdocument.h>
|
#include "context/DocumentContextReader.hpp"
|
||||||
|
#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(
|
||||||
: m_requestHandler(this)
|
const Settings::GeneralSettings &generalSettings,
|
||||||
|
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(&m_requestHandler,
|
connect(
|
||||||
&LLMCore::RequestHandler::completionReceived,
|
&m_requestHandler,
|
||||||
this,
|
&LLMCore::RequestHandler::completionReceived,
|
||||||
&LLMClientInterface::sendCompletionToClient);
|
this,
|
||||||
|
&LLMClientInterface::sendCompletionToClient);
|
||||||
|
|
||||||
|
// TODO handle error
|
||||||
|
// connect(
|
||||||
|
// &m_requestHandler,
|
||||||
|
// &LLMCore::RequestHandler::requestFinished,
|
||||||
|
// this,
|
||||||
|
// [this](const QString &, bool success, const QString &errorString) {
|
||||||
|
// if (!success) {
|
||||||
|
// emit error(errorString);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
||||||
{
|
{
|
||||||
return "Qode Assist";
|
return "QodeAssist";
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::startImpl()
|
void LLMClientInterface::startImpl()
|
||||||
@ -73,7 +99,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();
|
||||||
startTimeMeasurement(requestId);
|
m_performanceLogger.startTimeMeasurement(requestId);
|
||||||
handleCompletion(request);
|
handleCompletion(request);
|
||||||
} else if (method == "$/cancelRequest") {
|
} else if (method == "$/cancelRequest") {
|
||||||
handleCancelRequest(request);
|
handleCancelRequest(request);
|
||||||
@ -146,96 +172,192 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
|
|||||||
|
|
||||||
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||||
{
|
{
|
||||||
auto updatedContext = prepareContext(request);
|
auto filePath = Context::extractFilePathFromRequest(request);
|
||||||
auto &completeSettings = Settings::codeCompletionSettings();
|
auto documentInfo = m_documentReader.readDocument(filePath);
|
||||||
|
if (!documentInfo.document) {
|
||||||
|
LOG_MESSAGE("Error: Document is not available for" + filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto providerName = Settings::generalSettings().ccProvider();
|
auto updatedContext = prepareContext(request, documentInfo);
|
||||||
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 = Settings::generalSettings().ccTemplate();
|
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
: m_generalSettings.ccPreset1Template();
|
||||||
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::Fim;
|
config.requestType = LLMCore::RequestType::CodeCompletion;
|
||||||
config.provider = provider;
|
config.provider = provider;
|
||||||
config.promptTemplate = promptTemplate;
|
config.promptTemplate = promptTemplate;
|
||||||
config.url = QUrl(
|
// TODO refactor networking
|
||||||
QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
|
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||||
|
QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
|
||||||
config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, {"stream", true}};
|
: QString{"generateContent?"};
|
||||||
|
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
||||||
config.multiLineCompletion = completeSettings.multiLineCompletion();
|
} else {
|
||||||
|
config.url = QUrl(
|
||||||
QString systemPrompt;
|
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
|
||||||
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;
|
||||||
|
|
||||||
config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
|
QString systemPrompt;
|
||||||
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
|
if (m_completeSettings.useSystemPrompt())
|
||||||
|
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(const QJsonObject &request,
|
LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||||
const QStringView &accumulatedCompletion)
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||||
{
|
{
|
||||||
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();
|
||||||
|
|
||||||
DocumentContextReader reader(textDocument);
|
Context::DocumentContextReader
|
||||||
return reader.prepareContext(lineNumber, cursorPosition);
|
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
|
||||||
|
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
QString LLMClientInterface::endpoint(
|
||||||
const QJsonObject &request,
|
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
|
||||||
bool isComplete)
|
|
||||||
{
|
{
|
||||||
|
QString endpoint;
|
||||||
|
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
|
||||||
|
: m_generalSettings.ccEndpointMode.stringValue();
|
||||||
|
if (endpointMode == "Auto") {
|
||||||
|
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
||||||
|
: provider->chatEndpoint();
|
||||||
|
} else if (endpointMode == "Custom") {
|
||||||
|
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||||
|
: m_generalSettings.ccCustomEndpoint();
|
||||||
|
} else if (endpointMode == "FIM") {
|
||||||
|
endpoint = provider->completionEndpoint();
|
||||||
|
} else if (endpointMode == "Chat") {
|
||||||
|
endpoint = provider->chatEndpoint();
|
||||||
|
}
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||||
|
{
|
||||||
|
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;
|
||||||
QJsonObject end = position;
|
QJsonObject end = position;
|
||||||
end["character"] = position["character"].toInt() + completion.length();
|
end["character"] = position["character"].toInt() + processedCompletion.length();
|
||||||
range["end"] = end;
|
range["end"] = end;
|
||||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||||
@ -248,37 +370,13 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
|||||||
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(QString("Full response: \n%1")
|
LOG_MESSAGE(
|
||||||
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
QString("Full response: \n%1")
|
||||||
|
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
||||||
|
|
||||||
QString requestId = request["id"].toString();
|
QString requestId = request["id"].toString();
|
||||||
endTimeMeasurement(requestId);
|
m_performanceLogger.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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -22,8 +22,16 @@
|
|||||||
#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;
|
||||||
@ -35,20 +43,29 @@ 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(const QString &completion,
|
void sendCompletionToClient(
|
||||||
const QJsonObject &request,
|
const QString &completion, const QJsonObject &request, bool isComplete);
|
||||||
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);
|
||||||
@ -58,16 +75,19 @@ private:
|
|||||||
void handleExit(const QJsonObject &request);
|
void handleExit(const QJsonObject &request);
|
||||||
void handleCancelRequest(const QJsonObject &request);
|
void handleCancelRequest(const QJsonObject &request);
|
||||||
|
|
||||||
LLMCore::ContextData prepareContext(const QJsonObject &request,
|
LLMCore::ContextData prepareContext(
|
||||||
const QStringView &accumulatedCompletion = QString{});
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||||
|
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
|
||||||
|
|
||||||
LLMCore::RequestHandler m_requestHandler;
|
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||||
|
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;
|
||||||
QMap<QString, qint64> m_requestStartTimes;
|
Context::ContextManager *m_contextManager;
|
||||||
|
|
||||||
void startTimeMeasurement(const QString &requestId);
|
|
||||||
void endTimeMeasurement(const QString &requestId);
|
|
||||||
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
* 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
|
||||||
@ -18,107 +23,173 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#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(const Completion &completion, QTextDocument *origin)
|
QString mergeWithRightText(const QString &suggestion, const QString &rightText)
|
||||||
: m_completion(completion)
|
|
||||||
, m_linesCount(0)
|
|
||||||
{
|
{
|
||||||
int startPos = completion.range().start().toPositionInDocument(origin);
|
if (suggestion.isEmpty() || rightText.isEmpty()) {
|
||||||
int endPos = completion.range().end().toPositionInDocument(origin);
|
return suggestion;
|
||||||
|
}
|
||||||
|
|
||||||
startPos = qBound(0, startPos, origin->characterCount() - 1);
|
int j = 0;
|
||||||
endPos = qBound(startPos, endPos, origin->characterCount() - 1);
|
QString processed = rightText;
|
||||||
|
QSet<int> matchedPositions;
|
||||||
|
|
||||||
m_start = QTextCursor(origin);
|
for (int i = 0; i < suggestion.length() && j < processed.length(); ++i) {
|
||||||
m_start.setPosition(startPos);
|
if (suggestion[i] == processed[j]) {
|
||||||
m_start.setKeepPositionOnInsert(true);
|
matchedPositions.insert(j);
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QTextCursor cursor(origin);
|
if (matchedPositions.isEmpty()) {
|
||||||
|
return suggestion + rightText;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<int> positions = matchedPositions.values();
|
||||||
|
std::sort(positions.begin(), positions.end(), std::greater<int>());
|
||||||
|
for (int pos : positions) {
|
||||||
|
processed.remove(pos, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
LLMSuggestion::LLMSuggestion(
|
||||||
|
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
|
||||||
|
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
|
||||||
|
{
|
||||||
|
const auto &data = suggestions[currentCompletion];
|
||||||
|
|
||||||
|
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
|
||||||
|
int endPos = data.range.end.toPositionInDocument(sourceDocument);
|
||||||
|
|
||||||
|
startPos = qBound(0, startPos, sourceDocument->characterCount());
|
||||||
|
endPos = qBound(startPos, endPos, sourceDocument->characterCount());
|
||||||
|
|
||||||
|
QTextCursor cursor(sourceDocument);
|
||||||
cursor.setPosition(startPos);
|
cursor.setPosition(startPos);
|
||||||
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
|
||||||
|
|
||||||
QTextBlock block = cursor.block();
|
QTextBlock block = cursor.block();
|
||||||
QString blockText = block.text();
|
QString blockText = block.text();
|
||||||
|
|
||||||
int startPosInBlock = startPos - block.position();
|
int cursorPositionInBlock = cursor.positionInBlock();
|
||||||
int endPosInBlock = endPos - block.position();
|
|
||||||
|
|
||||||
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, completion.text());
|
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||||
|
|
||||||
document()->setPlainText(blockText);
|
if (!data.text.contains('\n')) {
|
||||||
|
QString processedRightText = mergeWithRightText(data.text, rightText);
|
||||||
setCurrentPosition(m_start.position());
|
processedRightText = processedRightText.mid(data.text.length());
|
||||||
}
|
QString displayText = blockText.left(cursorPositionInBlock) + data.text
|
||||||
|
+ processedRightText;
|
||||||
bool LLMSuggestion::apply()
|
replacementDocument()->setPlainText(displayText);
|
||||||
{
|
} else {
|
||||||
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
|
QString displayText = blockText.left(cursorPositionInBlock) + data.text;
|
||||||
cursor.beginEditBlock();
|
replacementDocument()->setPlainText(displayText);
|
||||||
cursor.removeSelectedText();
|
}
|
||||||
cursor.insertText(m_completion.text());
|
|
||||||
cursor.endEditBlock();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
|
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
|
||||||
{
|
{
|
||||||
return applyNextLine(widget);
|
return applyPart(Word, widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LLMSuggestion::applyNextLine(TextEditor::TextEditorWidget *widget)
|
bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
|
||||||
{
|
{
|
||||||
const QString text = m_completion.text();
|
return applyPart(Line, widget);
|
||||||
QStringList lines = text.split('\n');
|
|
||||||
|
|
||||||
if (m_linesCount < lines.size())
|
|
||||||
m_linesCount++;
|
|
||||||
|
|
||||||
showTooltip(widget, m_linesCount);
|
|
||||||
|
|
||||||
return m_linesCount == lines.size() && !Utils::ToolTip::isVisible();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMSuggestion::onCounterFinished(int count)
|
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||||
{
|
{
|
||||||
Utils::ToolTip::hide();
|
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||||
m_linesCount = 0;
|
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||||
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
|
QTextCursor currentCursor = widget->textCursor();
|
||||||
cursor.beginEditBlock();
|
const QString text = suggestions()[currentSuggestion()].text;
|
||||||
cursor.removeSelectedText();
|
|
||||||
|
|
||||||
QStringList lines = m_completion.text().split('\n');
|
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
|
||||||
QString textToInsert = lines.mid(0, count).join('\n');
|
+ (cursor.selectionEnd() - cursor.selectionStart());
|
||||||
|
|
||||||
cursor.insertText(textToInsert);
|
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
|
||||||
cursor.endEditBlock();
|
|
||||||
|
if (next == -1) {
|
||||||
|
if (part == Line) {
|
||||||
|
next = text.length();
|
||||||
|
} else {
|
||||||
|
return apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part == Line)
|
||||||
|
++next;
|
||||||
|
|
||||||
|
QString subText = text.mid(startPos, next - startPos);
|
||||||
|
|
||||||
|
if (subText.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextBlock currentBlock = currentCursor.block();
|
||||||
|
QString textAfterCursor = currentBlock.text().mid(currentCursor.positionInBlock());
|
||||||
|
|
||||||
|
if (!subText.contains('\n')) {
|
||||||
|
QTextCursor deleteCursor = currentCursor;
|
||||||
|
deleteCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
deleteCursor.removeSelectedText();
|
||||||
|
|
||||||
|
QString mergedText = mergeWithRightText(subText, textAfterCursor);
|
||||||
|
currentCursor.insertText(mergedText);
|
||||||
|
} else {
|
||||||
|
currentCursor.insertText(subText);
|
||||||
|
|
||||||
|
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
|
||||||
|
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
|
||||||
|
if (!newCompletionText.isEmpty()) {
|
||||||
|
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
|
||||||
|
const Utils::Text::Position newEnd{newStart.line, int(newCompletionText.length())};
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMSuggestion::reset()
|
bool LLMSuggestion::apply()
|
||||||
{
|
{
|
||||||
m_start.removeSelectedText();
|
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||||
}
|
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||||
|
const QString text = suggestions()[currentSuggestion()].text;
|
||||||
|
|
||||||
int LLMSuggestion::position()
|
QTextBlock currentBlock = cursor.block();
|
||||||
{
|
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||||
return m_start.position();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count)
|
QTextCursor editCursor = cursor;
|
||||||
{
|
|
||||||
Utils::ToolTip::hide();
|
int firstLineEnd = text.indexOf('\n');
|
||||||
QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight());
|
if (firstLineEnd != -1) {
|
||||||
pos += QPoint(-10, -50);
|
QString firstLine = text.left(firstLineEnd);
|
||||||
m_counterTooltip = new CounterTooltip(count);
|
QString restOfText = text.mid(firstLineEnd);
|
||||||
Utils::ToolTip::show(pos, m_counterTooltip, widget);
|
|
||||||
connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished);
|
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
|
||||||
|
QString mergedFirstLine = mergeWithRightText(firstLine, textAfterCursor);
|
||||||
|
editCursor.insertText(mergedFirstLine + restOfText);
|
||||||
|
} else {
|
||||||
|
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
|
||||||
|
QString mergedText = mergeWithRightText(text, textAfterCursor);
|
||||||
|
editCursor.insertText(mergedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
* 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
|
||||||
@ -19,37 +24,22 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <texteditor/texteditor.h>
|
||||||
#include "LSPCompletion.hpp"
|
#include <texteditor/textsuggestion.h>
|
||||||
#include <texteditor/textdocumentlayout.h>
|
|
||||||
|
|
||||||
#include "utils/CounterTooltip.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion
|
class LLMSuggestion : public TextEditor::CyclicSuggestion
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
|
||||||
public:
|
public:
|
||||||
LLMSuggestion(const Completion &completion, QTextDocument *origin);
|
enum Part { Word, Line };
|
||||||
|
|
||||||
bool apply() final;
|
LLMSuggestion(
|
||||||
bool applyWord(TextEditor::TextEditorWidget *widget) final;
|
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
|
||||||
bool applyNextLine(TextEditor::TextEditorWidget *widget);
|
|
||||||
void reset() final;
|
|
||||||
int position() final;
|
|
||||||
|
|
||||||
const Completion &completion() const { return m_completion; }
|
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
||||||
|
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
||||||
void showTooltip(TextEditor::TextEditorWidget *widget, int count);
|
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||||
void onCounterFinished(int count);
|
bool apply() override;
|
||||||
|
|
||||||
private:
|
|
||||||
Completion m_completion;
|
|
||||||
QTextCursor m_start;
|
|
||||||
int m_linesCount;
|
|
||||||
|
|
||||||
CounterTooltip *m_counterTooltip = nullptr;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* 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.
|
||||||
*
|
*
|
||||||
@ -68,9 +68,10 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
|
|||||||
public:
|
public:
|
||||||
static constexpr LanguageServerProtocol::Key docKey{"doc"};
|
static constexpr LanguageServerProtocol::Key docKey{"doc"};
|
||||||
|
|
||||||
GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
|
GetCompletionParams(
|
||||||
int version,
|
const LanguageServerProtocol::TextDocumentIdentifier &document,
|
||||||
const LanguageServerProtocol::Position &position)
|
int version,
|
||||||
|
const LanguageServerProtocol::Position &position)
|
||||||
{
|
{
|
||||||
setTextDocument(document);
|
setTextDocument(document);
|
||||||
setVersion(version);
|
setVersion(version);
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.3.9",
|
"Version" : "0.5.13",
|
||||||
"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" : "GNU General Public License Usage
|
"License" : "GPLv3",
|
||||||
|
"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}
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,11 @@
|
|||||||
<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>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of Qode Assist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
* 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,16 +24,20 @@
|
|||||||
|
|
||||||
#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;
|
||||||
@ -43,11 +47,12 @@ using namespace Core;
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
QodeAssistClient::QodeAssistClient()
|
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||||
: LanguageClient::Client(new LLMClientInterface())
|
: LanguageClient::Client(clientInterface)
|
||||||
|
, m_llmClient(clientInterface)
|
||||||
, m_recentCharCount(0)
|
, m_recentCharCount(0)
|
||||||
{
|
{
|
||||||
setName("Qode Assist");
|
setName("QodeAssist");
|
||||||
LanguageClient::LanguageFilter filter;
|
LanguageClient::LanguageFilter filter;
|
||||||
filter.mimeTypes = QStringList() << "*";
|
filter.mimeTypes = QStringList() << "*";
|
||||||
setSupportedLanguage(filter);
|
setSupportedLanguage(filter);
|
||||||
@ -70,48 +75,70 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Client::openDocument(document);
|
Client::openDocument(document);
|
||||||
connect(document,
|
connect(
|
||||||
&TextDocument::contentsChangedWithPosition,
|
document,
|
||||||
this,
|
&TextDocument::contentsChangedWithPosition,
|
||||||
[this, document](int position, int charsRemoved, int charsAdded) {
|
this,
|
||||||
Q_UNUSED(charsRemoved)
|
[this, document](int position, int charsRemoved, int charsAdded) {
|
||||||
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())
|
||||||
ChangesManager::instance().addChange(document,
|
Context::ChangesManager::instance()
|
||||||
position,
|
.addChange(document, position, charsRemoved, charsAdded);
|
||||||
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;
|
|
||||||
|
|
||||||
m_recentCharCount += charsAdded;
|
const int cursorPosition = widget->textCursor().position();
|
||||||
|
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
||||||
|
return;
|
||||||
|
|
||||||
if (m_typingTimer.elapsed()
|
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||||
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
m_recentCharCount = 0;
|
||||||
m_recentCharCount = charsAdded;
|
m_typingTimer.restart();
|
||||||
m_typingTimer.restart();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_recentCharCount
|
QTextCursor cursor = widget->textCursor();
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||||
scheduleRequest(widget);
|
QString lastChar = cursor.selectedText();
|
||||||
}
|
|
||||||
});
|
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)
|
||||||
@ -126,14 +153,26 @@ 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{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
|
GetCompletionRequest request{
|
||||||
documentVersion(filePath),
|
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
|
||||||
Position(cursor.mainCursor())}};
|
documentVersion(filePath),
|
||||||
|
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);
|
||||||
@ -143,6 +182,35 @@ 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);
|
||||||
@ -172,8 +240,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(const GetCompletionRequest::Response &response,
|
void QodeAssistClient::handleCompletions(
|
||||||
TextEditor::TextEditorWidget *editor)
|
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
if (response.error())
|
if (response.error())
|
||||||
log(*response.error());
|
log(*response.error());
|
||||||
@ -193,8 +261,8 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
|||||||
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 = Utils::filtered(result->completions().toListOrEmpty(),
|
QList<Completion> completions
|
||||||
isValidCompletion);
|
= Utils::filtered(result->completions().toListOrEmpty(), 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) {
|
||||||
@ -211,10 +279,19 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
|||||||
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(
|
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||||
std::make_unique<LLMSuggestion>(completions.first(), editor->document()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,13 +300,18 @@ 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
|
||||||
{
|
{
|
||||||
return Settings::generalSettings().enableQodeAssist();
|
if (!project)
|
||||||
|
return Settings::generalSettings().enableQodeAssist();
|
||||||
|
|
||||||
|
Settings::ProjectSettings settings(project);
|
||||||
|
return settings.isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::setupConnections()
|
void QodeAssistClient::setupConnections()
|
||||||
@ -239,18 +321,13 @@ void QodeAssistClient::setupConnections()
|
|||||||
openDocument(textDocument);
|
openDocument(textDocument);
|
||||||
};
|
};
|
||||||
|
|
||||||
m_documentOpenedConnection = connect(EditorManager::instance(),
|
m_documentOpenedConnection
|
||||||
&EditorManager::documentOpened,
|
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc);
|
||||||
this,
|
m_documentClosedConnection = connect(
|
||||||
openDoc);
|
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) {
|
||||||
m_documentClosedConnection = connect(EditorManager::instance(),
|
if (auto textDocument = qobject_cast<TextDocument *>(document))
|
||||||
&EditorManager::documentClosed,
|
closeDocument(textDocument);
|
||||||
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);
|
||||||
@ -265,4 +342,32 @@ 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
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of Qode Assist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
* 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,32 +24,40 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <languageclient/client.h>
|
#include "LLMClientInterface.hpp"
|
||||||
|
|
||||||
#include "LSPCompletion.hpp"
|
#include "LSPCompletion.hpp"
|
||||||
|
#include "QuickRefactorHandler.hpp"
|
||||||
|
#include "widgets/CompletionProgressHandler.hpp"
|
||||||
|
#include "widgets/EditorChatButtonHandler.hpp"
|
||||||
|
#include <languageclient/client.h>
|
||||||
|
#include <llmcore/IPromptProvider.hpp>
|
||||||
|
#include <llmcore/IProviderRegistry.hpp>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
class QodeAssistClient : public LanguageClient::Client
|
class QodeAssistClient : public LanguageClient::Client
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit QodeAssistClient();
|
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||||
~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(const GetCompletionRequest::Response &response,
|
void handleCompletions(
|
||||||
TextEditor::TextEditorWidget *editor);
|
const GetCompletionRequest::Response &response, 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;
|
||||||
@ -58,6 +66,10 @@ 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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
293
QuickRefactorHandler.cpp
Normal file
293
QuickRefactorHandler.cpp
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
77
QuickRefactorHandler.hpp
Normal file
77
QuickRefactorHandler.hpp
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
307
README.md
307
README.md
@ -1,45 +1,80 @@
|
|||||||
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
||||||
[](https://discord.gg/DGgMtTteAD)
|
|
||||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
[](https://discord.gg/BGMkUsXUgf)
|
||||||
|
|
||||||
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. [Installation](#installation)
|
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
|
||||||
3. [Configure Plugin](#configure-plugin)
|
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
|
||||||
4. [Supported LLM Providers](#supported-llm-providers)
|
4. [Configure for OpenAI](#configure-for-openai)
|
||||||
5. [Recommended Models](#recommended-models)
|
5. [Configure for Mistral AI](#configure-for-mistral-ai)
|
||||||
- [Ollama](#ollama)
|
6. [Configure for Google AI](#configure-for-google-ai)
|
||||||
- [LM Studio](#lm-studio)
|
7. [Configure for Ollama](#configure-for-ollama)
|
||||||
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
8. [Configure for llama.cpp](#configure-for-llamacpp)
|
||||||
7. [Development Progress](#development-progress)
|
9. [System Prompt Configuration](#system-prompt-configuration)
|
||||||
8. [Hotkeys](#hotkeys)
|
10. [File Context Feature](#file-context-feature)
|
||||||
9. [Troubleshooting](#troubleshooting)
|
11. [Quick Refactoring Feature](#quick-refactoring-feature)
|
||||||
10. [Support the Development](#support-the-development-of-qodeassist)
|
12. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||||
11. [How to Build](#how-to-build)
|
13. [Development Progress](#development-progress)
|
||||||
|
14. [Hotkeys](#hotkeys)
|
||||||
|
15. [Ignoring Files](#ignoring-files)
|
||||||
|
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
|
||||||
- LM Studio (experimental)
|
- llama.cpp
|
||||||
- OpenAI-compatible providers (experimental)
|
- OpenAI
|
||||||
|
- Anthropic Claude
|
||||||
|
- LM Studio
|
||||||
|
- Mistral 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">
|
||||||
@ -50,11 +85,80 @@ QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides
|
|||||||
<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>
|
||||||
|
|
||||||
## Installation
|
<details>
|
||||||
|
<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>
|
||||||
|
|
||||||
1. Install Latest QtCreator
|
## Install plugin to QtCreator
|
||||||
2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
1. Install Latest Qt Creator
|
||||||
3. Install a language models in Ollama via terminal. For example, you can run:
|
2. Download the QodeAssist plugin for your Qt Creator
|
||||||
|
- 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):
|
||||||
```
|
```
|
||||||
@ -68,60 +172,90 @@ 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 "Qode Assist" tab
|
2. Navigate to the "QodeAssist" 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
|
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
|
||||||
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>
|
||||||
|
|
||||||
## Supported LLM Providers
|
## Configure for llama.cpp
|
||||||
QodeAssist currently supports the following LLM (Large Language Model) providers:
|
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||||
- [Ollama](https://ollama.com)
|
2. Go to General tab and configure:
|
||||||
- [LM Studio](https://lmstudio.ai) (experimental)
|
- Set "llama.cpp" as the provider for code completion or/and chat assistant
|
||||||
- OpenAI compatible providers (experimental)
|
- Set the llama.cpp URL (e.g. http://localhost:8080)
|
||||||
|
- 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>
|
||||||
|
|
||||||
## Recommended Models:
|
## System Prompt Configuration
|
||||||
QodeAssist has been thoroughly tested and optimized for use with the following language models:
|
|
||||||
|
|
||||||
- Qwen2.5-coder
|
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.
|
||||||
- CodeLlama
|
|
||||||
- StarCoder2
|
|
||||||
- DeepSeek-Coder-V2
|
|
||||||
|
|
||||||
### Ollama:
|
## File Context Feature
|
||||||
### For autocomplete(FIM)
|
|
||||||
```
|
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.
|
||||||
ollama run codellama:7b-code
|
|
||||||
ollama run starcoder2:7b
|
### Attached Files
|
||||||
ollama run qwen2.5-coder:7b-base
|
|
||||||
ollama run deepseek-coder-v2:16b-lite-base-q3_K_M
|
Attachments are designed for one-time code analysis and specific queries:
|
||||||
```
|
- Files are included only in the current message
|
||||||
### For chat
|
- Content is discarded after the message is processed
|
||||||
```
|
- Ideal for:
|
||||||
ollama run codellama:7b-instruct
|
- Getting specific feedback on code changes
|
||||||
ollama run starcoder2:instruct
|
- Code review requests
|
||||||
ollama run qwen2.5-coder:7b-instruct
|
- Analyzing isolated code segments
|
||||||
ollama run deepseek-coder-v2
|
- 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
|
||||||
|
|
||||||
|
## Quick Refactoring Feature
|
||||||
|
### Setup
|
||||||
|
Since this is actually a small chat with redirected output, the main settings of the provider, model and template are taken from the chat settings
|
||||||
|
### Using
|
||||||
|
The request to model consist of instructions to model, selection code and cursor position
|
||||||
|
The default instruction is: "Refactor the code to improve its quality and maintainability." and sending if text field is empty
|
||||||
|
Also there buttons to quick call instractions:
|
||||||
|
* Repeat latest instruction, will activate after sending first request in QtCreator session
|
||||||
|
* Improve current selection code
|
||||||
|
* Suggestion alternative variant of selection code
|
||||||
|
* Other instructions[TBD]
|
||||||
|
|
||||||
## QtCreator Version Compatibility
|
## QtCreator Version Compatibility
|
||||||
|
|
||||||
|
- QtCreator 16.0.2 - 0.5.13 - 0.x.x
|
||||||
|
- 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
|
||||||
|
|
||||||
@ -133,16 +267,44 @@ ollama run deepseek-coder-v2
|
|||||||
- [x] Sharing diff with model
|
- [x] Sharing diff with model
|
||||||
- [ ] Sharing project source with model
|
- [ ] Sharing project source with model
|
||||||
- [ ] Support for more providers and models
|
- [ ] Support for more providers and models
|
||||||
|
- [ ] Support MCP
|
||||||
|
|
||||||
## Hotkeys
|
## Hotkeys
|
||||||
|
|
||||||
- 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 line by line, you can use the "Move cursor word right" shortcut:
|
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
|
||||||
- On Mac: Option + Right Arrow
|
- To call Quick Refactor dialog, select some code or place cursor and press
|
||||||
- On Windows: Alt + Right Arrow
|
- on Mac: Option + Command + R
|
||||||
|
- on Windows: Ctrl + Alt + R
|
||||||
|
- on Linux with KDE Plasma: Ctrl + Alt + R
|
||||||
|
|
||||||
|
## Ignoring Files
|
||||||
|
QodeAssist supports the ability to ignore files in context using a .qodeassistignore file. This allows you to exclude specific files from the context during code completion and in the chat assistant, which is especially useful for large projects.
|
||||||
|
|
||||||
|
### How to Use .qodeassistignore
|
||||||
|
- Create a .qodeassistignore file in the root directory of your project near CMakeLists.txt or pro.
|
||||||
|
- Add patterns for files and directories that should be excluded from the context.
|
||||||
|
- QodeAssist will automatically detect this file and apply the exclusion rules.
|
||||||
|
|
||||||
|
### .qodeassistignore File Format
|
||||||
|
The file format is similar to .gitignore:
|
||||||
|
- Each pattern is written on a separate line
|
||||||
|
- Empty lines are ignored
|
||||||
|
- Lines starting with # are considered comments
|
||||||
|
- Standard wildcards work the same as in .gitignore
|
||||||
|
- To negate a pattern, use ! at the beginning of the line
|
||||||
|
```
|
||||||
|
# Ignore all files in the build directory
|
||||||
|
/build
|
||||||
|
*.tmp
|
||||||
|
# Ignore a specific file
|
||||||
|
src/generated/autogen.cpp
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@ -153,20 +315,17 @@ 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. Check the endpoint:
|
2. Confirm that the selected model and template are compatible:
|
||||||
|
|
||||||
Make sure the endpoint in the settings matches the one required by your provider
|
Ensure you've chosen the correct model in the "Select Models" option
|
||||||
- For Ollama, it should be /api/generate
|
Verify that the selected prompt template matches the model you're using
|
||||||
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
|
|
||||||
|
|
||||||
3. Confirm that the selected model and template are compatible:
|
3. On Linux the prebuilt binaries support only ubuntu 22.04+ or simililliar os.
|
||||||
|
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 "Qode Assist" tab
|
2. Navigate to the "QodeAssist" 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
|
||||||
@ -205,3 +364,7 @@ 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
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
72
UpdateStatusWidget.cpp
Normal file
72
UpdateStatusWidget.cpp
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
47
UpdateStatusWidget.hpp
Normal file
47
UpdateStatusWidget.hpp
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QFrame>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class UpdateStatusWidget : public QFrame
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit UpdateStatusWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setDefaultAction(QAction *action);
|
||||||
|
void showUpdateAvailable(const QString &version);
|
||||||
|
void hideUpdateInfo();
|
||||||
|
|
||||||
|
QPushButton *updateButton() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QToolButton *m_actionButton;
|
||||||
|
QLabel *m_versionLabel;
|
||||||
|
QPushButton *m_updateButton;
|
||||||
|
};
|
||||||
|
} // namespace QodeAssist
|
28
Version.hpp
Normal file
28
Version.hpp
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -23,16 +23,15 @@
|
|||||||
|
|
||||||
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()
|
||||||
{
|
{
|
||||||
@ -42,4 +41,4 @@ Core::NavigationView NavigationPanel::createWidget()
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
} // namespace QodeAssist::Chat
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <coreplugin/inavigationwidgetfactory.h>
|
#include <coreplugin/inavigationwidgetfactory.h>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@ -34,4 +34,4 @@ public:
|
|||||||
Core::NavigationView createWidget() override;
|
Core::NavigationView createWidget() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
} // namespace QodeAssist::Chat
|
||||||
|
26
context/CMakeLists.txt
Normal file
26
context/CMakeLists.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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})
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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 "settings/CodeCompletionSettings.hpp"
|
#include "CodeCompletionSettings.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
ChangesManager &ChangesManager::instance()
|
ChangesManager &ChangesManager::instance()
|
||||||
{
|
{
|
||||||
@ -30,17 +30,12 @@ ChangesManager &ChangesManager::instance()
|
|||||||
|
|
||||||
ChangesManager::ChangesManager()
|
ChangesManager::ChangesManager()
|
||||||
: QObject(nullptr)
|
: QObject(nullptr)
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
|
|
||||||
ChangesManager::~ChangesManager()
|
ChangesManager::~ChangesManager() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChangesManager::addChange(TextEditor::TextDocument *document,
|
void ChangesManager::addChange(
|
||||||
int position,
|
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded)
|
||||||
int charsRemoved,
|
|
||||||
int charsAdded)
|
|
||||||
{
|
{
|
||||||
auto &documentQueue = m_documentChanges[document];
|
auto &documentQueue = m_documentChanges[document];
|
||||||
|
|
||||||
@ -51,9 +46,10 @@ void ChangesManager::addChange(TextEditor::TextDocument *document,
|
|||||||
|
|
||||||
ChangeInfo change{fileName, lineNumber, lineContent};
|
ChangeInfo change{fileName, lineNumber, lineContent};
|
||||||
|
|
||||||
auto it = std::find_if(documentQueue.begin(),
|
auto it
|
||||||
documentQueue.end(),
|
= std::find_if(documentQueue.begin(), documentQueue.end(), [lineNumber](const ChangeInfo &c) {
|
||||||
[lineNumber](const ChangeInfo &c) { return c.lineNumber == lineNumber; });
|
return c.lineNumber == lineNumber;
|
||||||
|
});
|
||||||
|
|
||||||
if (it != documentQueue.end()) {
|
if (it != documentQueue.end()) {
|
||||||
it->lineContent = lineContent;
|
it->lineContent = lineContent;
|
||||||
@ -79,4 +75,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist::Context
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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 {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
class ChangesManager : public QObject
|
class ChangesManager : public QObject
|
||||||
{
|
{
|
||||||
@ -41,10 +41,8 @@ public:
|
|||||||
|
|
||||||
static ChangesManager &instance();
|
static ChangesManager &instance();
|
||||||
|
|
||||||
void addChange(TextEditor::TextDocument *document,
|
void addChange(
|
||||||
int position,
|
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
|
||||||
int charsRemoved,
|
|
||||||
int charsAdded);
|
|
||||||
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
|
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -58,4 +56,4 @@ private:
|
|||||||
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
|
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist::Context
|
32
context/ContentFile.hpp
Normal file
32
context/ContentFile.hpp
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
185
context/ContextManager.cpp
Normal file
185
context/ContextManager.cpp
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
60
context/ContextManager.hpp
Normal file
60
context/ContextManager.hpp
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
299
context/DocumentContextReader.cpp
Normal file
299
context/DocumentContextReader.cpp
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
92
context/DocumentContextReader.hpp
Normal file
92
context/DocumentContextReader.hpp
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
45
context/DocumentReaderQtCreator.hpp
Normal file
45
context/DocumentReaderQtCreator.hpp
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
49
context/IContextManager.hpp
Normal file
49
context/IContextManager.hpp
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
41
context/IDocumentReader.hpp
Normal file
41
context/IDocumentReader.hpp
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
274
context/IgnoreManager.cpp
Normal file
274
context/IgnoreManager.cpp
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
62
context/IgnoreManager.hpp
Normal file
62
context/IgnoreManager.hpp
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
70
context/ProgrammingLanguage.cpp
Normal file
70
context/ProgrammingLanguage.cpp
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
43
context/ProgrammingLanguage.hpp
Normal file
43
context/ProgrammingLanguage.hpp
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
54
context/TokenUtils.cpp
Normal file
54
context/TokenUtils.cpp
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
36
context/TokenUtils.hpp
Normal file
36
context/TokenUtils.hpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
34
context/Utils.hpp
Normal file
34
context/Utils.hpp
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
@ -3,10 +3,19 @@ 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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -20,14 +20,37 @@
|
|||||||
#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
|
||||||
{
|
{
|
||||||
QString prefix;
|
std::optional<QString> systemPrompt;
|
||||||
QString suffix;
|
std::optional<QString> prefix;
|
||||||
QString fileContext;
|
std::optional<QString> suffix;
|
||||||
|
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
|
||||||
|
39
llmcore/IPromptProvider.hpp
Normal file
39
llmcore/IPromptProvider.hpp
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
36
llmcore/IProviderRegistry.hpp
Normal file
36
llmcore/IProviderRegistry.hpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
102
llmcore/OllamaMessage.cpp
Normal file
102
llmcore/OllamaMessage.cpp
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <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
|
71
llmcore/OllamaMessage.hpp
Normal file
71
llmcore/OllamaMessage.hpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
82
llmcore/OpenAIMessage.cpp
Normal file
82
llmcore/OpenAIMessage.cpp
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
56
llmcore/OpenAIMessage.hpp
Normal file
56
llmcore/OpenAIMessage.hpp
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
53
llmcore/PromptProviderChat.hpp
Normal file
53
llmcore/PromptProviderChat.hpp
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
52
llmcore/PromptProviderFim.hpp
Normal file
52
llmcore/PromptProviderFim.hpp
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -24,10 +24,11 @@
|
|||||||
#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
|
||||||
{
|
{
|
||||||
@ -35,8 +36,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 bool isSupportProvider(ProviderID id) const = 0;
|
||||||
};
|
};
|
||||||
} // namespace QodeAssist::LLMCore
|
} // namespace QodeAssist::LLMCore
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -37,19 +37,48 @@ 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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -35,13 +35,11 @@ public:
|
|||||||
template<typename T>
|
template<typename T>
|
||||||
void registerTemplate()
|
void registerTemplate()
|
||||||
{
|
{
|
||||||
static_assert(std::is_base_of<PromptTemplate, T>::value,
|
static_assert(std::is_base_of<PromptTemplate, T>::value, "T must inherit from PromptTemplate");
|
||||||
"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();
|
||||||
if (template_ptr->type() == TemplateType::Fim) {
|
m_fimTemplates[name] = template_ptr;
|
||||||
m_fimTemplates[name] = template_ptr;
|
if (template_ptr->type() == TemplateType::Chat) {
|
||||||
} else if (template_ptr->type() == TemplateType::Chat) {
|
|
||||||
m_chatTemplates[name] = template_ptr;
|
m_chatTemplates[name] = template_ptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,6 +50,9 @@ 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;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -19,9 +19,13 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
#include "RequestType.hpp"
|
|
||||||
#include <utils/environment.h>
|
#include <utils/environment.h>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "ContextData.hpp"
|
||||||
|
#include "PromptTemplate.hpp"
|
||||||
|
#include "RequestType.hpp"
|
||||||
|
|
||||||
class QNetworkReply;
|
class QNetworkReply;
|
||||||
class QJsonObject;
|
class QJsonObject;
|
||||||
@ -38,10 +42,18 @@ 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(
|
||||||
virtual void prepareRequest(QJsonObject &request, RequestType type) = 0;
|
QJsonObject &request,
|
||||||
|
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
|
||||||
|
34
llmcore/ProviderID.hpp
Normal file
34
llmcore/ProviderID.hpp
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
};
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@ -39,6 +39,8 @@ 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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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
|
class ProvidersManager : public IProviderRegistry
|
||||||
{
|
{
|
||||||
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);
|
Provider *getProviderByName(const QString &providerName) override;
|
||||||
|
|
||||||
QStringList providersNames() const;
|
QStringList providersNames() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ProvidersManager() = default;
|
ProvidersManager() = default;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2025 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 {
|
||||||
|
|
||||||
@ -35,6 +35,7 @@ struct LLMConfig
|
|||||||
QJsonObject providerRequest;
|
QJsonObject providerRequest;
|
||||||
RequestType requestType;
|
RequestType requestType;
|
||||||
bool multiLineCompletion;
|
bool multiLineCompletion;
|
||||||
|
QString apiKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::LLMCore
|
} // namespace QodeAssist::LLMCore
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user