mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-06-04 01:28:58 -04:00
Compare commits
277 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 | ||
|
6503887091 | ||
|
50087aa744 | ||
|
4f2dc0c450 | ||
|
80fe388bdd | ||
|
8375d85f7d | ||
|
54b2cc7011 | ||
|
f209cb75a2 | ||
|
5e813ba402 | ||
|
7af8fc2ddc | ||
|
46300f7635 | ||
|
0a1c941d8b | ||
|
f86182408d | ||
|
252db4c5f7 | ||
|
e5af3a2884 | ||
|
bb543d1f40 | ||
|
a184916d7b | ||
|
7ad8ddfee4 | ||
|
0ed6fb4a6b | ||
|
00fce5db99 | ||
|
251a9bae03 | ||
|
3dba9d7abe | ||
|
45b0f3f18e | ||
|
4432d4019d | ||
|
f679d76d43 | ||
|
29f94561ef | ||
|
cd6c766ed2 | ||
|
6d3bc362b3 | ||
|
87393b681f | ||
|
5d496fee58 | ||
|
9902623ba0 | ||
|
61f1f0ae4f | ||
|
bc93bce03b | ||
|
85d039cbd5 | ||
|
2acaef553d | ||
|
b141e54e3e | ||
|
1ec6098210 | ||
|
9c945f066b | ||
|
4a82e9c046 | ||
|
838d69623c | ||
|
693e429bdd | ||
|
496d8feb66 | ||
|
40a568ebd9 | ||
|
5b43eb4fd2 | ||
|
9c2516cd4c | ||
|
2257e6e45f | ||
|
80eda8c167 | ||
|
3db2691114 |
108
.clang-format
Normal file
108
.clang-format
Normal file
@ -0,0 +1,108 @@
|
||||
# .clang-format from Qt Creator
|
||||
# https://github.com/qt-creator/qt-creator/blob/master/.clang-format
|
||||
#
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/clang-format.json
|
||||
#
|
||||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
AlignConsecutiveAssignments: None
|
||||
AlignConsecutiveDeclarations: None
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterFunction: true
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: true
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: false
|
||||
BreakBeforeBinaryOperators: All
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeComma
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 100
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- forever # avoids { wrapped to next line
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeCategories:
|
||||
- Regex: '^<Q.*'
|
||||
Priority: 200
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
# Do not add QT_BEGIN_NAMESPACE/QT_END_NAMESPACE as this will indent lines in between.
|
||||
MacroBlockBegin: ""
|
||||
MacroBlockEnd: ""
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 88
|
||||
PenaltyBreakBeforeFirstCallParameter: 300
|
||||
PenaltyBreakComment: 500
|
||||
PenaltyBreakFirstLessLess: 400
|
||||
PenaltyBreakString: 600
|
||||
PenaltyExcessCharacter: 50
|
||||
PenaltyReturnTypeOnItsOwnLine: 300
|
||||
PointerAlignment: Right
|
||||
ReflowComments: false
|
||||
SortIncludes: CaseSensitive
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: true
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: c++17
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -3,7 +3,7 @@
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: palm1r
|
||||
ko_fi: qodeassist
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
|
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Log**
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request 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'));
|
172
.github/workflows/build_cmake.yml
vendored
172
.github/workflows/build_cmake.yml
vendored
@ -1,19 +1,24 @@
|
||||
name: Build plugin
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: QodeAssist
|
||||
QT_VERSION: 6.7.3
|
||||
QT_CREATOR_VERSION: 14.0.2
|
||||
QT_CREATOR_SNAPSHOT: NO
|
||||
MACOS_DEPLOYMENT_TARGET: "11.0"
|
||||
CMAKE_VERSION: "3.29.6"
|
||||
NINJA_VERSION: "1.12.1"
|
||||
|
||||
jobs:
|
||||
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 }}
|
||||
outputs:
|
||||
tag: ${{ steps.git.outputs.tag }}
|
||||
@ -23,76 +28,57 @@ jobs:
|
||||
- {
|
||||
name: "Windows Latest MSVC", artifact: "Windows-x64",
|
||||
os: windows-latest,
|
||||
platform: windows_x64,
|
||||
cc: "cl", cxx: "cl",
|
||||
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
||||
}
|
||||
- {
|
||||
name: "Ubuntu Latest GCC", artifact: "Linux-x64",
|
||||
os: ubuntu-latest,
|
||||
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64",
|
||||
os: ubuntu-22.04,
|
||||
platform: linux_x64,
|
||||
cc: "gcc", cxx: "g++"
|
||||
}
|
||||
- {
|
||||
name: "macOS Latest Clang", artifact: "macOS-universal",
|
||||
os: macos-latest,
|
||||
platform: mac_x64,
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout submodules
|
||||
id: git
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
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()
|
||||
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()
|
||||
|
||||
- name: Download Ninja and CMake
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
set(cmake_version "$ENV{CMAKE_VERSION}")
|
||||
set(ninja_version "$ENV{NINJA_VERSION}")
|
||||
uses: lukka/get-cmake@latest
|
||||
with:
|
||||
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
||||
ninjaVersion: ${{ env.NINJA_VERSION }}
|
||||
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(ninja_suffix "win.zip")
|
||||
set(cmake_suffix "windows-x86_64.zip")
|
||||
set(cmake_dir "cmake-${cmake_version}-windows-x86_64/bin")
|
||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
||||
set(ninja_suffix "linux.zip")
|
||||
set(cmake_suffix "linux-x86_64.tar.gz")
|
||||
set(cmake_dir "cmake-${cmake_version}-linux-x86_64/bin")
|
||||
elseif ("${{ runner.os }}" STREQUAL "macOS")
|
||||
set(ninja_suffix "mac.zip")
|
||||
set(cmake_suffix "macos-universal.tar.gz")
|
||||
set(cmake_dir "cmake-${cmake_version}-macos-universal/CMake.app/Contents/bin")
|
||||
endif()
|
||||
|
||||
set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")
|
||||
file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)
|
||||
|
||||
set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")
|
||||
file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)
|
||||
|
||||
# Add to PATH environment variable
|
||||
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)
|
||||
set(path_separator ":")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(path_separator ";")
|
||||
endif()
|
||||
file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}")
|
||||
|
||||
if (NOT "${{ runner.os }}" STREQUAL "Windows")
|
||||
execute_process(
|
||||
COMMAND chmod +x ninja
|
||||
COMMAND chmod +x ${cmake_dir}/cmake
|
||||
)
|
||||
endif()
|
||||
|
||||
- name: Install system libs
|
||||
- name: Install dependencies
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
if ("${{ runner.os }}" STREQUAL "Linux")
|
||||
@ -100,7 +86,13 @@ jobs:
|
||||
COMMAND sudo apt update
|
||||
)
|
||||
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
|
||||
)
|
||||
if (NOT result EQUAL 0)
|
||||
@ -112,14 +104,14 @@ jobs:
|
||||
id: qt
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
set(qt_version "$ENV{QT_VERSION}")
|
||||
set(qt_version "${{ matrix.qt_config.qt_version }}")
|
||||
|
||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(url_os "windows_x86")
|
||||
set(qt_package_arch_suffix "win64_msvc2019_64")
|
||||
set(qt_dir_prefix "${qt_version}/msvc2019_64")
|
||||
set(qt_package_suffix "-Windows-Windows_10_22H2-MSVC2019-Windows-Windows_10_22H2-X86_64")
|
||||
set(qt_package_arch_suffix "win64_msvc2022_64")
|
||||
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
||||
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64")
|
||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
||||
set(url_os "linux_x64")
|
||||
if (qt_version VERSION_LESS "6.7.0")
|
||||
@ -128,15 +120,15 @@ jobs:
|
||||
set(qt_package_arch_suffix "linux_gcc_64")
|
||||
endif()
|
||||
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")
|
||||
set(url_os "mac_x64")
|
||||
set(qt_package_arch_suffix "clang_64")
|
||||
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()
|
||||
|
||||
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(READ ./Updates.xml updates_xml)
|
||||
@ -146,7 +138,7 @@ jobs:
|
||||
file(MAKE_DIRECTORY qt6)
|
||||
|
||||
# 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}")
|
||||
|
||||
message("Downloading Qt to ${qt_dir}")
|
||||
@ -165,11 +157,17 @@ jobs:
|
||||
|
||||
foreach(package qt5compat qtshadertools)
|
||||
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
|
||||
)
|
||||
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
|
||||
if ("${{ runner.os }}" STREQUAL "Linux")
|
||||
if (qt_version VERSION_LESS "6.7.0")
|
||||
@ -177,47 +175,26 @@ jobs:
|
||||
else()
|
||||
set(uic_suffix "Rhel8.6-x86_64")
|
||||
endif()
|
||||
downloadAndExtract(
|
||||
downloadAndExtractLibicu(
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-${uic_suffix}.7z"
|
||||
icu.7z
|
||||
)
|
||||
endif()
|
||||
|
||||
- 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
|
||||
shell: cmake -P {0}
|
||||
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)
|
||||
# Save the path for other steps
|
||||
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
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
@ -255,7 +232,7 @@ jobs:
|
||||
COMMAND python
|
||||
-u
|
||||
"${{ 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 .
|
||||
--build build
|
||||
--qt-path "${{ steps.qt.outputs.qt_dir }}"
|
||||
@ -273,13 +250,18 @@ jobs:
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ 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}}-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:
|
||||
if: contains(github.ref, 'tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [build]
|
||||
|
||||
steps:
|
||||
- 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
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -34,6 +34,7 @@ Thumbs.db
|
||||
*.rc
|
||||
/.qmake.cache
|
||||
/.qmake.stash
|
||||
.qmlls.ini
|
||||
|
||||
# qtcreator generated files
|
||||
*.pro.user*
|
||||
|
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
|
3
.qmlformat.ini
Normal file
3
.qmlformat.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[General]
|
||||
IndentWidth=4
|
||||
NewlineType=native
|
1
3rdparty/inja
vendored
Submodule
1
3rdparty/inja
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 384a6bef3f1b69f8e429b28ee206df832cb2a6c8
|
@ -8,25 +8,54 @@ set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
find_package(QtCreator REQUIRED COMPONENTS Core)
|
||||
find_package(Qt6 COMPONENTS Core Gui 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(settings)
|
||||
add_subdirectory(logger)
|
||||
add_subdirectory(chatview)
|
||||
add_subdirectory(ChatView)
|
||||
add_subdirectory(context)
|
||||
if(GTest_FOUND)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
add_qtc_plugin(QodeAssist
|
||||
PLUGIN_DEPENDS
|
||||
QtCreator::Core
|
||||
QtCreator::LanguageClient
|
||||
QtCreator::TextEditor
|
||||
QtCreator::ProjectExplorer
|
||||
DEPENDS
|
||||
Qt::Core
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Widgets
|
||||
Qt::Network
|
||||
QtCreator::ExtensionSystem
|
||||
QtCreator::Utils
|
||||
QtCreator::ProjectExplorer
|
||||
QodeAssistChatViewplugin
|
||||
SOURCES
|
||||
.github/workflows/build_cmake.yml
|
||||
.github/workflows/README.md
|
||||
@ -35,26 +64,65 @@ add_qtc_plugin(QodeAssist
|
||||
QodeAssistConstants.hpp
|
||||
QodeAssisttr.h
|
||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||
templates/Templates.hpp
|
||||
templates/CodeLlamaFim.hpp
|
||||
templates/Ollama.hpp
|
||||
templates/Claude.hpp
|
||||
templates/OpenAI.hpp
|
||||
templates/MistralAI.hpp
|
||||
templates/StarCoder2Fim.hpp
|
||||
templates/DeepSeekCoderFim.hpp
|
||||
templates/CustomFimTemplate.hpp
|
||||
templates/DeepSeekCoderChat.hpp
|
||||
templates/CodeLlamaChat.hpp
|
||||
templates/QwenChat.hpp
|
||||
templates/StarCoderChat.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/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.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
|
||||
LSPCompletion.hpp
|
||||
LLMSuggestion.hpp LLMSuggestion.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/NavigationPanel.hpp chat/NavigationPanel.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
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssist PRIVATE QodeAssistChatViewplugin)
|
||||
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,14 +1,30 @@
|
||||
qt_add_library(QodeAssistChatView STATIC)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
|
||||
qt_add_qml_module(QodeAssistChatView
|
||||
URI ChatView
|
||||
VERSION 1.0
|
||||
DEPENDENCIES QtQuick
|
||||
QML_FILES
|
||||
qml/RootItem.qml
|
||||
qml/ChatItem.qml
|
||||
qml/Badge.qml
|
||||
qml/dialog/CodeBlock.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
|
||||
ChatWidget.hpp ChatWidget.cpp
|
||||
ChatModel.hpp ChatModel.cpp
|
||||
@ -16,6 +32,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
ClientInterface.hpp ClientInterface.cpp
|
||||
MessagePart.hpp
|
||||
ChatUtils.h ChatUtils.cpp
|
||||
ChatSerializer.hpp ChatSerializer.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
@ -28,8 +45,9 @@ target_link_libraries(QodeAssistChatView
|
||||
QtCreator::Utils
|
||||
LLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
225
ChatView/ChatModel.cpp
Normal file
225
ChatView/ChatModel.cpp
Normal file
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include <utils/aspects.h>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
#include <QtQml>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatModel::ChatModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
connect(
|
||||
&settings.chatTokensThreshold,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatModel::tokensThresholdChanged);
|
||||
}
|
||||
|
||||
int ChatModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_messages.size();
|
||||
}
|
||||
|
||||
QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_messages.size())
|
||||
return QVariant();
|
||||
|
||||
const Message &message = m_messages[index.row()];
|
||||
switch (static_cast<Roles>(role)) {
|
||||
case Roles::RoleType:
|
||||
return QVariant::fromValue(message.role);
|
||||
case Roles::Content: {
|
||||
return message.content;
|
||||
}
|
||||
case Roles::Attachments: {
|
||||
QStringList filenames;
|
||||
for (const auto &attachment : message.attachments) {
|
||||
filenames << attachment.filename;
|
||||
}
|
||||
return filenames;
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ChatModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[Roles::RoleType] = "roleType";
|
||||
roles[Roles::Content] = "content";
|
||||
roles[Roles::Attachments] = "attachments";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void ChatModel::addMessage(
|
||||
const QString &content,
|
||||
ChatRole role,
|
||||
const QString &id,
|
||||
const QList<Context::ContentFile> &attachments)
|
||||
{
|
||||
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) {
|
||||
Message &lastMessage = m_messages.last();
|
||||
lastMessage.content = content;
|
||||
lastMessage.attachments = attachments;
|
||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
Message newMessage{role, content, id};
|
||||
newMessage.attachments = attachments;
|
||||
m_messages.append(newMessage);
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
||||
{
|
||||
return m_messages;
|
||||
}
|
||||
|
||||
void ChatModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_messages.clear();
|
||||
endResetModel();
|
||||
emit modelReseted();
|
||||
}
|
||||
|
||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||
{
|
||||
QList<MessagePart> parts;
|
||||
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
||||
int lastIndex = 0;
|
||||
auto blockMatches = codeBlockRegex.globalMatch(content);
|
||||
bool foundCodeBlock = blockMatches.hasNext();
|
||||
|
||||
while (blockMatches.hasNext()) {
|
||||
auto match = blockMatches.next();
|
||||
if (match.capturedStart() > lastIndex) {
|
||||
QString textBetween
|
||||
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||
if (!textBetween.isEmpty()) {
|
||||
parts.append({MessagePart::Text, textBetween, ""});
|
||||
}
|
||||
}
|
||||
parts.append({MessagePart::Code, match.captured(2).trimmed(), match.captured(1)});
|
||||
lastIndex = match.capturedEnd();
|
||||
}
|
||||
|
||||
if (lastIndex < content.length()) {
|
||||
QString remainingText = content.mid(lastIndex).trimmed();
|
||||
|
||||
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, ""});
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const
|
||||
{
|
||||
QJsonArray messages;
|
||||
messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}});
|
||||
|
||||
for (const auto &message : m_messages) {
|
||||
QString role;
|
||||
switch (message.role) {
|
||||
case ChatRole::User:
|
||||
role = "user";
|
||||
break;
|
||||
case ChatRole::Assistant:
|
||||
role = "assistant";
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int ChatModel::tokensThreshold() const
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
return settings.chatTokensThreshold();
|
||||
}
|
||||
|
||||
QString ChatModel::lastMessageId() const
|
||||
{
|
||||
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
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -24,28 +24,31 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <qqmlintegration.h>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
#include "context/ContentFile.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int totalTokens READ totalTokens NOTIFY totalTokensChanged FINAL)
|
||||
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
enum Roles { RoleType = Qt::UserRole, Content };
|
||||
|
||||
enum ChatRole { System, User, Assistant };
|
||||
Q_ENUM(ChatRole)
|
||||
|
||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
|
||||
|
||||
struct Message
|
||||
{
|
||||
ChatRole role;
|
||||
QString content;
|
||||
int tokenCount;
|
||||
QString id;
|
||||
|
||||
QList<Context::ContentFile> attachments;
|
||||
};
|
||||
|
||||
explicit ChatModel(QObject *parent = nullptr);
|
||||
@ -54,28 +57,30 @@ public:
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void addMessage(const QString &content, ChatRole role);
|
||||
Q_INVOKABLE void addMessage(
|
||||
const QString &content,
|
||||
ChatRole role,
|
||||
const QString &id,
|
||||
const QList<Context::ContentFile> &attachments = {});
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||
|
||||
QVector<Message> getChatHistory() const;
|
||||
QJsonArray prepareMessagesForRequest(LLMCore::ContextData context) const;
|
||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
||||
|
||||
int totalTokens() const;
|
||||
int tokensThreshold() const;
|
||||
|
||||
QString currentModel() const;
|
||||
QString lastMessageId() const;
|
||||
|
||||
Q_INVOKABLE void resetModelTo(int index);
|
||||
|
||||
signals:
|
||||
void totalTokensChanged();
|
||||
void tokensThresholdChanged();
|
||||
void modelReseted();
|
||||
|
||||
private:
|
||||
void trim();
|
||||
int estimateTokenCount(const QString &text) const;
|
||||
|
||||
QVector<Message> m_messages;
|
||||
int m_totalTokens = 0;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
605
ChatView/ChatRootView.cpp
Normal file
605
ChatView/ChatRootView.cpp
Normal file
@ -0,0 +1,605 @@
|
||||
/*
|
||||
* 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 "ChatRootView.hpp"
|
||||
|
||||
#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/utilsicons.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "context/TokenUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_chatModel(new 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();
|
||||
|
||||
connect(
|
||||
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
||||
|
||||
connect(
|
||||
m_clientInterface,
|
||||
&ClientInterface::messageReceivedCompletely,
|
||||
this,
|
||||
&ChatRootView::autosave);
|
||||
|
||||
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
|
||||
{
|
||||
return m_chatModel;
|
||||
}
|
||||
|
||||
void ChatRootView::sendMessage(const QString &message)
|
||||
{
|
||||
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);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
autosave();
|
||||
m_chatModel->clear();
|
||||
setRecentFilePath(QString{});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
|
||||
clearAttachmentFiles();
|
||||
}
|
||||
|
||||
void ChatRootView::copyToClipboard(const QString &text)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
void ChatRootView::cancelRequest()
|
||||
{
|
||||
m_clientInterface->cancelRequest();
|
||||
}
|
||||
|
||||
void ChatRootView::clearAttachmentFiles()
|
||||
{
|
||||
if (!m_attachmentFiles.isEmpty()) {
|
||||
m_attachmentFiles.clear();
|
||||
emit attachmentFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::clearLinkedFiles()
|
||||
{
|
||||
if (!m_linkedFiles.isEmpty()) {
|
||||
m_linkedFiles.clear();
|
||||
emit linkedFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString ChatRootView::getChatsHistoryDir() const
|
||||
{
|
||||
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(".")) {
|
||||
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
|
||||
return QString();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
QString ChatRootView::currentTemplate() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.caModel();
|
||||
}
|
||||
|
||||
void ChatRootView::saveHistory(const QString &filePath)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatRootView::loadHistory(const QString &filePath)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
void ChatRootView::showSaveDialog()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
void ChatRootView::showLoadDialog()
|
||||
{
|
||||
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
|
133
ChatView/ChatRootView.hpp
Normal file
133
ChatView/ChatRootView.hpp
Normal file
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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 <QQuickItem>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
#include "llmcore/PromptProviderChat.hpp"
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
||||
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged 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
|
||||
|
||||
public:
|
||||
ChatRootView(QQuickItem *parent = nullptr);
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
void saveHistory(const QString &filePath);
|
||||
void loadHistory(const QString &filePath);
|
||||
|
||||
Q_INVOKABLE void showSaveDialog();
|
||||
Q_INVOKABLE void showLoadDialog();
|
||||
|
||||
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:
|
||||
void sendMessage(const QString &message);
|
||||
void copyToClipboard(const QString &text);
|
||||
void cancelRequest();
|
||||
void clearAttachmentFiles();
|
||||
void clearLinkedFiles();
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
void attachmentFilesChanged();
|
||||
void linkedFilesChanged();
|
||||
void inputTokensCountChanged();
|
||||
void isSyncOpenFilesChanged();
|
||||
void chatFileNameChanged();
|
||||
void textFamilyChanged();
|
||||
void codeFamilyChanged();
|
||||
void codeFontSizeChanged();
|
||||
void textFontSizeChanged();
|
||||
void textFormatChanged();
|
||||
|
||||
private:
|
||||
QString getChatsHistoryDir() const;
|
||||
QString getSuggestedFileName() const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
LLMCore::PromptProviderChat m_promptProvider;
|
||||
ClientInterface *m_clientInterface;
|
||||
QString m_currentTemplate;
|
||||
QString m_recentFilePath;
|
||||
QStringList m_attachmentFiles;
|
||||
QStringList m_linkedFiles;
|
||||
int m_messageTokensCount{0};
|
||||
int m_inputTokensCount{0};
|
||||
bool m_isSyncOpenFiles;
|
||||
QList<Core::IEditor *> m_currentEditors;
|
||||
};
|
||||
|
||||
} // 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
|
68
ChatView/ChatUtils.cpp
Normal file
68
ChatView/ChatUtils.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 "ChatUtils.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
void ChatUtils::copyToClipboard(const QString &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
|
40
ChatView/ChatUtils.h
Normal file
40
ChatView/ChatUtils.h
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatUtils : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_NAMED_ELEMENT(ChatUtils)
|
||||
|
||||
public:
|
||||
explicit ChatUtils(QObject *parent = nullptr)
|
||||
: QObject(parent) {};
|
||||
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text);
|
||||
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -27,7 +27,7 @@ namespace QodeAssist::Chat {
|
||||
ChatWidget::ChatWidget(QWidget *parent)
|
||||
: QQuickWidget(parent)
|
||||
{
|
||||
setSource(QUrl("qrc:/ChatView/qml/RootItem.qml"));
|
||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
}
|
||||
|
||||
@ -40,4 +40,4 @@ void ChatWidget::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.
|
||||
*
|
||||
@ -38,4 +38,4 @@ signals:
|
||||
void clearPressed();
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace QodeAssist::Chat
|
217
ChatView/ClientInterface.cpp
Normal file
217
ChatView/ClientInterface.cpp
Normal file
@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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 "ClientInterface.hpp"
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(
|
||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_requestHandler(new LLMCore::RequestHandler(this))
|
||||
, m_chatModel(chatModel)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
{
|
||||
connect(
|
||||
m_requestHandler,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
|
||||
handleLLMResponse(completion, request, isComplete);
|
||||
});
|
||||
|
||||
connect(
|
||||
m_requestHandler,
|
||||
&LLMCore::RequestHandler::requestFinished,
|
||||
this,
|
||||
[this](const QString &, bool success, const QString &errorString) {
|
||||
if (!success) {
|
||||
emit errorOccurred(errorString);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientInterface::~ClientInterface() = default;
|
||||
|
||||
void ClientInterface::sendMessage(
|
||||
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
|
||||
{
|
||||
cancelRequest();
|
||||
|
||||
auto attachFiles = m_contextManager->getContentFiles(attachments);
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
||||
|
||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
return;
|
||||
}
|
||||
|
||||
LLMCore::ContextData context;
|
||||
|
||||
if (chatAssistantSettings.useSystemPrompt()) {
|
||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||
}
|
||||
context.systemPrompt = systemPrompt;
|
||||
}
|
||||
|
||||
QVector<LLMCore::Message> messages;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content});
|
||||
}
|
||||
context.history = messages;
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Chat;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
|
||||
: 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()}};
|
||||
}
|
||||
|
||||
config.apiKey = provider->apiKey();
|
||||
|
||||
config.provider
|
||||
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
|
||||
|
||||
QJsonObject request{{"id", QUuid::createUuid().toString()}};
|
||||
m_requestHandler->sendLLMRequest(config, request);
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
{
|
||||
m_chatModel->clear();
|
||||
LOG_MESSAGE("Chat history cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::cancelRequest()
|
||||
{
|
||||
auto id = m_chatModel->lastMessageId();
|
||||
m_requestHandler->cancelRequest(id);
|
||||
}
|
||||
|
||||
void ClientInterface::handleLLMResponse(
|
||||
const QString &response, const QJsonObject &request, bool isComplete)
|
||||
{
|
||||
const auto message = response.trimmed();
|
||||
|
||||
if (!message.isEmpty()) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString ClientInterface::getCurrentFileContext() const
|
||||
{
|
||||
auto currentEditor = Core::EditorManager::currentEditor();
|
||||
if (!currentEditor) {
|
||||
LOG_MESSAGE("No active editor found");
|
||||
return QString();
|
||||
}
|
||||
|
||||
auto textDocument = qobject_cast<TextEditor::TextDocument *>(currentEditor->document());
|
||||
if (!textDocument) {
|
||||
LOG_MESSAGE("Current document is not a text document");
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
|
||||
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString());
|
||||
|
||||
QString content = textDocument->document()->toPlainText();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -25,6 +25,8 @@
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "RequestHandler.hpp"
|
||||
#include "llmcore/IPromptProvider.hpp"
|
||||
#include <context/ContextManager.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@ -33,22 +35,33 @@ class ClientInterface : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
explicit ClientInterface(
|
||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void sendMessage(const QString &message);
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments = {},
|
||||
const QList<QString> &linkedFiles = {});
|
||||
void clearMessages();
|
||||
void cancelRequest();
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
signals:
|
||||
void messageReceived(const QString &message);
|
||||
void errorOccurred(const QString &error);
|
||||
void messageReceivedCompletely();
|
||||
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, bool isComplete);
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||
QString getCurrentFileContext() const;
|
||||
QString getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
||||
|
||||
LLMCore::RequestHandler *m_requestHandler;
|
||||
QString m_accumulatedResponse;
|
||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
ChatModel *m_chatModel;
|
||||
LLMCore::RequestHandler *m_requestHandler;
|
||||
Context::ContextManager *m_contextManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
@ -23,18 +23,18 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property alias text: badgeText.text
|
||||
property alias fontColor: badgeText.color
|
||||
|
||||
width: badgeText.implicitWidth + radius
|
||||
height: badgeText.implicitHeight + 6
|
||||
color: "lightgreen"
|
||||
radius: height / 2
|
||||
implicitWidth: badgeText.implicitWidth + root.radius
|
||||
implicitHeight: badgeText.implicitHeight + 6
|
||||
color: palette.button
|
||||
radius: root.height / 2
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
border.color: "gray"
|
||||
|
||||
Text {
|
||||
id: badgeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: palette.buttonText
|
||||
}
|
||||
}
|
213
ChatView/qml/ChatItem.qml
Normal file
213
ChatView/qml/ChatItem.qml
Normal file
@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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 ChatView
|
||||
import QtQuick.Layouts
|
||||
import "./dialog"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias msgModel: msgCreator.model
|
||||
property alias messageAttachments: attachmentsModel.model
|
||||
property string textFontFamily: Qt.application.font.family
|
||||
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
|
||||
|
||||
property bool isUserMessage: false
|
||||
property int messageIndex: -1
|
||||
property real listViewContentY: 0
|
||||
|
||||
signal resetChatToMessage(int index)
|
||||
|
||||
height: msgColumn.implicitHeight + 10
|
||||
radius: 8
|
||||
color: isUserMessage ? palette.alternateBase
|
||||
: palette.base
|
||||
|
||||
HoverHandler {
|
||||
id: mouse
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: msgColumn
|
||||
|
||||
x: 5
|
||||
width: parent.width - x
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: msgCreator
|
||||
delegate: Loader {
|
||||
id: msgCreatorDelegate
|
||||
// Fix me:
|
||||
// why does `required property MessagePart modelData` not work?
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredWidth: root.width
|
||||
sourceComponent: {
|
||||
// If `required property MessagePart modelData` is used
|
||||
// and conversion to MessagePart fails, you're left
|
||||
// with a nullptr. This tests that to prevent crashing.
|
||||
if(!modelData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch(modelData.type) {
|
||||
case MessagePart.Text: return textComponent;
|
||||
case MessagePart.Code: return codeBlockComponent;
|
||||
default: return textComponent;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
TextComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: codeBlockComponent
|
||||
CodeBlockComponent {
|
||||
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 {
|
||||
required property var itemData
|
||||
height: implicitHeight + 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 10
|
||||
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
|
||||
: itemData.text
|
||||
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 {
|
||||
id: codeblock
|
||||
|
||||
required property var itemData
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
code: itemData.text
|
||||
language: itemData.language
|
||||
codeFontFamily: root.codeFontFamily
|
||||
codeFontSize: root.codeFontSize
|
||||
}
|
||||
}
|
232
ChatView/qml/RootItem.qml
Normal file
232
ChatView/qml/RootItem.qml
Normal file
@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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.Controls.Basic as QQC
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
import "./controls"
|
||||
import "./parts"
|
||||
|
||||
ChatRootView {
|
||||
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 {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
color: palette.window
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.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()
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: chatListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
leftMargin: 5
|
||||
model: root.chatModel
|
||||
clip: true
|
||||
spacing: 10
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 2000
|
||||
|
||||
delegate: ChatItem {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width - scroll.width
|
||||
msgModel: root.chatModel.processMessageContent(model.content)
|
||||
messageAttachments: model.attachments
|
||||
isUserMessage: model.roleType === ChatModel.User
|
||||
messageIndex: index
|
||||
listViewContentY: chatListView.contentY
|
||||
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 {
|
||||
width: ListView.view.width - scroll.width
|
||||
height: 30
|
||||
}
|
||||
|
||||
ScrollBar.vertical: QQC.ScrollBar {
|
||||
id: scroll
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
root.scrollToBottom()
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (atYEnd) {
|
||||
root.scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: view
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 30
|
||||
Layout.maximumHeight: root.height / 2
|
||||
|
||||
QQC.TextArea {
|
||||
id: messageInput
|
||||
|
||||
placeholderText: qsTr("Type your message here...")
|
||||
placeholderTextColor: palette.mid
|
||||
color: palette.text
|
||||
background: Rectangle {
|
||||
radius: 2
|
||||
color: palette.base
|
||||
border.color: messageInput.activeFocus ? palette.highlight : palette.button
|
||||
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) {
|
||||
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
|
||||
root.sendChatMessage()
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AttachedFilesPlace {
|
||||
id: attachedFilesPlace
|
||||
|
||||
Layout.fillWidth: true
|
||||
attachedFilesModel: root.attachmentFiles
|
||||
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg"
|
||||
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
|
||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
|
||||
}
|
||||
|
||||
AttachedFilesPlace {
|
||||
id: linkedFilesPlace
|
||||
|
||||
Layout.fillWidth: true
|
||||
attachedFilesModel: root.linkedFiles
|
||||
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
|
||||
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
|
||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
||||
}
|
||||
|
||||
BottomBar {
|
||||
id: bottomBar
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
sendButton.onClicked: root.sendChatMessage()
|
||||
stopButton.onClicked: root.cancelRequest()
|
||||
syncOpenFiles {
|
||||
checked: root.isSyncOpenFiles
|
||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||
}
|
||||
attachFiles.onClicked: root.showAttachFilesDialog()
|
||||
linkFiles.onClicked: root.showLinkFilesDialog()
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
root.chatModel.clear()
|
||||
root.clearAttachmentFiles()
|
||||
root.updateInputTokensCount()
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
Qt.callLater(chatListView.positionViewAtEnd)
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
root.sendMessage(messageInput.text)
|
||||
messageInput.text = ""
|
||||
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.
|
||||
*
|
||||
@ -26,26 +26,33 @@ Rectangle {
|
||||
|
||||
property string code: ""
|
||||
property string language: ""
|
||||
property color selectionColor
|
||||
|
||||
readonly property string monospaceFont: {
|
||||
switch (Qt.platform.os) {
|
||||
case "windows":
|
||||
return "Consolas";
|
||||
case "osx":
|
||||
return "Menlo";
|
||||
case "linux":
|
||||
return "DejaVu Sans Mono";
|
||||
default:
|
||||
return "monospace";
|
||||
property real currentContentY: 0
|
||||
property real blockStart: 0
|
||||
|
||||
property alias codeFontFamily: codeText.font.family
|
||||
property alias codeFontSize: codeText.font.pointSize
|
||||
|
||||
readonly property real buttonTopMargin: 5
|
||||
readonly property real blockEnd: blockStart + root.height
|
||||
readonly property real maxButtonOffset: Math.max(0, root.height - copyButton.height - buttonTopMargin)
|
||||
|
||||
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)
|
||||
: Qt.lighter(root.color, 1.3)
|
||||
border.width: 2
|
||||
radius: 4
|
||||
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: codeText.implicitHeight + 20
|
||||
|
||||
@ -55,17 +62,14 @@ Rectangle {
|
||||
|
||||
TextEdit {
|
||||
id: codeText
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
text: root.code
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
font.family: monospaceFont
|
||||
font.pointSize: 12
|
||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: root.selectionColor
|
||||
selectionColor: palette.highlight
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
@ -77,14 +81,20 @@ Rectangle {
|
||||
text: root.language
|
||||
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
||||
: Qt.lighter(root.color, 1.1)
|
||||
font.pointSize: 8
|
||||
font.pointSize: codeText.font.pointSize - 4
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 5
|
||||
text: "Copy"
|
||||
QoAButton {
|
||||
id: copyButton
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: root.buttonPosition
|
||||
right: parent.right
|
||||
rightMargin: root.buttonTopMargin
|
||||
}
|
||||
|
||||
text: qsTr("Copy")
|
||||
onClicked: {
|
||||
utils.copyToClipboard(root.code)
|
||||
text = qsTr("Copied")
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -25,5 +25,6 @@ TextEdit {
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
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
|
231
ConfigurationManager.cpp
Normal file
231
ConfigurationManager.cpp
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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 "ConfigurationManager.hpp"
|
||||
|
||||
#include <settings/ButtonAspect.hpp>
|
||||
#include <QTimer>
|
||||
|
||||
#include "QodeAssisttr.h"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
ConfigurationManager &ConfigurationManager::instance()
|
||||
{
|
||||
static ConfigurationManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void ConfigurationManager::init()
|
||||
{
|
||||
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)
|
||||
: QObject(parent)
|
||||
, m_generalSettings(Settings::generalSettings())
|
||||
, m_providersManager(LLMCore::ProvidersManager::instance())
|
||||
, m_templateManger(LLMCore::PromptTemplateManager::instance())
|
||||
{}
|
||||
|
||||
void ConfigurationManager::setupConnections()
|
||||
{
|
||||
using Config = ConfigurationManager;
|
||||
using Button = ButtonAspect;
|
||||
|
||||
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.ccSelectTemplate, &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.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()
|
||||
{
|
||||
const auto providersList = m_providersManager.providersNames();
|
||||
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
|
||||
? m_generalSettings.ccProvider
|
||||
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
|
||||
? m_generalSettings.ccPreset1Provider
|
||||
: m_generalSettings.caProvider;
|
||||
|
||||
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectModel()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
|
||||
|
||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
||||
: m_generalSettings.caProvider.volatileValue();
|
||||
|
||||
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
|
||||
: m_generalSettings.caUrl.volatileValue();
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
||||
: m_generalSettings.caModel;
|
||||
|
||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||
if (!provider->supportsModelListing()) {
|
||||
m_generalSettings.showModelsNotSupportedDialog(targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto modelList = provider->getInstalledModels(providerUrl);
|
||||
|
||||
if (modelList.isEmpty()) {
|
||||
m_generalSettings.showModelsNotFoundDialog(targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectTemplate()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
||||
: m_generalSettings.caProvider.volatileValue();
|
||||
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
||||
|
||||
const auto templateList = isCodeCompletion || isPreset1
|
||||
? m_templateManger.getFimTemplatesForProvider(providerID)
|
||||
: m_templateManger.getChatTemplatesForProvider(providerID);
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Template
|
||||
: m_generalSettings.caTemplate;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectUrl()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
QStringList urls;
|
||||
for (const auto &name : m_providersManager.providersNames()) {
|
||||
const auto url = m_providersManager.getProviderByName(name)->url();
|
||||
if (!urls.contains(url))
|
||||
urls.append(url);
|
||||
}
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
||||
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
||||
? m_generalSettings.ccPreset1Url
|
||||
: m_generalSettings.caUrl;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
||||
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
63
ConfigurationManager.hpp
Normal file
63
ConfigurationManager.hpp
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ConfigurationManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ConfigurationManager &instance();
|
||||
|
||||
void init();
|
||||
|
||||
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
||||
void updateAllTemplateDescriptions();
|
||||
void checkTemplate(const Utils::StringAspect &templateAspect);
|
||||
void checkAllTemplate();
|
||||
|
||||
public slots:
|
||||
void selectProvider();
|
||||
void selectModel();
|
||||
void selectTemplate();
|
||||
void selectUrl();
|
||||
|
||||
private:
|
||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
||||
~ConfigurationManager() = default;
|
||||
ConfigurationManager(const ConfigurationManager &) = delete;
|
||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
||||
|
||||
Settings::GeneralSettings &m_generalSettings;
|
||||
LLMCore::ProvidersManager &m_providersManager;
|
||||
LLMCore::PromptTemplateManager &m_templateManger;
|
||||
|
||||
void setupConnections();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
@ -1,255 +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/ContextSettings.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 instructions = getInstructions();
|
||||
|
||||
return {contextBefore, contextAfter, instructions};
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const
|
||||
{
|
||||
if (Settings::contextSettings().readFullFile()) {
|
||||
return readWholeFileBefore(lineNumber, cursorPosition);
|
||||
} else {
|
||||
int effectiveStartLine;
|
||||
int beforeCursor = Settings::contextSettings().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::contextSettings().readFullFile()) {
|
||||
return readWholeFileAfter(lineNumber, cursorPosition);
|
||||
} else {
|
||||
int endLine = qMin(m_document->blockCount() - 1,
|
||||
lineNumber + Settings::contextSettings().readStringsAfterCursor());
|
||||
return getContextBetween(lineNumber + 1, endLine, -1);
|
||||
}
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getInstructions() const
|
||||
{
|
||||
QString instructions;
|
||||
|
||||
if (Settings::contextSettings().useFilePathInContext())
|
||||
instructions += getLanguageAndFileInfo();
|
||||
|
||||
if (Settings::contextSettings().useProjectChangesCache())
|
||||
instructions += ChangesManager::instance().getRecentChangesContext(m_textDocument);
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
@ -1,65 +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;
|
||||
QString getInstructions() 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.
|
||||
*
|
||||
@ -23,30 +23,56 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
#include <texteditor/textdocument.h>
|
||||
|
||||
#include "DocumentContextReader.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "CodeHandler.hpp"
|
||||
#include "context/DocumentContextReader.hpp"
|
||||
#include "context/Utils.hpp"
|
||||
#include "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "settings/ContextSettings.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMClientInterface::LLMClientInterface()
|
||||
: m_requestHandler(this)
|
||||
LLMClientInterface::LLMClientInterface(
|
||||
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,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
&LLMClientInterface::sendCompletionToClient);
|
||||
connect(
|
||||
&m_requestHandler,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
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
|
||||
{
|
||||
return "Qode Assist";
|
||||
return "QodeAssist";
|
||||
}
|
||||
|
||||
void LLMClientInterface::startImpl()
|
||||
@ -73,7 +99,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
handleTextDocumentDidOpen(request);
|
||||
} else if (method == "getCompletionsCycling") {
|
||||
QString requestId = request["id"].toString();
|
||||
startTimeMeasurement(requestId);
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
handleCompletion(request);
|
||||
} else if (method == "$/cancelRequest") {
|
||||
handleCancelRequest(request);
|
||||
@ -146,71 +172,192 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
|
||||
|
||||
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
{
|
||||
auto updatedContext = prepareContext(request);
|
||||
auto filePath = Context::extractFilePathFromRequest(request);
|
||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
||||
if (!documentInfo.document) {
|
||||
LOG_MESSAGE("Error: Document is not available for" + filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
auto updatedContext = prepareContext(request, documentInfo);
|
||||
|
||||
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) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||
: m_generalSettings.ccPreset1Template();
|
||||
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO refactor to dynamic presets system
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Fim;
|
||||
config.provider = LLMCore::ProvidersManager::instance().getCurrentFimProvider();
|
||||
config.promptTemplate = LLMCore::PromptTemplateManager::instance().getCurrentFimTemplate();
|
||||
config.url = QUrl(QString("%1%2").arg(Settings::generalSettings().url(),
|
||||
Settings::generalSettings().endPoint()));
|
||||
config.requestType = LLMCore::RequestType::CodeCompletion;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
// TODO refactor networking
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
|
||||
: QString{"generateContent?"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
||||
} else {
|
||||
config.url = QUrl(
|
||||
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
|
||||
config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}};
|
||||
}
|
||||
config.apiKey = provider->apiKey();
|
||||
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
|
||||
|
||||
config.providerRequest = {{"model", Settings::generalSettings().modelName.value()},
|
||||
{"stream", true},
|
||||
{"stop",
|
||||
QJsonArray::fromStringList(config.promptTemplate->stopWords())}};
|
||||
config.multiLineCompletion = Settings::generalSettings().multiLineCompletion();
|
||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
||||
if (!stopWords.isEmpty())
|
||||
config.providerRequest["stop"] = stopWords;
|
||||
|
||||
if (Settings::contextSettings().useSystemPrompt())
|
||||
config.providerRequest["system"] = Settings::contextSettings().systemPrompt();
|
||||
QString systemPrompt;
|
||||
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());
|
||||
|
||||
config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
|
||||
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
|
||||
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);
|
||||
}
|
||||
|
||||
LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
|
||||
const QStringView &accumulatedCompletion)
|
||||
LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||
{
|
||||
QJsonObject params = request["params"].toObject();
|
||||
QJsonObject doc = params["doc"].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 lineNumber = position["line"].toInt();
|
||||
|
||||
DocumentContextReader reader(textDocument);
|
||||
return reader.prepareContext(lineNumber, cursorPosition);
|
||||
Context::DocumentContextReader
|
||||
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
|
||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
||||
const QJsonObject &request,
|
||||
bool isComplete)
|
||||
QString LLMClientInterface::endpoint(
|
||||
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
|
||||
{
|
||||
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 response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response[LanguageServerProtocol::idKey] = request["id"];
|
||||
|
||||
QJsonObject result;
|
||||
QJsonArray completions;
|
||||
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;
|
||||
range["start"] = position;
|
||||
QJsonObject end = position;
|
||||
end["character"] = position["character"].toInt() + completion.length();
|
||||
end["character"] = position["character"].toInt() + processedCompletion.length();
|
||||
range["end"] = end;
|
||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||
@ -223,37 +370,13 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
||||
QString("Completions: \n%1")
|
||||
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
|
||||
|
||||
LOG_MESSAGE(QString("Full response: \n%1")
|
||||
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
||||
LOG_MESSAGE(
|
||||
QString("Full response: \n%1")
|
||||
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
endTimeMeasurement(requestId);
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
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
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -22,8 +22,16 @@
|
||||
#include <languageclient/languageclientinterface.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <context/ProgrammingLanguage.hpp>
|
||||
#include <llmcore/ContextData.hpp>
|
||||
#include <llmcore/IPromptProvider.hpp>
|
||||
#include <llmcore/IProviderRegistry.hpp>
|
||||
#include <llmcore/RequestHandler.hpp>
|
||||
#include <logger/IRequestPerformanceLogger.hpp>
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
|
||||
class QNetworkReply;
|
||||
class QNetworkAccessManager;
|
||||
@ -35,20 +43,29 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
|
||||
Q_OBJECT
|
||||
|
||||
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;
|
||||
|
||||
void sendCompletionToClient(const QString &completion,
|
||||
const QJsonObject &request,
|
||||
bool isComplete);
|
||||
void sendCompletionToClient(
|
||||
const QString &completion, const QJsonObject &request, bool isComplete);
|
||||
|
||||
void handleCompletion(const QJsonObject &request);
|
||||
|
||||
// exposed for tests
|
||||
void sendData(const QByteArray &data) override;
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
protected:
|
||||
void startImpl() override;
|
||||
void sendData(const QByteArray &data) override;
|
||||
void parseCurrentMessage() override;
|
||||
|
||||
private:
|
||||
void handleInitialize(const QJsonObject &request);
|
||||
@ -58,16 +75,19 @@ private:
|
||||
void handleExit(const QJsonObject &request);
|
||||
void handleCancelRequest(const QJsonObject &request);
|
||||
|
||||
LLMCore::ContextData prepareContext(const QJsonObject &request,
|
||||
const QStringView &accumulatedCompletion = QString{});
|
||||
LLMCore::ContextData prepareContext(
|
||||
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;
|
||||
QMap<QString, qint64> m_requestStartTimes;
|
||||
|
||||
void startTimeMeasurement(const QString &requestId);
|
||||
void endTimeMeasurement(const QString &requestId);
|
||||
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
|
||||
Context::ContextManager *m_contextManager;
|
||||
};
|
||||
|
||||
} // 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.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
*
|
||||
* Petr Mironychev portions:
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@ -18,107 +23,173 @@
|
||||
*/
|
||||
|
||||
#include "LLMSuggestion.hpp"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QtWidgets/qtoolbar.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/stringutils.h>
|
||||
#include <utils/tooltip/tooltip.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin)
|
||||
: m_completion(completion)
|
||||
, m_linesCount(0)
|
||||
QString mergeWithRightText(const QString &suggestion, const QString &rightText)
|
||||
{
|
||||
int startPos = completion.range().start().toPositionInDocument(origin);
|
||||
int endPos = completion.range().end().toPositionInDocument(origin);
|
||||
if (suggestion.isEmpty() || rightText.isEmpty()) {
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
startPos = qBound(0, startPos, origin->characterCount() - 1);
|
||||
endPos = qBound(startPos, endPos, origin->characterCount() - 1);
|
||||
int j = 0;
|
||||
QString processed = rightText;
|
||||
QSet<int> matchedPositions;
|
||||
|
||||
m_start = QTextCursor(origin);
|
||||
m_start.setPosition(startPos);
|
||||
m_start.setKeepPositionOnInsert(true);
|
||||
for (int i = 0; i < suggestion.length() && j < processed.length(); ++i) {
|
||||
if (suggestion[i] == processed[j]) {
|
||||
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(endPos, QTextCursor::KeepAnchor);
|
||||
|
||||
QTextBlock block = cursor.block();
|
||||
QString blockText = block.text();
|
||||
|
||||
int startPosInBlock = startPos - block.position();
|
||||
int endPosInBlock = endPos - block.position();
|
||||
int cursorPositionInBlock = cursor.positionInBlock();
|
||||
|
||||
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, completion.text());
|
||||
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||
|
||||
document()->setPlainText(blockText);
|
||||
|
||||
setCurrentPosition(m_start.position());
|
||||
}
|
||||
|
||||
bool LLMSuggestion::apply()
|
||||
{
|
||||
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
|
||||
cursor.beginEditBlock();
|
||||
cursor.removeSelectedText();
|
||||
cursor.insertText(m_completion.text());
|
||||
cursor.endEditBlock();
|
||||
return true;
|
||||
if (!data.text.contains('\n')) {
|
||||
QString processedRightText = mergeWithRightText(data.text, rightText);
|
||||
processedRightText = processedRightText.mid(data.text.length());
|
||||
QString displayText = blockText.left(cursorPositionInBlock) + data.text
|
||||
+ processedRightText;
|
||||
replacementDocument()->setPlainText(displayText);
|
||||
} else {
|
||||
QString displayText = blockText.left(cursorPositionInBlock) + data.text;
|
||||
replacementDocument()->setPlainText(displayText);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
QStringList lines = text.split('\n');
|
||||
|
||||
if (m_linesCount < lines.size())
|
||||
m_linesCount++;
|
||||
|
||||
showTooltip(widget, m_linesCount);
|
||||
|
||||
return m_linesCount == lines.size() && !Utils::ToolTip::isVisible();
|
||||
return applyPart(Line, widget);
|
||||
}
|
||||
|
||||
void LLMSuggestion::onCounterFinished(int count)
|
||||
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
Utils::ToolTip::hide();
|
||||
m_linesCount = 0;
|
||||
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
|
||||
cursor.beginEditBlock();
|
||||
cursor.removeSelectedText();
|
||||
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||
QTextCursor currentCursor = widget->textCursor();
|
||||
const QString text = suggestions()[currentSuggestion()].text;
|
||||
|
||||
QStringList lines = m_completion.text().split('\n');
|
||||
QString textToInsert = lines.mid(0, count).join('\n');
|
||||
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
|
||||
+ (cursor.selectionEnd() - cursor.selectionStart());
|
||||
|
||||
cursor.insertText(textToInsert);
|
||||
cursor.endEditBlock();
|
||||
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
|
||||
|
||||
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()
|
||||
{
|
||||
return m_start.position();
|
||||
}
|
||||
QTextBlock currentBlock = cursor.block();
|
||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||
|
||||
void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count)
|
||||
{
|
||||
Utils::ToolTip::hide();
|
||||
QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight());
|
||||
pos += QPoint(-10, -50);
|
||||
m_counterTooltip = new CounterTooltip(count);
|
||||
Utils::ToolTip::show(pos, m_counterTooltip, widget);
|
||||
connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished);
|
||||
QTextCursor editCursor = cursor;
|
||||
|
||||
int firstLineEnd = text.indexOf('\n');
|
||||
if (firstLineEnd != -1) {
|
||||
QString firstLine = text.left(firstLineEnd);
|
||||
QString restOfText = text.mid(firstLineEnd);
|
||||
|
||||
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
|
||||
|
@ -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.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
*
|
||||
* Petr Mironychev portions:
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@ -19,37 +24,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include "LSPCompletion.hpp"
|
||||
#include <texteditor/textdocumentlayout.h>
|
||||
|
||||
#include "utils/CounterTooltip.hpp"
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <texteditor/textsuggestion.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion
|
||||
class LLMSuggestion : public TextEditor::CyclicSuggestion
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LLMSuggestion(const Completion &completion, QTextDocument *origin);
|
||||
enum Part { Word, Line };
|
||||
|
||||
bool apply() final;
|
||||
bool applyWord(TextEditor::TextEditorWidget *widget) final;
|
||||
bool applyNextLine(TextEditor::TextEditorWidget *widget);
|
||||
void reset() final;
|
||||
int position() final;
|
||||
LLMSuggestion(
|
||||
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
|
||||
|
||||
const Completion &completion() const { return m_completion; }
|
||||
|
||||
void showTooltip(TextEditor::TextEditorWidget *widget, int count);
|
||||
void onCounterFinished(int count);
|
||||
|
||||
private:
|
||||
Completion m_completion;
|
||||
QTextCursor m_start;
|
||||
int m_linesCount;
|
||||
|
||||
CounterTooltip *m_counterTooltip = nullptr;
|
||||
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||
bool apply() override;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -68,9 +68,10 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
|
||||
public:
|
||||
static constexpr LanguageServerProtocol::Key docKey{"doc"};
|
||||
|
||||
GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
|
||||
int version,
|
||||
const LanguageServerProtocol::Position &position)
|
||||
GetCompletionParams(
|
||||
const LanguageServerProtocol::TextDocumentIdentifier &document,
|
||||
int version,
|
||||
const LanguageServerProtocol::Position &position)
|
||||
{
|
||||
setTextDocument(document);
|
||||
setVersion(version);
|
||||
|
@ -1,16 +1,13 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.3.2",
|
||||
"CompatVersion" : "${IDE_VERSION_COMPAT}",
|
||||
"Version" : "0.5.13",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||
"License" : "GNU General Public License Usage
|
||||
|
||||
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)"],
|
||||
"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).",
|
||||
"Url" : "https://github.com/Palm1r/QodeAssist",
|
||||
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
||||
${IDE_PLUGIN_DEPENDENCIES}
|
||||
}
|
||||
|
@ -2,5 +2,11 @@
|
||||
<qresource prefix="/">
|
||||
<file>resources/images/qoderassist-icon@2x.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>
|
||||
</RCC>
|
||||
|
@ -1,8 +1,8 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of Qode Assist.
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
@ -24,16 +24,20 @@
|
||||
|
||||
#include "QodeAssistClient.hpp"
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QTimer>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <languageclient/languageclientsettings.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
#include "LLMSuggestion.hpp"
|
||||
#include "core/ChangesManager.h"
|
||||
#include "settings/ContextSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProjectSettings.hpp"
|
||||
#include <context/ChangesManager.h>
|
||||
#include <logger/Logger.hpp>
|
||||
|
||||
using namespace LanguageServerProtocol;
|
||||
using namespace TextEditor;
|
||||
@ -43,11 +47,12 @@ using namespace Core;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
QodeAssistClient::QodeAssistClient()
|
||||
: LanguageClient::Client(new LLMClientInterface())
|
||||
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||
: LanguageClient::Client(clientInterface)
|
||||
, m_llmClient(clientInterface)
|
||||
, m_recentCharCount(0)
|
||||
{
|
||||
setName("Qode Assist");
|
||||
setName("QodeAssist");
|
||||
LanguageClient::LanguageFilter filter;
|
||||
filter.mimeTypes = QStringList() << "*";
|
||||
setSupportedLanguage(filter);
|
||||
@ -70,47 +75,70 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||
return;
|
||||
|
||||
Client::openDocument(document);
|
||||
connect(document,
|
||||
&TextDocument::contentsChangedWithPosition,
|
||||
this,
|
||||
[this, document](int position, int charsRemoved, int charsAdded) {
|
||||
Q_UNUSED(charsRemoved)
|
||||
if (!Settings::generalSettings().enableAutoComplete())
|
||||
return;
|
||||
connect(
|
||||
document,
|
||||
&TextDocument::contentsChangedWithPosition,
|
||||
this,
|
||||
[this, document](int position, int charsRemoved, int charsAdded) {
|
||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||
return;
|
||||
|
||||
auto project = ProjectManager::projectForFile(document->filePath());
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
auto project = ProjectManager::projectForFile(document->filePath());
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
|
||||
auto textEditor = BaseTextEditor::currentTextEditor();
|
||||
if (!textEditor || textEditor->document() != document)
|
||||
return;
|
||||
auto textEditor = BaseTextEditor::currentTextEditor();
|
||||
if (!textEditor || textEditor->document() != document)
|
||||
return;
|
||||
|
||||
if (Settings::contextSettings().useProjectChangesCache())
|
||||
ChangesManager::instance().addChange(document,
|
||||
position,
|
||||
charsRemoved,
|
||||
charsAdded);
|
||||
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
||||
Context::ChangesManager::instance()
|
||||
.addChange(document, position, charsRemoved, charsAdded);
|
||||
|
||||
TextEditorWidget *widget = textEditor->editorWidget();
|
||||
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
|
||||
return;
|
||||
const int cursorPosition = widget->textCursor().position();
|
||||
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
||||
return;
|
||||
TextEditorWidget *widget = textEditor->editorWidget();
|
||||
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
|
||||
return;
|
||||
|
||||
m_recentCharCount += charsAdded;
|
||||
const int cursorPosition = widget->textCursor().position();
|
||||
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
||||
return;
|
||||
|
||||
if (m_typingTimer.elapsed()
|
||||
> Settings::generalSettings().autoCompletionTypingInterval()) {
|
||||
m_recentCharCount = charsAdded;
|
||||
m_typingTimer.restart();
|
||||
}
|
||||
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||
m_recentCharCount = 0;
|
||||
m_typingTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_recentCharCount > Settings::generalSettings().autoCompletionCharThreshold()) {
|
||||
scheduleRequest(widget);
|
||||
}
|
||||
});
|
||||
QTextCursor cursor = widget->textCursor();
|
||||
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||
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)
|
||||
@ -125,14 +153,26 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
||||
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;
|
||||
}
|
||||
|
||||
MultiTextCursor cursor = editor->multiTextCursor();
|
||||
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
||||
return;
|
||||
|
||||
const FilePath filePath = editor->textDocument()->filePath();
|
||||
GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
|
||||
documentVersion(filePath),
|
||||
Position(cursor.mainCursor())}};
|
||||
GetCompletionRequest request{
|
||||
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
|
||||
documentVersion(filePath),
|
||||
Position(cursor.mainCursor())}};
|
||||
if (Settings::codeCompletionSettings().showProgressWidget()) {
|
||||
m_progressHandler.showProgress(editor);
|
||||
}
|
||||
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
||||
const GetCompletionRequest::Response &response) {
|
||||
QTC_ASSERT(editor, return);
|
||||
@ -142,6 +182,35 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
||||
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)
|
||||
{
|
||||
cancelRunningRequest(editor);
|
||||
@ -154,7 +223,8 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||
if (editor
|
||||
&& editor->textCursor().position()
|
||||
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
|
||||
&& m_recentCharCount > Settings::generalSettings().autoCompletionCharThreshold())
|
||||
&& m_recentCharCount
|
||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold())
|
||||
requestCompletions(editor);
|
||||
});
|
||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||
@ -168,10 +238,10 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||
}
|
||||
|
||||
it.value()->setProperty("cursorPosition", editor->textCursor().position());
|
||||
it.value()->start(Settings::generalSettings().startSuggestionTimer());
|
||||
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
|
||||
}
|
||||
void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
|
||||
TextEditor::TextEditorWidget *editor)
|
||||
void QodeAssistClient::handleCompletions(
|
||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
||||
{
|
||||
if (response.error())
|
||||
log(*response.error());
|
||||
@ -191,8 +261,8 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
||||
auto isValidCompletion = [](const Completion &completion) {
|
||||
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
||||
};
|
||||
QList<Completion> completions = Utils::filtered(result->completions().toListOrEmpty(),
|
||||
isValidCompletion);
|
||||
QList<Completion> completions
|
||||
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
||||
|
||||
// remove trailing whitespaces from the end of the completions
|
||||
for (Completion &completion : completions) {
|
||||
@ -209,10 +279,19 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
||||
if (delta > 0)
|
||||
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())
|
||||
return;
|
||||
editor->insertSuggestion(
|
||||
std::make_unique<LLMSuggestion>(completions.first(), editor->document()));
|
||||
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,13 +300,18 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
||||
const auto it = m_runningRequests.constFind(editor);
|
||||
if (it == m_runningRequests.constEnd())
|
||||
return;
|
||||
m_progressHandler.hideProgress();
|
||||
cancelRequest(it->id());
|
||||
m_runningRequests.erase(it);
|
||||
}
|
||||
|
||||
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()
|
||||
@ -237,18 +321,13 @@ void QodeAssistClient::setupConnections()
|
||||
openDocument(textDocument);
|
||||
};
|
||||
|
||||
m_documentOpenedConnection = connect(EditorManager::instance(),
|
||||
&EditorManager::documentOpened,
|
||||
this,
|
||||
openDoc);
|
||||
m_documentClosedConnection = connect(EditorManager::instance(),
|
||||
&EditorManager::documentClosed,
|
||||
this,
|
||||
[this](IDocument *document) {
|
||||
if (auto textDocument = qobject_cast<TextDocument *>(
|
||||
document))
|
||||
closeDocument(textDocument);
|
||||
});
|
||||
m_documentOpenedConnection
|
||||
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc);
|
||||
m_documentClosedConnection = connect(
|
||||
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) {
|
||||
if (auto textDocument = qobject_cast<TextDocument *>(document))
|
||||
closeDocument(textDocument);
|
||||
});
|
||||
|
||||
for (IDocument *doc : DocumentModel::openedDocuments())
|
||||
openDoc(doc);
|
||||
@ -263,4 +342,32 @@ void QodeAssistClient::cleanupConnections()
|
||||
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
|
||||
|
@ -1,8 +1,8 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of Qode Assist.
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
@ -24,32 +24,40 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <languageclient/client.h>
|
||||
|
||||
#include "LLMClientInterface.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 {
|
||||
|
||||
class QodeAssistClient : public LanguageClient::Client
|
||||
{
|
||||
public:
|
||||
explicit QodeAssistClient();
|
||||
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||
~QodeAssistClient() override;
|
||||
|
||||
void openDocument(TextEditor::TextDocument *document) override;
|
||||
bool canOpenProject(ProjectExplorer::Project *project) override;
|
||||
|
||||
void requestCompletions(TextEditor::TextEditorWidget *editor);
|
||||
void requestQuickRefactor(
|
||||
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
||||
|
||||
private:
|
||||
void scheduleRequest(TextEditor::TextEditorWidget *editor);
|
||||
void handleCompletions(const GetCompletionRequest::Response &response,
|
||||
TextEditor::TextEditorWidget *editor);
|
||||
void handleCompletions(
|
||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
|
||||
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
|
||||
bool isEnabled(ProjectExplorer::Project *project) const;
|
||||
|
||||
void setupConnections();
|
||||
void cleanupConnections();
|
||||
void handleRefactoringResult(const RefactorResult &result);
|
||||
|
||||
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
||||
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
||||
@ -58,6 +66,10 @@ private:
|
||||
|
||||
QElapsedTimer m_typingTimer;
|
||||
int m_recentCharCount;
|
||||
CompletionProgressHandler m_progressHandler;
|
||||
EditorChatButtonHandler m_chatButtonHandler;
|
||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||
LLMClientInterface *m_llmClient;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
|
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
|
374
README.md
374
README.md
@ -1,113 +1,264 @@
|
||||
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
||||
[](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.
|
||||
|
||||
Code completion:
|
||||
⚠️ **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
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
|
||||
## Table of Contents
|
||||
1. [Overview](#overview)
|
||||
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
|
||||
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
|
||||
4. [Configure for OpenAI](#configure-for-openai)
|
||||
5. [Configure for Mistral AI](#configure-for-mistral-ai)
|
||||
6. [Configure for Google AI](#configure-for-google-ai)
|
||||
7. [Configure for Ollama](#configure-for-ollama)
|
||||
8. [Configure for llama.cpp](#configure-for-llamacpp)
|
||||
9. [System Prompt Configuration](#system-prompt-configuration)
|
||||
10. [File Context Feature](#file-context-feature)
|
||||
11. [Quick Refactoring Feature](#quick-refactoring-feature)
|
||||
12. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
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
|
||||
|
||||
Chat with LLM models in side panels:
|
||||
- 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:
|
||||
- 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:
|
||||
- Ollama
|
||||
- llama.cpp
|
||||
- OpenAI
|
||||
- Anthropic Claude
|
||||
- LM Studio
|
||||
- Mistral AI
|
||||
- Google AI
|
||||
- OpenAI-compatible providers(eg. llama.cpp, https://openrouter.ai)
|
||||
- Extensive library of model-specific templates
|
||||
- Easy configuration and model selection
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/ead5a5d9-b40a-4f17-af05-77fa2bcb3a61" width="600" alt="QodeAssistChat">
|
||||
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!
|
||||
|
||||
Chat with LLM models in bottom panel:
|
||||
<details>
|
||||
<summary>Code completion: (click to expand)</summary>
|
||||
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
|
||||
</details>
|
||||
|
||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
||||
<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>
|
||||
|
||||
## Plugin installation using Ollama as an example
|
||||
<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>
|
||||
|
||||
1. Install Latest QtCreator
|
||||
2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
||||
3. Install a language models in Ollama via terminal. For example, you can run:
|
||||
<details>
|
||||
<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">
|
||||
</details>
|
||||
|
||||
For suggestions:
|
||||
```
|
||||
ollama run codellama:7b-code
|
||||
```
|
||||
For chat:
|
||||
```
|
||||
ollama run codellama:7b-instruct
|
||||
```
|
||||
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...
|
||||
<details>
|
||||
<summary>Chat with LLM models in bottom panel: (click to expand)</summary>
|
||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
||||
</details>
|
||||
|
||||
<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>
|
||||
|
||||
## Install plugin to QtCreator
|
||||
1. Install Latest Qt Creator
|
||||
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 Plugin
|
||||
## 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>
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/00ad980f-b470-48eb-9aaa-077783d38798" width="600" alt="Configuere QodeAssist">
|
||||
## 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>
|
||||
|
||||
1. Open Qt Creator settings
|
||||
2. Navigate to the "Qode Assist" tab
|
||||
3. Select "General" page
|
||||
4. Choose your LLM provider (e.g., Ollama)
|
||||
5. Select the installed model by the "Select Model" button
|
||||
- For LM Studio you will see current loaded model
|
||||
6. Choose the prompt template that corresponds to your model
|
||||
7. Apply the settings
|
||||
## 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):
|
||||
```
|
||||
ollama run qwen2.5-coder:7b
|
||||
```
|
||||
For better performance (16GB+ RAM):
|
||||
```
|
||||
ollama run qwen2.5-coder:14b
|
||||
```
|
||||
For high-end systems (32GB+ RAM):
|
||||
```
|
||||
ollama run qwen2.5-coder:32b
|
||||
```
|
||||
|
||||
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
|
||||
2. Navigate to the "QodeAssist" tab
|
||||
3. On the "General" page, verify:
|
||||
- Ollama is selected as your LLM provider
|
||||
- The URL is set to http://localhost:11434
|
||||
- Your installed model appears in the model selection
|
||||
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
|
||||
4. Click Apply if you made any changes
|
||||
|
||||
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>
|
||||
|
||||
[](https://ko-fi.com/P5P412V96G)
|
||||
## Configure for llama.cpp
|
||||
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||
2. Go to General tab and configure:
|
||||
- Set "llama.cpp" as the provider for code completion or/and chat assistant
|
||||
- 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>
|
||||
|
||||
## Supported LLM Providers
|
||||
QodeAssist currently supports the following LLM (Large Language Model) providers:
|
||||
- [Ollama](https://ollama.com)
|
||||
- [LM Studio](https://lmstudio.ai)
|
||||
- OpenAI compatible providers
|
||||
## System Prompt Configuration
|
||||
|
||||
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.
|
||||
|
||||
## File Context Feature
|
||||
|
||||
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.
|
||||
|
||||
### Attached Files
|
||||
|
||||
Attachments are designed for one-time code analysis and specific queries:
|
||||
- Files are included only in the current message
|
||||
- Content is discarded after the message is processed
|
||||
- Ideal for:
|
||||
- Getting specific feedback on code changes
|
||||
- Code review requests
|
||||
- Analyzing isolated code segments
|
||||
- Quick implementation questions
|
||||
- Files can be attached using the paperclip icon in the chat interface
|
||||
- Multiple files can be attached to a single message
|
||||
|
||||
### Linked Files
|
||||
|
||||
Linked files provide persistent context throughout the conversation:
|
||||
|
||||
- Files remain accessible for the entire chat session
|
||||
- Content is included in every message exchange
|
||||
- Files are automatically refreshed - always using latest content from disk
|
||||
- Perfect for:
|
||||
- Long-term refactoring discussions
|
||||
- Complex architectural changes
|
||||
- Multi-file implementations
|
||||
- Maintaining context across related questions
|
||||
- Can be managed using the link icon in the chat interface
|
||||
- Supports automatic syncing with open editor files (can be enabled in settings)
|
||||
- Files can be added/removed at any time during the conversation
|
||||
|
||||
## 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 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.1 - 0.2.2 plugin version and below
|
||||
|
||||
## Supported Models
|
||||
|
||||
QodeAssist has been thoroughly tested and optimized for use with the following language models:
|
||||
|
||||
- Llama
|
||||
- CodeLlama
|
||||
- StarCoder2
|
||||
- DeepSeek-Coder-V2
|
||||
- Qwen-2.5
|
||||
|
||||
These models have demonstrated excellent performance in code completion and assistance tasks within the QodeAssist environment.
|
||||
|
||||
### Tested Models
|
||||
|
||||
#### Ollama:
|
||||
- [starcoder2](https://ollama.com/library/starcoder2)
|
||||
- [codellama](https://ollama.com/library/codellama)
|
||||
|
||||
#### LM Studio:
|
||||
- [second-state/StarCoder2-7B-GGUF](https://huggingface.co/second-state/StarCoder2-7B-GGUF)
|
||||
- [TheBloke/CodeLlama-7B-GGUF](https://huggingface.co/TheBloke/CodeLlama-7B-GGUF)
|
||||
|
||||
Please note that while these models have been specifically tested and confirmed to work well with QodeAssist, other models compatible with the supported providers may also work. We encourage users to experiment with different models and report their experiences.
|
||||
|
||||
If you've successfully used a model that's not listed here, please let us know by opening an issue or submitting a pull request to update this list.
|
||||
|
||||
### Custom Prompts
|
||||
|
||||
For advanced users or those with specific requirements, QodeAssist offers the flexibility to create, save, and load custom prompts using JSON templates. This feature allows you to tailor the AI's behavior to your exact needs.
|
||||
|
||||
To get started with custom prompts:
|
||||
|
||||
1. Navigate to the "Custom Template" option in the FIM Prompt Settings.
|
||||
2. Create your custom JSON prompt template.
|
||||
3. Use the "Save Custom Template to JSON" button to store your template for future use.
|
||||
4. To use a previously saved template, click "Load Custom Template from JSON".
|
||||
5. Make sure to select "Custom Template" from the dropdown menu in the FIM Prompt Settings on the General page to activate your custom template.
|
||||
|
||||
For inspiration and examples of effective custom prompts, please refer to the `rawPromptExamples` folder in our repository.
|
||||
|
||||
<img width="600" alt="Custom template" src="https://github.com/user-attachments/assets/4a14c552-baba-4531-ab4f-cb1f9ac6620b">
|
||||
<img width="600" alt="Select custom template" src="https://github.com/user-attachments/assets/3651dafd-83f9-4df9-943f-69c28cd3d8a3">
|
||||
|
||||
## Development Progress
|
||||
|
||||
- [x] Basic plugin with code autocomplete functionality
|
||||
@ -116,16 +267,44 @@ For inspiration and examples of effective custom prompts, please refer to the `r
|
||||
- [x] Sharing diff with model
|
||||
- [ ] Sharing project source with model
|
||||
- [ ] Support for more providers and models
|
||||
- [ ] Support MCP
|
||||
|
||||
## Hotkeys
|
||||
|
||||
- To call manual request to suggestion, you can use or change it in settings
|
||||
- on Mac: Option + Command + 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 line by line, you can use the "Move cursor word right" shortcut:
|
||||
- On Mac: Option + Right Arrow
|
||||
- On Windows: Alt + Right Arrow
|
||||
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
|
||||
- To call Quick Refactor dialog, select some code or place cursor and press
|
||||
- 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
|
||||
|
||||
@ -136,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 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
|
||||
- For Ollama, it should be /api/generate
|
||||
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
|
||||
Ensure you've chosen the correct model in the "Select Models" option
|
||||
Verify that the selected prompt template matches the model you're using
|
||||
|
||||
3. Confirm that the selected model and template are compatible:
|
||||
|
||||
Ensure you've chosen the correct model in the "Select Models" option
|
||||
Verify that the selected prompt template matches the model you're using
|
||||
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
|
||||
|
||||
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
|
||||
1. Open Qt Creator settings
|
||||
2. Navigate to the "Qode Assist" tab
|
||||
2. Navigate to the "QodeAssist" tab
|
||||
3. Pick settings page for reset
|
||||
4. Click on the "Reset Page to Defaults" button
|
||||
- The API key will not reset
|
||||
@ -165,7 +341,6 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||
|
||||
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
|
||||
- [](https://ko-fi.com/P5P412V96G)
|
||||
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
|
||||
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
|
||||
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
|
||||
@ -184,3 +359,12 @@ where `<path_to_qtcreator>` is the relative or absolute path to a Qt Creator bui
|
||||
combined binary and development package (Windows / Linux), or to the `Qt Creator.app/Contents/Resources/`
|
||||
directory of a combined binary and development package (macOS), and `<path_to_plugin_source>` is the
|
||||
relative or absolute path to this plugin directory.
|
||||
|
||||
## For Contributors
|
||||
|
||||
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
|
||||
|
||||

|
||||

|
||||
|
||||
|
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.
|
||||
*
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "chatview/ChatWidget.hpp"
|
||||
#include "ChatView/ChatWidget.hpp"
|
||||
#include <coreplugin/ioutputpane.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -19,20 +19,19 @@
|
||||
|
||||
#include "NavigationPanel.hpp"
|
||||
|
||||
#include "chatview/ChatWidget.hpp"
|
||||
#include "ChatView/ChatWidget.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
NavigationPanel::NavigationPanel() {
|
||||
NavigationPanel::NavigationPanel()
|
||||
{
|
||||
setDisplayName(tr("QodeAssist Chat"));
|
||||
setPriority(500);
|
||||
setId("QodeAssistChat");
|
||||
setActivationSequence(QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C));
|
||||
}
|
||||
|
||||
NavigationPanel::~NavigationPanel()
|
||||
{
|
||||
}
|
||||
NavigationPanel::~NavigationPanel() {}
|
||||
|
||||
Core::NavigationView NavigationPanel::createWidget()
|
||||
{
|
||||
@ -42,4 +41,4 @@ Core::NavigationView NavigationPanel::createWidget()
|
||||
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.
|
||||
*
|
||||
@ -19,8 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <coreplugin/inavigationwidgetfactory.h>
|
||||
#include <QObject>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@ -34,4 +34,4 @@ public:
|
||||
Core::NavigationView createWidget() override;
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace QodeAssist::Chat
|
||||
|
@ -1,179 +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 "ChatModel.hpp"
|
||||
#include <QtCore/qjsonobject.h>
|
||||
#include <QtQml>
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatModel::ChatModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_totalTokens(0)
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(&settings.chatTokensThreshold,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatModel::tokensThresholdChanged);
|
||||
}
|
||||
|
||||
int ChatModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_messages.size();
|
||||
}
|
||||
|
||||
QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_messages.size())
|
||||
return QVariant();
|
||||
|
||||
const Message &message = m_messages[index.row()];
|
||||
switch (static_cast<Roles>(role)) {
|
||||
case Roles::RoleType:
|
||||
return QVariant::fromValue(message.role);
|
||||
case Roles::Content: {
|
||||
return message.content;
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ChatModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[Roles::RoleType] = "roleType";
|
||||
roles[Roles::Content] = "content";
|
||||
return roles;
|
||||
}
|
||||
|
||||
QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
||||
{
|
||||
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::addMessage(const QString &content, ChatRole role)
|
||||
{
|
||||
int tokenCount = estimateTokenCount(content);
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
m_messages.append({role, content, tokenCount});
|
||||
m_totalTokens += tokenCount;
|
||||
endInsertRows();
|
||||
trim();
|
||||
emit totalTokensChanged();
|
||||
}
|
||||
|
||||
void ChatModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_messages.clear();
|
||||
m_totalTokens = 0;
|
||||
endResetModel();
|
||||
emit totalTokensChanged();
|
||||
}
|
||||
|
||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||
{
|
||||
QList<MessagePart> parts;
|
||||
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
||||
int lastIndex = 0;
|
||||
auto blockMatches = codeBlockRegex.globalMatch(content);
|
||||
|
||||
while (blockMatches.hasNext()) {
|
||||
auto match = blockMatches.next();
|
||||
if (match.capturedStart() > lastIndex) {
|
||||
QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||
if (!textBetween.isEmpty()) {
|
||||
parts.append({MessagePart::Text, textBetween, ""});
|
||||
}
|
||||
}
|
||||
parts.append({MessagePart::Code, match.captured(2).trimmed(), match.captured(1)});
|
||||
lastIndex = match.capturedEnd();
|
||||
}
|
||||
|
||||
if (lastIndex < content.length()) {
|
||||
QString remainingText = content.mid(lastIndex).trimmed();
|
||||
if (!remainingText.isEmpty()) {
|
||||
parts.append({MessagePart::Text, remainingText, ""});
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
QJsonArray ChatModel::prepareMessagesForRequest(LLMCore::ContextData context) const
|
||||
{
|
||||
QJsonArray messages;
|
||||
|
||||
messages.append(QJsonObject{{"role", "system"}, {"content", context.systemPrompt}});
|
||||
|
||||
for (const auto &message : m_messages) {
|
||||
QString role;
|
||||
switch (message.role) {
|
||||
case ChatRole::User:
|
||||
role = "user";
|
||||
break;
|
||||
case ChatRole::Assistant:
|
||||
role = "assistant";
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
messages.append(QJsonObject{{"role", role}, {"content", message.content}});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
int ChatModel::totalTokens() const
|
||||
{
|
||||
return m_totalTokens;
|
||||
}
|
||||
|
||||
int ChatModel::tokensThreshold() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.chatTokensThreshold();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -1,127 +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 "ChatRootView.hpp"
|
||||
#include <QtGui/qclipboard.h>
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_chatModel(new ChatModel(this))
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(&settings.chatModelName,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::currentTemplateChanged);
|
||||
generateColors();
|
||||
}
|
||||
|
||||
ChatModel *ChatRootView::chatModel() const
|
||||
{
|
||||
return m_chatModel;
|
||||
}
|
||||
|
||||
QColor ChatRootView::backgroundColor() const
|
||||
{
|
||||
return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
||||
}
|
||||
|
||||
void ChatRootView::sendMessage(const QString &message) const
|
||||
{
|
||||
m_clientInterface->sendMessage(message);
|
||||
}
|
||||
|
||||
void ChatRootView::copyToClipboard(const QString &text)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
void ChatRootView::generateColors()
|
||||
{
|
||||
QColor baseColor = backgroundColor();
|
||||
bool isDarkTheme = baseColor.lightness() < 128;
|
||||
|
||||
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,
|
||||
float hueShift,
|
||||
float saturationMod,
|
||||
float lightnessMod)
|
||||
{
|
||||
float h, s, l, a;
|
||||
baseColor.getHslF(&h, &s, &l, &a);
|
||||
bool isDarkTheme = l < 0.5;
|
||||
|
||||
h = fmod(h + hueShift + 1.0, 1.0);
|
||||
|
||||
s = qBound(0.0f, s * saturationMod, 1.0f);
|
||||
|
||||
if (isDarkTheme) {
|
||||
l = qBound(0.0f, l * lightnessMod, 1.0f);
|
||||
} else {
|
||||
l = qBound(0.0f, l / lightnessMod, 1.0f);
|
||||
}
|
||||
|
||||
h = qBound(0.0f, h, 1.0f);
|
||||
s = qBound(0.0f, s, 1.0f);
|
||||
l = qBound(0.0f, l, 1.0f);
|
||||
a = qBound(0.0f, a, 1.0f);
|
||||
|
||||
return QColor::fromHslF(h, s, l, a);
|
||||
}
|
||||
|
||||
QString ChatRootView::currentTemplate() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.chatModelName();
|
||||
}
|
||||
|
||||
QColor ChatRootView::primaryColor() const
|
||||
{
|
||||
return m_primaryColor;
|
||||
}
|
||||
|
||||
QColor ChatRootView::secondaryColor() const
|
||||
{
|
||||
return m_secondaryColor;
|
||||
}
|
||||
|
||||
QColor ChatRootView::codeColor() const
|
||||
{
|
||||
return m_codeColor;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -1,75 +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 <QQuickItem>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||
Q_PROPERTY(QColor backgroundColor READ backgroundColor CONSTANT FINAL)
|
||||
Q_PROPERTY(QColor primaryColor READ primaryColor CONSTANT FINAL)
|
||||
Q_PROPERTY(QColor secondaryColor READ secondaryColor CONSTANT FINAL)
|
||||
Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL)
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
ChatRootView(QQuickItem *parent = nullptr);
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
QColor backgroundColor() const;
|
||||
QColor primaryColor() const;
|
||||
QColor secondaryColor() const;
|
||||
|
||||
QColor codeColor() const;
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message) const;
|
||||
void copyToClipboard(const QString &text);
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
|
||||
private:
|
||||
void generateColors();
|
||||
QColor generateColor(const QColor &baseColor,
|
||||
float hueShift,
|
||||
float saturationMod,
|
||||
float lightnessMod);
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
ClientInterface *m_clientInterface;
|
||||
QString m_currentTemplate;
|
||||
QColor m_primaryColor;
|
||||
QColor m_secondaryColor;
|
||||
QColor m_codeColor;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -1,13 +0,0 @@
|
||||
#include "ChatUtils.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
void ChatUtils::copyToClipboard(const QString &text)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
// Q_NAMESPACE
|
||||
|
||||
class ChatUtils : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_NAMED_ELEMENT(ChatUtils)
|
||||
|
||||
public:
|
||||
explicit ChatUtils(QObject *parent = nullptr)
|
||||
: QObject(parent) {};
|
||||
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -1,117 +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 "ClientInterface.hpp"
|
||||
#include "ContextSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_requestHandler(new LLMCore::RequestHandler(this))
|
||||
, m_chatModel(chatModel)
|
||||
{
|
||||
connect(m_requestHandler,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
[this](const QString &completion, const QJsonObject &, bool isComplete) {
|
||||
handleLLMResponse(completion, isComplete);
|
||||
});
|
||||
|
||||
connect(m_requestHandler,
|
||||
&LLMCore::RequestHandler::requestFinished,
|
||||
this,
|
||||
[this](const QString &, bool success, const QString &errorString) {
|
||||
if (!success) {
|
||||
emit errorOccurred(errorString);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientInterface::~ClientInterface() = default;
|
||||
|
||||
void ClientInterface::sendMessage(const QString &message)
|
||||
{
|
||||
LOG_MESSAGE("Sending message: " + message);
|
||||
LOG_MESSAGE("chatProvider " + Settings::generalSettings().chatLlmProviders.stringValue());
|
||||
LOG_MESSAGE("chatTemplate " + Settings::generalSettings().chatPrompts.stringValue());
|
||||
|
||||
auto chatTemplate = LLMCore::PromptTemplateManager::instance().getCurrentChatTemplate();
|
||||
auto chatProvider = LLMCore::ProvidersManager::instance().getCurrentChatProvider();
|
||||
|
||||
LLMCore::ContextData context;
|
||||
context.prefix = message;
|
||||
context.suffix = "";
|
||||
if (Settings::contextSettings().useChatSystemPrompt())
|
||||
context.systemPrompt = Settings::contextSettings().chatSystemPrompt();
|
||||
|
||||
QJsonObject providerRequest;
|
||||
providerRequest["model"] = Settings::generalSettings().chatModelName();
|
||||
providerRequest["stream"] = true;
|
||||
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(context);
|
||||
|
||||
chatTemplate->prepareRequest(providerRequest, context);
|
||||
chatProvider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Chat;
|
||||
config.provider = chatProvider;
|
||||
config.promptTemplate = chatTemplate;
|
||||
config.url = QString("%1%2").arg(Settings::generalSettings().chatUrl(),
|
||||
Settings::generalSettings().chatEndPoint());
|
||||
config.providerRequest = providerRequest;
|
||||
config.multiLineCompletion = Settings::generalSettings().multiLineCompletion();
|
||||
|
||||
QJsonObject request;
|
||||
request["id"] = QUuid::createUuid().toString();
|
||||
|
||||
m_accumulatedResponse.clear();
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User);
|
||||
m_requestHandler->sendLLMRequest(config, request);
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
{
|
||||
m_chatModel->clear();
|
||||
m_accumulatedResponse.clear();
|
||||
LOG_MESSAGE("Chat history cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::handleLLMResponse(const QString &response, bool isComplete)
|
||||
{
|
||||
m_accumulatedResponse += response;
|
||||
|
||||
if (isComplete) {
|
||||
LOG_MESSAGE("Message completed. Final response: " + m_accumulatedResponse);
|
||||
emit messageReceived(m_accumulatedResponse.trimmed());
|
||||
|
||||
m_chatModel->addMessage(m_accumulatedResponse.trimmed(), ChatModel::ChatRole::Assistant);
|
||||
m_accumulatedResponse.clear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -1,91 +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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import ChatView
|
||||
import "./dialog"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias msgModel: msgCreator.model
|
||||
property color fontColor
|
||||
property color codeBgColor
|
||||
property color selectionColor
|
||||
|
||||
height: msgColumn.height
|
||||
radius: 8
|
||||
|
||||
Column {
|
||||
id: msgColumn
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: msgCreator
|
||||
delegate: Loader {
|
||||
property var itemData: modelData
|
||||
|
||||
width: parent.width
|
||||
sourceComponent: {
|
||||
switch(modelData.type) {
|
||||
case MessagePart.Text: return textComponent;
|
||||
case MessagePart.Code: return codeBlockComponent;
|
||||
default: return textComponent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
|
||||
TextBlock {
|
||||
height: implicitHeight + 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 10
|
||||
text: itemData.text
|
||||
color: fontColor
|
||||
selectionColor: root.selectionColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Component {
|
||||
id: codeBlockComponent
|
||||
|
||||
CodeBlock {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
code: itemData.text
|
||||
language: itemData.language
|
||||
color: root.codeBgColor
|
||||
selectionColor: root.selectionColor
|
||||
}
|
||||
}
|
||||
}
|
@ -1,167 +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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Basic as QQC
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
ChatRootView {
|
||||
id: root
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
ListView {
|
||||
id: chatListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
leftMargin: 5
|
||||
model: root.chatModel
|
||||
clip: true
|
||||
spacing: 10
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 2000
|
||||
|
||||
delegate: ChatItem {
|
||||
width: ListView.view.width - scroll.width
|
||||
msgModel: root.chatModel.processMessageContent(model.content)
|
||||
color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor
|
||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
||||
codeBgColor: root.codeColor
|
||||
selectionColor: root.primaryColor.hslLightness > 0.5 ? Qt.darker(root.primaryColor, 1.5)
|
||||
: Qt.lighter(root.primaryColor, 1.5)
|
||||
|
||||
}
|
||||
|
||||
header: Item {
|
||||
width: ListView.view.width - scroll.width
|
||||
height: 30
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: scroll
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (atYEnd) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: view
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 30
|
||||
Layout.maximumHeight: root.height / 2
|
||||
|
||||
QQC.TextArea {
|
||||
id: messageInput
|
||||
|
||||
placeholderText: qsTr("Type your message here...")
|
||||
placeholderTextColor: "#888"
|
||||
color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
||||
background: Rectangle {
|
||||
radius: 2
|
||||
color: root.primaryColor
|
||||
border.color: root.primaryColor.hslLightness > 0.5 ? Qt.lighter(root.primaryColor, 1.5)
|
||||
: Qt.darker(root.primaryColor, 1.5)
|
||||
border.width: 1
|
||||
}
|
||||
Keys.onPressed: function(event) {
|
||||
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
|
||||
sendChatMessage()
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Button {
|
||||
id: sendButton
|
||||
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
text: qsTr("Send")
|
||||
onClicked: sendChatMessage()
|
||||
}
|
||||
Button {
|
||||
id: clearButton
|
||||
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
text: qsTr("Clear Chat")
|
||||
onClicked: clearChat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: bar
|
||||
|
||||
layoutDirection: Qt.RightToLeft
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: scroll.width
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
Badge {
|
||||
text: "%1/%2".arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
|
||||
color: root.codeColor
|
||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
root.chatModel.clear()
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
Qt.callLater(chatListView.positionViewAtEnd)
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
root.sendMessage(messageInput.text);
|
||||
messageInput.text = ""
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
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.
|
||||
*
|
||||
@ -18,10 +18,9 @@
|
||||
*/
|
||||
|
||||
#include "ChangesManager.h"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/ContextSettings.hpp"
|
||||
#include "CodeCompletionSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
ChangesManager &ChangesManager::instance()
|
||||
{
|
||||
@ -31,17 +30,12 @@ ChangesManager &ChangesManager::instance()
|
||||
|
||||
ChangesManager::ChangesManager()
|
||||
: QObject(nullptr)
|
||||
{
|
||||
}
|
||||
{}
|
||||
|
||||
ChangesManager::~ChangesManager()
|
||||
{
|
||||
}
|
||||
ChangesManager::~ChangesManager() {}
|
||||
|
||||
void ChangesManager::addChange(TextEditor::TextDocument *document,
|
||||
int position,
|
||||
int charsRemoved,
|
||||
int charsAdded)
|
||||
void ChangesManager::addChange(
|
||||
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded)
|
||||
{
|
||||
auto &documentQueue = m_documentChanges[document];
|
||||
|
||||
@ -52,16 +46,17 @@ void ChangesManager::addChange(TextEditor::TextDocument *document,
|
||||
|
||||
ChangeInfo change{fileName, lineNumber, lineContent};
|
||||
|
||||
auto it = std::find_if(documentQueue.begin(),
|
||||
documentQueue.end(),
|
||||
[lineNumber](const ChangeInfo &c) { return c.lineNumber == lineNumber; });
|
||||
auto it
|
||||
= std::find_if(documentQueue.begin(), documentQueue.end(), [lineNumber](const ChangeInfo &c) {
|
||||
return c.lineNumber == lineNumber;
|
||||
});
|
||||
|
||||
if (it != documentQueue.end()) {
|
||||
it->lineContent = lineContent;
|
||||
} else {
|
||||
documentQueue.enqueue(change);
|
||||
|
||||
if (documentQueue.size() > Settings::contextSettings().maxChangesCacheSize()) {
|
||||
if (documentQueue.size() > Settings::codeCompletionSettings().maxChangesCacheSize()) {
|
||||
documentQueue.dequeue();
|
||||
}
|
||||
}
|
||||
@ -80,4 +75,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *
|
||||
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.
|
||||
*
|
||||
@ -19,13 +19,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QQueue>
|
||||
#include <QTimer>
|
||||
#include <texteditor/textdocument.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
namespace QodeAssist::Context {
|
||||
|
||||
class ChangesManager : public QObject
|
||||
{
|
||||
@ -41,10 +41,8 @@ public:
|
||||
|
||||
static ChangesManager &instance();
|
||||
|
||||
void addChange(TextEditor::TextDocument *document,
|
||||
int position,
|
||||
int charsRemoved,
|
||||
int charsAdded);
|
||||
void addChange(
|
||||
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
|
||||
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
|
||||
|
||||
private:
|
||||
@ -58,4 +56,4 @@ private:
|
||||
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
|
||||
ProvidersManager.hpp ProvidersManager.cpp
|
||||
ContextData.hpp
|
||||
IPromptProvider.hpp
|
||||
IProviderRegistry.hpp
|
||||
PromptProviderChat.hpp
|
||||
PromptProviderFim.hpp
|
||||
PromptTemplate.hpp
|
||||
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
||||
RequestConfig.hpp
|
||||
RequestHandlerBase.hpp RequestHandlerBase.cpp
|
||||
RequestHandler.hpp RequestHandler.cpp
|
||||
OllamaMessage.hpp OllamaMessage.cpp
|
||||
OpenAIMessage.hpp OpenAIMessage.cpp
|
||||
ValidationUtils.hpp ValidationUtils.cpp
|
||||
ProviderID.hpp
|
||||
)
|
||||
|
||||
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.
|
||||
*
|
||||
@ -20,14 +20,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
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
|
||||
{
|
||||
QString prefix;
|
||||
QString suffix;
|
||||
QString systemPrompt;
|
||||
std::optional<QString> systemPrompt;
|
||||
std::optional<QString> prefix;
|
||||
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
|
||||
|
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
|
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