Compare commits

...

230 Commits
v0.3.9 ... main

Author SHA1 Message Date
Petr Mironychev
af3fdb58ff fix: Add custom endpoint to reset function 2025-05-18 20:06:46 +02:00
Petr Mironychev
637a4d9d4c
feat: Add custom providers endpoint (#188) 2025-05-17 09:21:06 +02:00
Petr Mironychev
7e2345773f
doc: Add support QtC 16.0.2 to README.md 2025-05-14 19:20:35 +02:00
Petr Mironychev
14a5ddbdd8 chore: Upgrade plugin to 0.5.13 2025-05-14 19:09:16 +02:00
Petr Mironychev
e178b7daa7
feat: Add multi QtCreator versions
* feat: Add multi qtc version
* feat Upgrade plugin to QtC 16.0.2
2025-05-14 16:06:15 +02:00
Petr Mironychev
4b353d5091
doc: Update targets in README.md 2025-05-10 15:09:35 +02:00
Petr Mironychev
f7ba7b95be
doc: Fix ignore files api in README.md 2025-05-02 08:58:31 +02:00
Petr Mironychev
6ae95fec45
doc: Added quick refactoring feature description to README.md 2025-05-02 08:31:34 +02:00
Petr Mironychev
dad8ab2bf3 chore: Update plugin to 0.5.12 2025-05-01 15:38:05 +02:00
Petr Mironychev
25a6983de0
refactor: Make connection more async (#182) 2025-05-01 15:35:33 +02:00
Petr Mironychev
4e05abc7d2 feat: Add settings for text format 2025-05-01 00:01:44 +02:00
Petr Mironychev
784529e344
feat: Add chat font settings (#180) 2025-04-30 22:44:59 +02:00
Petr Mironychev
155153a763 fix: Optimize searching unreadable symbols for markdown 2025-04-30 21:23:43 +02:00
Petr Mironychev
9225c0c1a9 fix: Check readable symbols for markdown 2025-04-29 21:55:44 +02:00
Petr Mironychev
43adc95857 feat: Add a floating "copy" button 2025-04-28 09:25:39 +02:00
Petr Mironychev
ee672f2cda refactor: Remove Chat preview scrollbar 2025-04-26 17:29:06 +02:00
Petr Mironychev
a3edb8a577 chore: Upgrade plugin to 0.5.11 2025-04-24 21:46:32 +02:00
Petr Mironychev
407d3b11c0 fix: Change maximum limit of chat tokens 2025-04-24 21:44:49 +02:00
Petr Mironychev
285e739074 refactor: Change base text style render to markdown 2025-04-24 21:38:54 +02:00
Petr Mironychev
f7e748ba7e chore: Upgrade plugin to 0.5.10 2025-04-24 03:24:00 +02:00
Petr Mironychev
acb1306321 fix: Improve detect unclose codeblock 2025-04-24 03:21:19 +02:00
Petr Mironychev
8b38ecc29b feat: Add Chat preview scroll bar 2025-04-24 02:54:21 +02:00
Petr Mironychev
cfb364f033 fix: Correct removing latest item in messages list 2025-04-24 01:32:01 +02:00
Petr Mironychev
2fe6850a06 refactor: Improve textsuggestion working 2025-04-24 01:25:45 +02:00
Petr Mironychev
3e9506ca92
doc: Add description of ignoring files feature to README.md 2025-04-21 09:40:34 +02:00
Petr Mironychev
d24adff0f5 chore: Update plugin version to 0.5.9 2025-04-21 09:18:02 +02:00
Petr Mironychev
447324eb07
feat: Make artifacts name more meaningful (#172) 2025-04-21 09:17:16 +02:00
Petr Mironychev
4ca494cc51 fix: Suggestion symbols count 2025-04-21 09:02:02 +02:00
Petr Mironychev
8a80dbe8f5 fix: Exclude ignore file from attach 2025-04-21 08:37:57 +02:00
Petr Mironychev
2b539bbdeb fix: Remove check ignoring file on open 2025-04-21 08:08:20 +02:00
Petr Mironychev
3f2c146df1 fix: Save codestral api key 2025-04-20 09:57:14 +02:00
Petr Mironychev
9a54f04a0d
feat: Add Codestral as separate ai provider (#171) 2025-04-20 09:48:36 +02:00
Petr Mironychev
7a33425d1a
feat: Add reset button to clean message list to specific message (#168) 2025-04-18 19:06:17 +02:00
Petr Mironychev
711aa672f2
fix: Increase mac threshold tokens for tokens (#167) 2025-04-18 17:56:48 +02:00
Petr Mironychev
8cb6a2f6d2
fix: Check patterns and remove filewatcher (#166)
* fix: Check patterns and remove filewatcher
* fix: Don't show log for non-existent ignore file
2025-04-18 10:55:46 +02:00
Petr Mironychev
2f9622e23e
Update details for Quick Refactor tool in README.md 2025-04-18 08:18:46 +02:00
Petr Mironychev
674b1fecde chore: Upgrade plugin version to 0.5.8 2025-04-17 10:40:10 +02:00
Petr Mironychev
b36d01d2c7
feat: Improve quick refactor dialog (#165) 2025-04-17 10:34:31 +02:00
Petr Mironychev
615175bea8
feat: Add file list for ignoring in request for llm (#163) 2025-04-17 09:12:47 +02:00
Petr Mironychev
7515599acb
doc: Add hotkey description for Quick Refactor to README.md 2025-04-14 14:38:12 +02:00
Petr Mironychev
3652d4d5d9
Fix QtCreator version compatibility 2025-04-14 10:54:01 +02:00
Petr Mironychev
75677770b2
Update QtCreator version compatibility README.md 2025-04-14 10:52:45 +02:00
Petr Mironychev
329a1efd5d
Update QtCreator version in README.md 2025-04-14 10:51:51 +02:00
Petr Mironychev
27760a3b99
doc: Add example of Quick Refactor to README.md 2025-04-14 01:59:49 +02:00
Petr Mironychev
a93b3cd7f5
chore: Upgrade plugin to QtCreator 16.0.1 (#162) 2025-04-14 01:32:51 +02:00
Petr Mironychev
bacde51d71
feat: Add quick refactor command via context menu (#161)
* feat: Add quick refactor command via context menu
* feat: Add settings for Quick Refactor
2025-04-14 01:01:44 +02:00
Petr Mironychev
418578743a feat: Prepare widget for chat 2025-04-09 18:27:25 +02:00
Petr Mironychev
56e5ef22f1
doc: Remove commercial support from README.md 2025-04-08 08:14:57 +02:00
Petr Mironychev
e90933d713 fix: Add hack for codellama fim models 2025-04-07 18:55:08 +02:00
Petr Mironychev
5b9c67c2d8
doc: Add sharing opened files feature description to README.md 2025-04-04 22:12:36 +02:00
Petr Mironychev
fe84a2a303 chore: Upgrade plugin version to 0.5.6 2025-04-04 18:39:18 +02:00
Petr Mironychev
62de53c306 chore: Update copyrights 2025-04-04 18:01:02 +02:00
Petr Mironychev
7c6a10936c
refactor: Simplify update mechanism (#159) 2025-04-04 17:55:36 +02:00
Petr Mironychev
032c9bbbf3 fix: Add input_extra to llama.cpp validator 2025-04-04 17:16:05 +02:00
Petr Mironychev
8906f98038
fix: Rework copyright searching (#158) 2025-04-04 15:29:36 +02:00
Agron
5126092449
doc: Minor Update README.md to include a shortcut on Linux (#157)
Added short-cut for Linux KDE Plasma
2025-04-04 14:37:36 +02:00
Petr Mironychev
9d2d70fc63
feat: Add sharing opened files with code completion requests (#156) 2025-04-04 10:38:06 +02:00
Petr Mironychev
ffaf6bd61b
feat: Add code completion request progress animation (#153) 2025-04-02 21:00:45 +02:00
Petr Mironychev
79218d8412
refactor: Replace singletone for context manager (#151) 2025-04-01 22:29:45 +02:00
Petr Mironychev
7e6e526ac8 refactor: Removed deprecated api keys fields 2025-03-27 02:12:07 +01:00
Petr Mironychev
80646e2af0
feat: Add additional language for handling to CodeCompletion settings (#150) 2025-03-27 02:07:09 +01:00
Petr Mironychev
5808a892c1 fix: Change expected name in test 2025-03-27 00:41:28 +01:00
Petr Mironychev
d58ff90458 fix: Fixed typo in the use of the project name 2025-03-27 00:34:10 +01:00
Petr Mironychev
7d06ab04dc feat: Add RunQtCreator target 2025-03-27 00:29:23 +01:00
Petr Mironychev
9d40e8ca25 chore: Upgrade plugin to 0.5.5 version 2025-03-20 19:02:11 +01:00
Mariusz Jaskółka
5b16c5403a
fix: Add authorization while getting installed models (#142) (#147) 2025-03-20 11:33:38 +01:00
Mariusz Jaskółka
4ddbe0b8b9
feat: Support Ollama authorization via BaseAuth (#145) (#146) 2025-03-20 11:14:50 +01:00
Petr Mironychev
f41e063c02 chore: Upgrade plugin version to 0.5.4 2025-03-17 02:51:31 +01:00
Petr Mironychev
9d7d084448
fix: Wrong template replace to first template (#143) 2025-03-17 02:48:18 +01:00
Petr Mironychev
1ca1ffc629
fix: Remove reading from replay leading to crash (#142) 2025-03-17 01:22:27 +01:00
Petr Mironychev
8419577ae5
fix: Resolve thread-related QNetworkAccessManager issue (#140)
Fixes "QObject: Cannot create children for a parent that is in a different thread" error by creating QNetworkAccessManager in the same thread where it's used, ensuring proper thread affinity for network operations.
2025-03-16 09:47:04 +01:00
Petr Mironychev
91a6a88130
doc: Add default path for installed plugin 2025-03-14 11:35:37 +01:00
Petr Mironychev
be38abc505
chore: upgrade plugin to 0.5.3 (#139) 2025-03-14 10:59:23 +01:00
Petr Mironychev
f2e0afb6b8
fix: Add qml in code handler for processing model answers (#138) 2025-03-14 10:47:30 +01:00
Petr Mironychev
3cf07238fd
doc: Update QtC version to 16 2025-03-14 09:32:54 +01:00
Petr Mironychev
b98f85a997
chore: Upgrade plugin to QtCreator 16 (#136) 2025-03-13 16:46:32 +01:00
Petr Mironychev
085659483f
doc: Add info about linux compatibility to README.md 2025-03-11 08:34:14 +01:00
Povilas Kanapickas
8a1fd5438e
chore: Add tests for LLMClientInterface (#131) 2025-03-10 21:54:17 +01:00
Povilas Kanapickas
78f69e82a5
chore: Checkout submodules when building (#133) 2025-03-10 19:17:30 +01:00
Povilas Kanapickas
3d770f91c7
refactor: Reduce dependency on TextDocument in ContextManager (#128) 2025-03-10 18:06:19 +01:00
Povilas Kanapickas
c724bace06
refactor: Move document access out of prepareContext() (#129) 2025-03-10 17:54:03 +01:00
Povilas Kanapickas
719065ebfc
refactor: Extract document reading to separate class (#127)
This decouples LLMClientInterface from Qt Creator text editor
implementation and allows to write tests
2025-03-10 17:42:40 +01:00
Povilas Kanapickas
a218064a4f
refactor: Introduce base class for RequestHandler (#125)
This will make it possible to write a mock implementation.
2025-03-10 17:29:45 +01:00
Povilas Kanapickas
13cd12b00a
chore: Add 3rdparty/inja dependency (#126) 2025-03-10 17:28:25 +01:00
Povilas Kanapickas
ed59be4199
refactor: Extract performance logging to separate class (#124)
This should not be responsibility of LLMClientInterface. Extracting this
class also adds flexibility to silence logging output in tests.
2025-03-10 17:10:01 +01:00
Povilas Kanapickas
7dd8b3d085
fix: Make build CMakeLists.txt standalone (#123)
Previously it depended on QODEASSIST_QT_CREATOR_VERSION_* flags being
passed to cmake during build process. Making it standalone saves time
for the users.
2025-03-10 17:00:07 +01:00
Povilas Kanapickas
3839d6896c
refactor: Pass LLMClientInterface to QodeAssistClient (#122)
Contructing LLMClientInterface in constructor of QodeAssistClient when
initializing base class severely limits what can be done. In particular,
no members can be referred to, because nothing of the class instance
itself has been initialized at that point of time.
2025-03-10 16:56:27 +01:00
Petr Mironychev
6b86637dcb
doc: Add llama.cpp description to README.md 2025-03-10 12:07:39 +01:00
Povilas Kanapickas
58c3e26e7f
refactor: Decouple LLMClientInterface from ProvidersManager (#120)
This will be needed for tests.
2025-03-10 10:40:51 +01:00
Povilas Kanapickas
98e1047bf1
refactor: Decouple prompt template manager from their users (#115)
This makes it possible to test the user classes
2025-03-10 02:13:10 +01:00
Petr Mironychev
b6f36d61ae
fix: Replace ubuntu-latest to ubuntu 22.04 (#119) 2025-03-10 01:33:03 +01:00
Petr Mironychev
f2f453ccc8 chore: Upgrade plugin to 0.5.1 2025-03-10 01:00:33 +01:00
Petr Mironychev
1bcfd749d5 fix: Change OpenAI model parser conditions 2025-03-10 00:47:16 +01:00
Petr Mironychev
e66f467214
feat: Add llama.cpp provider and fim template (#118) 2025-03-09 22:57:33 +01:00
Povilas Kanapickas
c9a3cdaf25
refactor: Reuse extractFilePathFromRequest() more (#117) 2025-03-08 16:18:44 +01:00
Povilas Kanapickas
7c483f89cd
chore: Replace deprecated FilePath::toString() with toFSPathString() (#116)
This solves a number of deprecation warnings during build.
FilePath::toFSPathString() has been available since couple of years ago.
2025-03-08 16:08:33 +01:00
Povilas Kanapickas
6c323642e4
refactor: Inject settings into LLMClientInterface (#114)
This reduces reliance on global state and makes it more possible to test
the code.
2025-03-08 15:08:15 +01:00
Povilas Kanapickas
3a494d5254
chore: Remove duplicate label setting for systemPromptForNonFimModels (#111)
The same call is several lines below.

Note that systemPrompt does not need label due to a similarly named
checkbox placed nearby. Thus users intuitively know what the text box is
for.
2025-03-08 10:40:22 +01:00
Povilas Kanapickas
44b3b0cc0c
refactor: Don't use global state in ContextManager::isSpecifyCompletion (#112)
Using global state makes testing things way harder.
2025-03-08 10:38:52 +01:00
Povilas Kanapickas
3aae923d43
feat: Improve describe recent changes in system prompt (#113)
Co-authored-by: Petr Mironychev <9195189+Palm1r@users.noreply.github.com>
2025-03-08 08:58:28 +01:00
Povilas Kanapickas
f94c79a5ff
fix: Improve support for code blocks without language (#108)
This makes it possible to represent code blocks in models that emit
their suggestion immediately after the ``` characters.
2025-03-07 15:30:22 +01:00
Povilas Kanapickas
9a5047618d
chore: Silence Qt warnings during tests (#110) 2025-03-07 01:58:09 +01:00
Povilas Kanapickas
90beebf2ee
Revert "refactor: Move all processing logic to CodeHandler::processText()" (#109) 2025-03-07 01:57:13 +01:00
Povilas Kanapickas
521261e0a3
refactor: Move all processing logic to CodeHandler::processText() (#107)
This will become useful once more processing modes are available
2025-03-06 18:49:28 +01:00
Povilas Kanapickas
5536de146c
chore: Remove dead code in RequestHandler (#106) 2025-03-06 14:19:01 +01:00
Povilas Kanapickas
81ac3c71fb
chore: Add tests for CodeHandler (#105)
* chore: Extract test utils to separate file

This makes it possible to reuse the utils in other test files.

* chore: Add tests for CodeHandler
2025-03-06 13:45:12 +01:00
Povilas Kanapickas
61ca5c9a1b
fix: Properly omit copyright information (#103)
This commit ensures that copyright information is always excluded and
that context is always split into prefix and suffix at correct position.
2025-03-06 13:00:15 +01:00
Povilas Kanapickas
8a167bf248
chore: Expand DocumentContextReaderTest to cover more conditions (#102)
Tests should have "before" and "after" cases side by side, so that it's
possible to easily verify that full context is extracted correctly.

Also tests should consistently cover the same conditions in all the
different scenarios.
2025-03-06 12:08:57 +01:00
Povilas Kanapickas
ab97f39ea4
chore: Fix pretty printing for QString (#101)
Previously QString was printed symbol by symbol. E.g.:

reader.readWholeFileAfter(3, 1)
    Which is: { "i", "n", "e", " ", "2", "
", "L", "i", "n", "e", " ", "3" }
2025-03-06 12:08:16 +01:00
Petr Mironychev
0d3493e7f6
feat: Change linux build base to ubuntu 22.04 (#99) 2025-03-06 11:55:04 +01:00
Povilas Kanapickas
1d062e1fe4
chore: Use zero-based line numbers in tests (#100)
This makes the line index arguments passed to functions match the actual
test data, thus tests are less confusing.
2025-03-06 11:53:27 +01:00
Povilas Kanapickas
5dceb5cd19
fix: Bring back old behavior of readStrings{After,Before}Cursor setting (#97) 2025-03-05 20:27:16 +01:00
Povilas Kanapickas
69a8aa80d9
refactor: Make DocumentContextReader::prepareContext() testable (#96) 2025-03-05 20:18:59 +01:00
Povilas Kanapickas
e218699e64
refactor: Reuse getContext{Before,After}() instead of duplicating logic (#95)
getContextAfter(int lineNumber, int cursorPosition) and
getContextBefore(int lineNumber, int cursorPosition) are currently not
tested. Thus as little logic as possible should live there.
2025-03-05 19:43:51 +01:00
Povilas Kanapickas
3dc0d910bf
fix: Fix off by one errors in getContext{Before,After}() (#94)
This also specifies what exactly getContext*() functions do. Before this
commit linesCount was sometimes interpreted as exclusive of current
line, which was confusing as linesCount + 1 lines were being returned.
2025-03-05 19:32:53 +01:00
Povilas Kanapickas
f9f2a86cea
fix: Correctly pick whole file context (#85)
Currently the current line is duplicated in both "before" and "after"
context. This is due to DocumentContextReader::readWholeFileAfter()
picking the current line part of which has been already included into
the "before" context.
2025-03-05 19:17:51 +01:00
Povilas Kanapickas
247256d4a4
chore: Add unit tests for DocumentContextReader (#90)
chore: Add unit tests for DocumentContextReader

The tests are based on GTest like some tests in Qt Creator itself, which
makes it easy to run as full Qt Creator does not need to be started.
2025-03-05 15:01:52 +01:00
Povilas Kanapickas
bcf7b6c226
refactor: Make DocumentContextReader usable outside Qt Creator context (#89)
This makes it possible to write simple unit tests for it without running
full Qt Creator. Not coupling DocumentContextReader to
TextEditor::TextDocument unnecessarily is also a better design in
general.
2025-03-05 01:53:02 +01:00
Povilas Kanapickas
29a3939c64
chore: Add Github Actions job for checking formatting (#92) 2025-03-05 01:46:17 +01:00
Povilas Kanapickas
cb3464170e
chore: Run clang-format over the codebase (#91)
This commit is a result of the following commands:

clang-format-19 --style=file -i $(git ls-files | fgrep .cpp)
clang-format-19 --style=file -i $(git ls-files | fgrep .hpp)
2025-03-05 01:45:15 +01:00
Povilas Kanapickas
ca0fb5efbb
fix: Make plugin registration be compatible with Qt Creator 16 (#80)
This introduces changes needed after the following commit in Qt Creator:

ba5e4b7eff

Core: Provide settings categories centrally
2025-03-04 11:29:30 +01:00
Povilas Kanapickas
d8a01504a3
chore: Add support for Qt Creator version to the plugin build script (#87)
This will allow to add code conditional on the Qt Creator version to the
plugin codebase. The Qt Creator version will be passed from the build
script automatically. This will also allow to easily extend the Github
Actions job matrix to create releases for more than one Qt Creator
version.

Using QT_VERSION_CHECK allows to reuse existing Qt patterns of checking
versions.

Code has been tested by invoking QODEASSIST_QT_CREATOR_VERSION in code.
2025-03-04 11:17:19 +01:00
Povilas Kanapickas
3b188740e8
fix: Make settings dialogs button order consistent (#84)
Currently on Linux most dialogs follow the following order of action
buttons: "OK", "Cancel" (left to right). However, several dialogs are
constructed explicitly and don't follow this convention.

This commit fixes this discrepancy.

Fixes: https://github.com/Palm1r/QodeAssist/issues/83
2025-03-03 19:12:01 +01:00
Povilas Kanapickas
0d22e1866e
fix: Add tooltip about where log messages can be seen (#86) 2025-03-03 19:00:25 +01:00
Povilas Kanapickas
61196cae90
chore: Run clang-format over the codebase (#82)
This commit is a result of the following commands:

clang-format-19 --style=file -i $(git ls-files | fgrep .cpp)
clang-format-19 --style=file -i $(git ls-files | fgrep .hpp)
2025-03-02 22:44:20 +01:00
Povilas Kanapickas
102bb114a1
refactor: Remove duplicate constants from SettingsConstants.hpp (#81)
These are duplicated in QodeAssistConstants.hpp. This prevents inclusion
of both files into any source file anywhere in the project.
2025-03-02 22:25:25 +01:00
Petr Mironychev
e507d7ee17
doc: Add discord link with description 2025-02-27 11:39:57 +01:00
Petr Mironychev
ed55c829af
doc: Add Mistral AI and Google AI 2025-02-27 00:20:32 +01:00
Petr Mironychev
d651a246de chore: Upgrade plugin to version 0.5.0 2025-02-26 23:25:19 +01:00
Petr Mironychev
c8e0f3268e fix: End completion position in lsp answer 2025-02-26 23:12:26 +01:00
Petr Mironychev
84025ec843 feat: Separate system prompt for fin and non-fim models 2025-02-26 22:51:38 +01:00
Petr Mironychev
f6fd411b2d feat: Add moving to api key settings page if needed 2025-02-26 21:51:11 +01:00
Petr Mironychev
903eb50e7a fix: Current template description in General Settings 2025-02-26 13:39:49 +01:00
Petr Mironychev
e8f7f031b6 feat: Enabling Position Independent Code 2025-02-26 13:25:29 +01:00
Petr Mironychev
90ae6cd1c0 feat: Add checkbox for disabling chat 2025-02-26 13:18:30 +01:00
Petr Mironychev
2ad0117498 fix: Crash in open General Settings after another settings tab 2025-02-26 11:15:36 +01:00
Petr Mironychev
9d58565de3 feat: Add template description in general settings 2025-02-25 21:58:39 +01:00
Petr Mironychev
8dba9b4baa fix: Saving mistral and google keys to settings 2025-02-25 19:21:44 +01:00
Petr Mironychev
7ba615a72d feat: Add Google AI provider and template 2025-02-25 14:26:56 +01:00
Petr Mironychev
1b06a651f0 fix: Chat item code style 2025-02-24 19:37:29 +01:00
Petr Mironychev
912c3d8c04 feat: Additional marker for user message in chat 2025-02-24 16:54:45 +01:00
Petr Mironychev
e924029ec2 feat: Add filter templates for each provider 2025-02-23 01:41:47 +01:00
Petr Mironychev
d96f44d42c
refactor: Rework providers and templates logic 2025-02-22 19:39:28 +01:00
Petr Mironychev
bd25736a55 refactor: Optimize SystemPrompt for code completion 2025-02-16 16:53:03 +01:00
Petr Mironychev
60936f6d84 refactor: Improve code completion message for Instruct models
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-02-12 02:05:37 +01:00
Petr Mironychev
7d23d0323f refactor: Improve system prompt and message 2025-02-12 01:47:52 +01:00
Petr Mironychev
1fa6a225a4
Add discord invite
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-02-05 02:01:32 +01:00
Petr Mironychev
31133e3378 chore: Update plugin to 0.4.13
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-02-02 23:28:31 +01:00
Petr Mironychev
a2f15fc843 refactor: Remove experimental Ubuntu 22.04 build from releases 2025-02-02 23:03:51 +01:00
Petr Mironychev
2a0beb6c4c feat: Add language-specific LLM preset configuration
- Add ability to configure separate provider/model/template for specific programming language
- Add UI controls for language preset configuration
- Support custom provider selection per language
- Support custom model selection per language
- Support custom template selection per language
2025-02-02 22:57:18 +01:00
Petr Mironychev
e836b86569 chore: Upgrade plugin to 0.4.12
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-02-01 09:37:21 +01:00
Petr Mironychev
288fefebe5 feat: Add CodeLlama QML FIM 2025-02-01 09:36:14 +01:00
Petr Mironychev
528badbf1e
Add commercial support to README.md
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-01-29 15:00:16 +01:00
Petr Mironychev
b789e42602 chore: Upgrade plugin to 0.4.11
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-01-27 00:55:39 +01:00
Petr Mironychev
4bf955462f feat: Update dialog offer open plugin folder 2025-01-27 00:54:49 +01:00
Petr Mironychev
5b99e68e53
doc: Added file context feature description 2025-01-27 00:14:38 +01:00
Petr Mironychev
0f1b277ef7 chore: Upgrade plugin to 0.4.10 2025-01-26 23:11:58 +01:00
Petr Mironychev
56995c9edf fix: Saving name of chat in native language
- Add button to show chat history folder in system viewer
2025-01-26 23:06:51 +01:00
Petr Mironychev
45aba6b6be fix: Sync editors and chat when sync enable 2025-01-26 22:35:43 +01:00
Petr Mironychev
1dfb3feb96 doc: Update info for QtC 15.0.1 changes
Some checks are pending
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Waiting to run
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 platform:linux_x64]) (push) Waiting to run
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Waiting to run
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Waiting to run
Build plugin / update_json (push) Blocked by required conditions
Build plugin / release (push) Blocked by required conditions
2025-01-26 10:08:03 +01:00
Petr Mironychev
2c49d45297 fix: Keep name of chat after saving 2025-01-26 10:03:34 +01:00
Petr Mironychev
31145f191b chore: Upgrade plugin to 0.4.9
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-01-24 18:27:07 +01:00
Petr Mironychev
9096adde6f fix: Remove installing plugin from update dialog 2025-01-24 18:23:11 +01:00
Petr Mironychev
b8e578d2d7
chore: Update plugin to QtCreator 15.0.1
* fix: Additional check qtc version
* build: Upgrade plugin to QtC 15.0.1
* chore: Upgrade plugin version to 0.4.8
2025-01-24 13:29:44 +01:00
Petr Mironychev
4e45774bce chore: Upgrade version to 0.4.7 2025-01-24 01:52:09 +01:00
Petr Mironychev
928490d31f fix: small style changes 2025-01-24 01:50:56 +01:00
Petr Mironychev
97163cf6c9 fix: Add calculate tokens after clean chat 2025-01-24 01:08:30 +01:00
Petr Mironychev
f85c162692 fix: Improve scroll bar style 2025-01-24 00:59:26 +01:00
Petr Mironychev
258053d826 feat: Add chat file name in top bar 2025-01-24 00:52:10 +01:00
Petr Mironychev
bf63ae5714 refactor: Improve systemPrompt for code completion 2025-01-24 00:37:52 +01:00
Petr Mironychev
ae76850e78 fix: Buttons order in urls dialog on general page 2025-01-24 00:29:15 +01:00
Petr Mironychev
bf3c0b3aa0 feat: Add auto sync open files with model context 2025-01-24 00:22:44 +01:00
Petr Mironychev
9add61c805
feat: Add possibility to link files to the current system prompt
- Add linking files to chat
- Rework tokens counting
2025-01-23 10:17:38 +01:00
Petr Mironychev
add86d2e67 chore: Upgrade version to 0.4.6 2025-01-21 15:05:48 +01:00
Petr Mironychev
a6c909d34d
exp: Add ubuntu 22.04 experimental builds 2025-01-21 14:58:44 +01:00
Petr Mironychev
2814dec3e5 fix: Improve file attachment handling
- Add files to existing list instead of replacing when using attach dialog
- Prevent duplicate files from being added to attachment list
2025-01-21 11:33:13 +01:00
Petr Mironychev
1b86b60de8
Add system prompt configuration to readme 2025-01-20 10:00:36 +01:00
Petr Mironychev
4b7f638731
Add setup OpenAI provider 2025-01-19 20:28:06 +01:00
Petr Mironychev
de046f0529 chore: Bump version to 0.4.5 2025-01-19 17:44:11 +01:00
Petr Mironychev
e975e143b1 fix: Handling Ollama messages 2025-01-19 17:32:12 +01:00
Petr Mironychev
c97c0f62e8 fix: Handling full input message from OpenAI compatible providers 2025-01-19 01:16:33 +01:00
Petr Mironychev
61fded34ea feat: add OpenAI provider settings 2025-01-19 00:50:23 +01:00
Petr Mironychev
289a19ac1a feat: Add OpenAI provider and template 2025-01-17 01:22:12 +01:00
Petr Mironychev
43ac662671 fix: Text width in chat item 2025-01-17 00:46:11 +01:00
Petr Mironychev
1d64d2afc9 refactor: Move to using colors from QtC theme palette 2025-01-15 00:05:12 +01:00
Petr Mironychev
9db61119aa
feat: Add check plugin update and dialog for update 2025-01-13 20:11:27 +01:00
Petr Mironychev
70481b3116
Remove discord link from README.md 2025-01-08 16:03:00 +01:00
Petr Mironychev
511f5b36eb
Upgrade to version 0.4.4
* feat: Add attachments for message
* feat: Support QtC color palette for chat view
* feat: Improve code completion from non-FIM models
* refactor: Removed trimming messages
* chore: Bump version to 0.4.4
2025-01-08 02:05:25 +01:00
Petr Mironychev
35012865c7 refactor: Claude user message and code completion system prompt 2024-12-25 17:50:47 +01:00
Petr Mironychev
f27429aa66 refactor: Move context to separate lib 2024-12-24 22:45:20 +01:00
Petr Mironychev
113d5adcf4 fix: Add description for deprecated settings 2024-12-24 22:00:53 +01:00
Petr Mironychev
30ea89cdc2 chore: Bump version to 0.4.3 2024-12-23 23:36:47 +01:00
Petr Mironychev
13469edce6 doc: Add Claude to README 2024-12-23 23:34:28 +01:00
Petr Mironychev
ee2c3950e8 fix: path to chat file without project 2024-12-23 23:17:43 +01:00
Petr Mironychev
d04e5bc967
Add Claude provider and templates for chat and code (#55)
* feat: Add provider settings
* feat: Add Claude provider
* feat: Add Claude templates
* refactor: Setting input sensitivity
* fix: Back text after read code block
* fix: Add missing system message for ollama fim
2024-12-23 22:22:01 +01:00
Petr Mironychev
d8ef9d0120
refactor: Update issue templates 2024-12-23 18:36:27 +01:00
Petr Mironychev
e544e46d76
feat: Add saving and loading chat history
* feat: Add chat history path
* feat: Add save and load chat
* fix: Change badge width calculation
* refactor: Move chat action to top
* feat: Add autosave of messageReceived
* feat: Add settings for autosave
2024-12-23 18:34:01 +01:00
Petr Mironychev
63f0900511 fix: remove additional message in Ollama Auto Chat template 2024-12-23 16:43:37 +01:00
Petr Mironychev
7dee6f62c0 feat: Add project settings panel 2024-12-21 14:11:45 +01:00
Petr Mironychev
dc06ea2ed5 🔖 chore: Bump version to 0.4.2 2024-12-17 10:33:29 +01:00
Petr Mironychev
fc5e1adc0d 🐛 fix: Fix context for MessageBuilder 2024-12-17 10:32:32 +01:00
Petr Mironychev
93e59fb2dc
Add multiline code completion description to README.md 2024-12-17 01:36:28 +01:00
Petr Mironychev
cd2a56cde0 🔖 chore: Bump version to 0.4.1 2024-12-17 00:55:53 +01:00
Petr Mironychev
09cde8fd3d ♻️ refactor: Multiline text suggestion 2024-12-17 00:47:15 +01:00
Petr Mironychev
ac8080542d
feat: Add using instruct model in code completion
*  feat: Add MessageBuilder for code completion
*  feat: Add move text from request to comments
*  feat: Add settings for process text of instruct model
* 🐛 fix: Add stop to ollama request validator
* 🐛 fix: Template double delete
2024-12-17 00:35:17 +01:00
Petr Mironychev
7376a11a05 feat: Add request validator 2024-12-15 02:08:35 +01:00
Petr Mironychev
10e8b16caf 🐛 fix: Content description in network request 2024-12-12 21:02:23 +01:00
Petr Mironychev
a38debb140 🐛 fix: Remove test buttons 2024-12-12 15:25:31 +01:00
Petr Mironychev
844ac35a59 🐛 fix: Change name for OpenRouter provider 2024-12-12 15:09:21 +01:00
Petr Mironychev
16b77a5722 feat: Add stream option to settings 2024-12-10 21:46:39 +01:00
Petr Mironychev
c070fd5cfd feat: Add OpenRouter provider 2024-12-10 21:28:15 +01:00
Petr Mironychev
882047d7b2 ♻️ refactor: Improve response handler for LMStudio 2024-12-10 17:13:56 +01:00
Petr Mironychev
b692402897 ♻️ refactor: Improve Ollama response handler 2024-12-10 08:25:30 +01:00
Petr Mironychev
8102ba95f9
Update README.md 2024-12-03 22:33:43 +01:00
Petr Mironychev
f8bb9998ab 🐛 fix: Fix tags 2024-12-03 21:52:41 +01:00
Petr Mironychev
6dab055ca2 🐛 fix: FIx json error formating 2024-12-03 21:46:24 +01:00
Petr Mironychev
7b31fff9f2 🐛 fix: Update qodeassist json 2024-12-03 21:32:18 +01:00
Petr Mironychev
be9156fd0e 🐛 fix: Add plugins data 2024-12-03 21:16:39 +01:00
Petr Mironychev
657413344d
Add icons to README 2024-12-03 21:11:31 +01:00
Petr Mironychev
5f3deb44b9
doc: temporary fix for qtc version 2024-12-03 12:00:03 +01:00
Petr Mironychev
55e2b24b8d
Upgrade plugin to Qt Creator 15
* 🐛 fix: Change plugin configs
* 🐛 fix: Update Button aspect api
* 🐛 fix: Temproary fix for LLMSuggestions
* 🐛 fix: Update github actions
* 🔖 chore: Upgrade version in README
2024-12-03 11:15:35 +01:00
Petr Mironychev
76c17f03dd feat: Add model-template compatibility table 2024-11-26 14:03:28 +01:00
Petr Mironychev
19c25043fb 🔖 chore: Bump version to 0.3.10 2024-11-26 11:48:10 +01:00
Petr Mironychev
56b5ea8e68 feat: Improve OpenAI message handling 2024-11-26 11:43:51 +01:00
Petr Mironychev
b475f15e3d feat: Improve system prompt for code completion 2024-11-26 11:29:20 +01:00
Petr Mironychev
31f4516e7b feat: Add removing codeblock wrappers from code completion 2024-11-26 11:26:50 +01:00
Petr Mironychev
bfdbc755e3 🐛 fix: Move api key from request json to config 2024-11-26 10:52:47 +01:00
Petr Mironychev
30964d90d5 🐛 fix: Change format for context in system prompt 2024-11-26 10:15:20 +01:00
Petr Mironychev
1261f913bb ♻️ refactor: Rework currents and add new templates
Add Alpaca, Llama3, LLama2, ChatML templates
2024-11-26 00:28:27 +01:00
Petr Mironychev
36d5242a1f 🐛 fix: Removing message from chat after complete receiving 2024-11-25 23:00:53 +01:00
208 changed files with 13347 additions and 2079 deletions

View File

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

109
.github/scripts/plugin.json vendored Normal file
View 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
View 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'));

View File

@ -9,18 +9,16 @@ on:
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 }}
@ -30,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")
@ -107,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)
@ -119,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")
@ -135,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)
@ -153,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}")
@ -172,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")
@ -184,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: |
@ -262,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 }}"
@ -280,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
View File

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

3
.gitmodules vendored Normal file
View File

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

1
3rdparty/inja vendored Submodule

@ -0,0 +1 @@
Subproject commit 384a6bef3f1b69f8e429b28ee206df832cb2a6c8

View File

@ -8,14 +8,38 @@ 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 Quick Widgets Network REQUIRED)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test REQUIRED)
find_package(GTest)
# IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
set(QODEASSIST_QT_CREATOR_VERSION_MAJOR ${CMAKE_MATCH_1})
set(QODEASSIST_QT_CREATOR_VERSION_MINOR ${CMAKE_MATCH_2})
set(QODEASSIST_QT_CREATOR_VERSION_PATCH ${CMAKE_MATCH_3})
if(NOT version_match)
message(FATAL_ERROR "Failed to parse Qt Creator version string: ${IDE_VERSION}")
endif()
message(STATUS "Qt Creator Version: ${QODEASSIST_QT_CREATOR_VERSION_MAJOR}.${QODEASSIST_QT_CREATOR_VERSION_MINOR}.${QODEASSIST_QT_CREATOR_VERSION_PATCH}")
add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
)
add_subdirectory(llmcore)
add_subdirectory(settings)
add_subdirectory(logger)
add_subdirectory(ChatView)
add_subdirectory(context)
if(GTest_FOUND)
add_subdirectory(test)
endif()
add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS
@ -40,27 +64,65 @@ add_qtc_plugin(QodeAssist
QodeAssistConstants.hpp
QodeAssisttr.h
LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/StarCoder2Fim.hpp
templates/DeepSeekCoderFim.hpp
templates/CustomFimTemplate.hpp
templates/DeepSeekCoderChat.hpp
templates/CodeLlamaChat.hpp
templates/Qwen.hpp
templates/StarCoderChat.hpp
templates/Ollama.hpp
templates/BasicChat.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp
templates/Qwen.hpp
templates/OpenAICompatible.hpp
templates/Llama3.hpp
templates/ChatML.hpp
templates/Alpaca.hpp
templates/Llama2.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/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
)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable
NAMES
qtcreator "Qt Creator"
PATHS
"${QtCreatorCorePath}/../../../bin"
"${QtCreatorCorePath}/../../../MacOS"
NO_DEFAULT_PATH
)
if (QtCreatorExecutable)
add_custom_target(RunQtCreator
COMMAND ${QtCreatorExecutable} -pluginpath $<TARGET_FILE_DIR:QodeAssist>
DEPENDS QodeAssist
)
set_target_properties(RunQtCreator PROPERTIES FOLDER "qtc_runnable")
endif()

View File

@ -1,8 +1,8 @@
qt_add_library(QodeAssistChatView STATIC)
qt_policy(SET QTP0001 NEW)
qt_policy(SET QTP0004 NEW)
# URI name should match the subdirectory name to suppress the warning
qt_add_qml_module(QodeAssistChatView
URI ChatView
VERSION 1.0
@ -13,6 +13,18 @@ qt_add_qml_module(QodeAssistChatView
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
@ -20,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
@ -32,6 +45,7 @@ target_link_libraries(QodeAssistChatView
QtCreator::Utils
LLMCore
QodeAssistSettings
Context
)
target_include_directories(QodeAssistChatView

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -18,9 +18,9 @@
*/
#include "ChatModel.hpp"
#include <utils/aspects.h>
#include <QtCore/qjsonobject.h>
#include <QtQml>
#include <utils/aspects.h>
#include "ChatAssistantSettings.hpp"
@ -28,14 +28,14 @@ namespace QodeAssist::Chat {
ChatModel::ChatModel(QObject *parent)
: QAbstractListModel(parent)
, m_totalTokens(0)
{
auto &settings = Settings::chatAssistantSettings();
connect(&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
this,
&ChatModel::tokensThresholdChanged);
connect(
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
this,
&ChatModel::tokensThresholdChanged);
}
int ChatModel::rowCount(const QModelIndex &parent) const
@ -55,6 +55,13 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
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();
}
@ -65,29 +72,37 @@ 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)
void ChatModel::addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments)
{
int tokenCount = estimateTokenCount(content);
QString fullContent = content;
if (!attachments.isEmpty()) {
fullContent += "\n\nAttached files list:";
for (const auto &attachment : attachments) {
fullContent += QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
}
}
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
Message &lastMessage = m_messages.last();
int oldTokenCount = lastMessage.tokenCount;
lastMessage.content = content;
lastMessage.tokenCount = tokenCount;
m_totalTokens += (tokenCount - oldTokenCount);
lastMessage.attachments = attachments;
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
m_messages.append({role, content, tokenCount, id});
m_totalTokens += tokenCount;
Message newMessage{role, content, id};
newMessage.attachments = attachments;
m_messages.append(newMessage);
endInsertRows();
}
trim();
emit totalTokensChanged();
}
QVector<ChatModel::Message> ChatModel::getChatHistory() const
@ -95,32 +110,12 @@ 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::clear()
{
beginResetModel();
m_messages.clear();
m_totalTokens = 0;
endResetModel();
emit totalTokensChanged();
emit modelReseted();
}
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
@ -129,11 +124,13 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
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();
QString textBetween
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
if (!textBetween.isEmpty()) {
parts.append({MessagePart::Text, textBetween, ""});
}
@ -144,7 +141,19 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
if (lastIndex < content.length()) {
QString remainingText = content.mid(lastIndex).trimmed();
if (!remainingText.isEmpty()) {
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
if (unclosedMatch.hasMatch()) {
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
if (!beforeCodeBlock.isEmpty()) {
parts.append({MessagePart::Text, beforeCodeBlock, ""});
}
parts.append(
{MessagePart::Code, unclosedMatch.captured(2).trimmed(), unclosedMatch.captured(1)});
} else if (!remainingText.isEmpty()) {
parts.append({MessagePart::Text, remainingText, ""});
}
}
@ -155,7 +164,6 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const
{
QJsonArray messages;
messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}});
for (const auto &message : m_messages) {
@ -170,17 +178,27 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
default:
continue;
}
messages.append(QJsonObject{{"role", role}, {"content", message.content}});
QString content
= message.attachments.isEmpty()
? message.content
: message.content + "\n\nAttached files list:"
+ std::accumulate(
message.attachments.begin(),
message.attachments.end(),
QString(),
[](QString acc, const Context::ContentFile &attachment) {
return acc
+ QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
});
messages.append(QJsonObject{{"role", role}, {"content", content}});
}
return messages;
}
int ChatModel::totalTokens() const
{
return m_totalTokens;
}
int ChatModel::tokensThreshold() const
{
auto &settings = Settings::chatAssistantSettings();
@ -192,4 +210,16 @@ 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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -26,27 +26,29 @@
#include <QJsonArray>
#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);
@ -55,29 +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, const QString &id);
Q_INVOKABLE void addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {});
Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() 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

View File

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

View File

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

View 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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -29,4 +29,40 @@ 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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -23,7 +23,6 @@
#include <qqmlintegration.h>
namespace QodeAssist::Chat {
// Q_NAMESPACE
class ChatUtils : public QObject
{
@ -35,6 +34,7 @@ public:
: QObject(parent) {};
Q_INVOKABLE void copyToClipboard(const QString &text);
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
};
} // namespace QodeAssist::Chat

View File

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

View File

@ -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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -19,11 +19,11 @@
#include "ClientInterface.hpp"
#include <texteditor/textdocument.h>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <texteditor/textdocument.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
@ -35,39 +35,47 @@
#include "ChatAssistantSettings.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
namespace QodeAssist::Chat {
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
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::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);
}
});
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, bool includeCurrentFile)
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();
@ -79,8 +87,7 @@ void ClientInterface::sendMessage(const QString &message, bool includeCurrentFil
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
@ -88,47 +95,47 @@ void ClientInterface::sendMessage(const QString &message, bool includeCurrentFil
}
LLMCore::ContextData context;
context.prefix = message;
context.suffix = "";
QString systemPrompt;
if (chatAssistantSettings.useSystemPrompt())
systemPrompt = chatAssistantSettings.systemPrompt();
if (includeCurrentFile) {
QString fileContext = getCurrentFileContext();
if (!fileContext.isEmpty()) {
systemPrompt = systemPrompt.append(fileContext);
if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt();
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
}
QJsonObject providerRequest;
providerRequest["model"] = Settings::generalSettings().caModel();
providerRequest["stream"] = true;
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
if (promptTemplate)
promptTemplate->prepareRequest(providerRequest, context);
else
qWarning("No prompt template found");
if (provider)
provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
else
qWarning("No provider found");
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;
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest = providerRequest;
config.multiLineCompletion = false;
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()}};
}
QJsonObject request;
request["id"] = QUuid::createUuid().toString();
config.apiKey = provider->apiKey();
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "");
config.provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QJsonObject request{{"id", QUuid::createUuid().toString()}};
m_requestHandler->sendLLMRequest(config, request);
}
@ -144,15 +151,20 @@ void ClientInterface::cancelRequest()
m_requestHandler->cancelRequest(id);
}
void ClientInterface::handleLLMResponse(const QString &response,
const QJsonObject &request,
bool isComplete)
void ClientInterface::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
QString messageId = request["id"].toString();
m_chatModel->addMessage(response.trimmed(), ChatModel::ChatRole::Assistant, messageId);
const auto message = response.trimmed();
if (isComplete) {
LOG_MESSAGE("Message completed. Final response for message " + messageId + ": " + response);
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();
}
}
}
@ -171,13 +183,35 @@ QString ClientInterface::getCurrentFileContext() const
}
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
.arg(textDocument->mimeType(), textDocument->filePath().toString());
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString());
QString content = textDocument->document()->toPlainText();
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toString()));
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toFSPathString()));
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
}
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

View File

@ -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, bool includeCurrentFile = false);
void sendMessage(
const QString &message,
const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {});
void clearMessages();
void cancelRequest();
Context::ContextManager *contextManager() const;
signals:
void errorOccurred(const QString &error);
void messageReceivedCompletely();
private:
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;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel;
LLMCore::RequestHandler *m_requestHandler;
Context::ContextManager *m_contextManager;
};
} // namespace QodeAssist::Chat

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -17,28 +17,54 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
pragma ComponentBehavior: Bound
import QtQuick
import ChatView
import QtQuick.Layouts
import "./dialog"
Rectangle {
id: root
property alias msgModel: msgCreator.model
property color fontColor
property color codeBgColor
property color selectionColor
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
height: msgColumn.height
property bool isUserMessage: false
property int messageIndex: -1
property real listViewContentY: 0
signal resetChatToMessage(int index)
height: msgColumn.implicitHeight + 10
radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
Column {
HoverHandler {
id: mouse
}
ColumnLayout {
id: msgColumn
x: 5
width: parent.width - x
anchors.verticalCenter: parent.verticalCenter
width: parent.width
spacing: 5
Repeater {
@ -49,7 +75,7 @@ Rectangle {
// why does `required property MessagePart modelData` not work?
required property var modelData
width: parent.width
Layout.preferredWidth: root.width
sourceComponent: {
// If `required property MessagePart modelData` is used
// and conversion to MessagePart fails, you're left
@ -76,10 +102,72 @@ Rectangle {
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 {
@ -87,13 +175,28 @@ Rectangle {
height: implicitHeight + 10
verticalAlignment: Text.AlignVCenter
leftPadding: 10
text: itemData.text
color: root.fontColor
selectionColor: root.selectionColor
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
@ -104,8 +207,7 @@ Rectangle {
code: itemData.text
language: itemData.language
color: root.codeBgColor
selectionColor: root.selectionColor
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -17,28 +17,66 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
pragma ComponentBehavior: Bound
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: root.backgroundColor
color: palette.window
}
ColumnLayout {
anchors {
fill: parent
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()
}
spacing: 10
ListView {
id: chatListView
@ -54,14 +92,25 @@ ChatRootView {
delegate: ChatItem {
required property var model
required property int index
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)
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 {
@ -69,7 +118,7 @@ ChatRootView {
height: 30
}
ScrollBar.vertical: ScrollBar {
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
}
@ -95,15 +144,28 @@ ChatRootView {
id: messageInput
placeholderText: qsTr("Type your message here...")
placeholderTextColor: "#888"
color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
placeholderTextColor: palette.mid
color: palette.text
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)
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()
@ -113,65 +175,49 @@ ChatRootView {
}
}
RowLayout {
AttachedFilesPlace {
id: attachedFilesPlace
Layout.fillWidth: true
spacing: 5
Button {
id: sendButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Send")
onClicked: root.sendChatMessage()
}
Button {
id: stopButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Stop")
onClicked: root.cancelRequest()
}
Button {
id: clearButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Clear Chat")
onClicked: root.clearChat()
}
CheckBox {
id: sharingCurrentFile
text: "Share current file with models"
checked: root.isSharingCurrentFile
}
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)
}
}
Row {
id: bar
AttachedFilesPlace {
id: linkedFilesPlace
layoutDirection: Qt.RightToLeft
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: scroll.width
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)
}
spacing: 10
Badge {
text: qsTr("tokens:%1/%2").arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
color: root.codeColor
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
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() {
@ -179,7 +225,7 @@ ChatRootView {
}
function sendChatMessage() {
root.sendMessage(messageInput.text, sharingCurrentFile.checked)
root.sendMessage(messageInput.text)
messageInput.text = ""
scrollToBottom()
}

View 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}}
}
}
}

View File

@ -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: root.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")

View File

@ -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
}

View 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
}
}
}
}
}
}

View 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
}
}
}

View 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
View 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
View 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

View File

@ -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 @@
#include "ConfigurationManager.hpp"
#include <QTimer>
#include <settings/ButtonAspect.hpp>
#include <QTimer>
#include "QodeAssisttr.h"
@ -35,6 +35,49 @@ ConfigurationManager &ConfigurationManager::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)
@ -57,6 +100,21 @@ void ConfigurationManager::setupConnections()
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()
@ -69,13 +127,13 @@ void ConfigurationManager::selectProvider()
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:"));
m_generalSettings.showSelectionDialog(
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
});
}
@ -86,14 +144,19 @@ void ConfigurationManager::selectModel()
return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel : m_generalSettings.caModel;
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
: isPreset1 ? m_generalSettings.ccPreset1Model
: m_generalSettings.caModel;
if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->supportsModelListing()) {
@ -122,18 +185,23 @@ void ConfigurationManager::selectTemplate()
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 ? m_templateManger.fimTemplatesNames()
: m_templateManger.chatTemplatesNames();
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:"));
m_generalSettings.showSelectionDialog(
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
});
}
@ -150,8 +218,9 @@ void ConfigurationManager::selectUrl()
urls.append(url);
}
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl)
? m_generalSettings.ccUrl
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]() {

View File

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

View File

@ -1,248 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "DocumentContextReader.hpp"
#include <QFileInfo>
#include <QTextBlock>
#include <languageserverprotocol/lsptypes.h>
#include "core/ChangesManager.h"
#include "settings/CodeCompletionSettings.hpp"
const QRegularExpression &getYearRegex()
{
static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b");
return yearRegex;
}
const QRegularExpression &getNameRegex()
{
static const QRegularExpression nameRegex("\\b[A-Z][a-z.]+ [A-Z][a-z.]+\\b");
return nameRegex;
}
const QRegularExpression &getCommentRegex()
{
static const QRegularExpression
commentRegex(R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))",
QRegularExpression::MultilineOption);
return commentRegex;
}
namespace QodeAssist {
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
: m_textDocument(textDocument)
, m_document(textDocument->document())
{
m_copyrightInfo = findCopyright();
}
QString DocumentContextReader::getLineText(int lineNumber, int cursorPosition) const
{
if (!m_document || lineNumber < 0)
return QString();
QTextBlock block = m_document->begin();
int currentLine = 0;
while (block.isValid()) {
if (currentLine == lineNumber) {
QString text = block.text();
if (cursorPosition >= 0 && cursorPosition <= text.length()) {
text = text.left(cursorPosition);
}
return text;
}
block = block.next();
currentLine++;
}
return QString();
}
QString DocumentContextReader::getContextBefore(int lineNumber,
int cursorPosition,
int linesCount) const
{
int effectiveStartLine;
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - linesCount);
} else {
effectiveStartLine = qMax(0, lineNumber - linesCount);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
QString DocumentContextReader::getContextAfter(int lineNumber,
int cursorPosition,
int linesCount) const
{
int endLine = qMin(m_document->blockCount() - 1, lineNumber + linesCount);
return getContextBetween(lineNumber + 1, endLine, cursorPosition);
}
QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const
{
int startLine = 0;
if (m_copyrightInfo.found) {
startLine = m_copyrightInfo.endLine + 1;
}
startLine = qMin(startLine, lineNumber);
QString result = getContextBetween(startLine, lineNumber, cursorPosition);
return result;
}
QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const
{
return getContextBetween(lineNumber, m_document->blockCount() - 1, cursorPosition);
}
QString DocumentContextReader::getLanguageAndFileInfo() const
{
if (!m_textDocument)
return QString();
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(
m_textDocument->mimeType());
QString mimeType = m_textDocument->mimeType();
QString filePath = m_textDocument->filePath().toString();
QString fileExtension = QFileInfo(filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, mimeType, filePath, fileExtension);
}
CopyrightInfo DocumentContextReader::findCopyright()
{
CopyrightInfo result = {-1, -1, false};
QString text = m_document->toPlainText();
QRegularExpressionMatchIterator matchIterator = getCommentRegex().globalMatch(text);
QList<CopyrightInfo> copyrightBlocks;
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured().toLower();
if (matchedText.contains("copyright") || matchedText.contains("(C)")
|| matchedText.contains("(c)") || matchedText.contains("©")
|| getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) {
int startPos = match.capturedStart();
int endPos = match.capturedEnd();
CopyrightInfo info;
info.startLine = m_document->findBlock(startPos).blockNumber();
info.endLine = m_document->findBlock(endPos).blockNumber();
info.found = true;
copyrightBlocks.append(info);
}
}
for (int i = 0; i < copyrightBlocks.size() - 1; ++i) {
if (copyrightBlocks[i].endLine + 1 >= copyrightBlocks[i + 1].startLine) {
copyrightBlocks[i].endLine = copyrightBlocks[i + 1].endLine;
copyrightBlocks.removeAt(i + 1);
--i;
}
}
if (!copyrightBlocks.isEmpty()) { // temproary solution, need cache
return copyrightBlocks.first();
}
return result;
}
QString DocumentContextReader::getContextBetween(int startLine,
int endLine,
int cursorPosition) const
{
QString context;
for (int i = startLine; i <= endLine; ++i) {
QTextBlock block = m_document->findBlockByNumber(i);
if (!block.isValid()) {
break;
}
if (i == endLine) {
context += block.text().left(cursorPosition);
} else {
context += block.text() + "\n";
}
}
return context;
}
CopyrightInfo DocumentContextReader::copyrightInfo() const
{
return m_copyrightInfo;
}
LLMCore::ContextData DocumentContextReader::prepareContext(int lineNumber, int cursorPosition) const
{
QString contextBefore = getContextBefore(lineNumber, cursorPosition);
QString contextAfter = getContextAfter(lineNumber, cursorPosition);
QString fileContext;
if (Settings::codeCompletionSettings().useFilePathInContext())
fileContext += getLanguageAndFileInfo();
if (Settings::codeCompletionSettings().useProjectChangesCache())
fileContext += ChangesManager::instance().getRecentChangesContext(m_textDocument);
return {contextBefore, contextAfter, fileContext};
}
QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileBefore(lineNumber, cursorPosition);
} else {
int effectiveStartLine;
int beforeCursor = Settings::codeCompletionSettings().readStringsBeforeCursor();
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - beforeCursor);
} else {
effectiveStartLine = qMax(0, lineNumber - beforeCursor);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
}
QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileAfter(lineNumber, cursorPosition);
} else {
int endLine = qMin(m_document->blockCount() - 1,
lineNumber + Settings::codeCompletionSettings().readStringsAfterCursor());
return getContextBetween(lineNumber + 1, endLine, -1);
}
}
} // namespace QodeAssist

View File

@ -1,64 +0,0 @@
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QTextDocument>
#include <texteditor/textdocument.h>
#include <llmcore/ContextData.hpp>
namespace QodeAssist {
struct CopyrightInfo
{
int startLine;
int endLine;
bool found;
};
class DocumentContextReader
{
public:
DocumentContextReader(TextEditor::TextDocument *textDocument);
QString getLineText(int lineNumber, int cursorPosition = -1) const;
QString getContextBefore(int lineNumber, int cursorPosition, int linesCount) const;
QString getContextAfter(int lineNumber, int cursorPosition, int linesCount) const;
QString readWholeFileBefore(int lineNumber, int cursorPosition) const;
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
QString getLanguageAndFileInfo() const;
CopyrightInfo findCopyright();
QString getContextBetween(int startLine, int endLine, int cursorPosition) const;
CopyrightInfo copyrightInfo() const;
LLMCore::ContextData prepareContext(int lineNumber, int cursorPosition) const;
private:
QString getContextBefore(int lineNumber, int cursorPosition) const;
QString getContextAfter(int lineNumber, int cursorPosition) const;
private:
TextEditor::TextDocument *m_textDocument;
QTextDocument *m_document;
CopyrightInfo m_copyrightInfo;
};
} // namespace QodeAssist

View File

@ -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 "CodeHandler.hpp"
#include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp"
#include "llmcore/PromptTemplateManager.hpp"
#include "llmcore/ProvidersManager.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,96 +172,192 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
void LLMClientInterface::handleCompletion(const QJsonObject &request)
{
auto updatedContext = prepareContext(request);
auto &completeSettings = Settings::codeCompletionSettings();
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 providerName = Settings::generalSettings().ccProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
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 = Settings::generalSettings().ccTemplate();
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
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.requestType = LLMCore::RequestType::CodeCompletion;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QUrl(
QString("%1%2").arg(Settings::generalSettings().ccUrl(), provider->completionEndpoint()));
config.providerRequest = {{"model", Settings::generalSettings().ccModel()}, {"stream", true}};
config.multiLineCompletion = completeSettings.multiLineCompletion();
QString systemPrompt;
if (completeSettings.useSystemPrompt())
systemPrompt.append(completeSettings.systemPrompt());
if (!updatedContext.fileContext.isEmpty())
systemPrompt.append(updatedContext.fileContext);
if (!systemPrompt.isEmpty())
config.providerRequest["system"] = systemPrompt;
// 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();
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords;
config.promptTemplate->prepareRequest(config.providerRequest, updatedContext);
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::Fim);
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());
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;
@ -248,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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -1,16 +1,13 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.3.9",
"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}
}

View File

@ -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>

View File

@ -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/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,48 +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::codeCompletionSettings().autoCompletion())
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::codeCompletionSettings().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::codeCompletionSettings().autoCompletionTypingInterval()) {
m_recentCharCount = charsAdded;
m_typingTimer.restart();
}
if (charsRemoved > 0 || charsAdded <= 0) {
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
if (m_recentCharCount
> Settings::codeCompletionSettings().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)
@ -126,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);
@ -143,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);
@ -172,8 +240,8 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position());
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());
@ -193,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) {
@ -211,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()));
}
}
@ -223,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()
@ -239,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);
@ -265,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

View File

@ -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

View File

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

View File

@ -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
View 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
View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
#include <QObject>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <llmcore/RequestHandler.hpp>
namespace QodeAssist {
struct RefactorResult
{
QString newText;
Utils::Text::Range insertRange;
bool success;
QString errorMessage;
};
class QuickRefactorHandler : public QObject
{
Q_OBJECT
public:
explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override;
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest();
signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result);
private:
void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
LLMCore::ContextData prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions);
LLMCore::RequestHandler *m_requestHandler;
TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress;
QString m_lastRequestId;
Context::ContextManager m_contextManager;
};
} // namespace QodeAssist

307
README.md
View File

@ -1,45 +1,80 @@
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
[![Discord](https://dcbadge.limes.pink/api/server/https://discord.gg/DGgMtTteAD?style=flat?theme=discord)](https://discord.gg/DGgMtTteAD)
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
![Static Badge](https://img.shields.io/badge/QtCreator-14.0.2-brightgreen)
![Static Badge](https://img.shields.io/badge/QtCreator-16.0.1-brightgreen)
![Static Badge](https://img.shields.io/badge/QtCreator-16.0.2-brightgreen)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](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-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
> - These services will consume API tokens which may result in charges to your account
> - The QodeAssist developer bears no responsibility for any charges incurred
> - Please carefully review the provider's pricing and your account settings before use
## Table of Contents
1. [Overview](#overview)
2. [Installation](#installation)
3. [Configure Plugin](#configure-plugin)
4. [Supported LLM Providers](#supported-llm-providers)
5. [Recommended Models](#recommended-models)
- [Ollama](#ollama)
- [LM Studio](#lm-studio)
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
7. [Development Progress](#development-progress)
8. [Hotkeys](#hotkeys)
9. [Troubleshooting](#troubleshooting)
10. [Support the Development](#support-the-development-of-qodeassist)
11. [How to Build](#how-to-build)
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
- 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
- LM Studio (experimental)
- OpenAI-compatible providers (experimental)
- 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
- Custom template support
- Easy configuration and model selection
Join our Discord Community: Have questions or want to discuss QodeAssist? Join our [Discord server](https://discord.gg/BGMkUsXUgf) to connect with other users and get support!
<details>
<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>
<details>
<summary>Quick refactor in code: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details>
<details>
<summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
</details>
<details>
<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">
@ -50,11 +85,80 @@ QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
</details>
## Installation
<details>
<summary>Automatic syncing with open editor files: (click to expand)</summary>
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
</details>
1. Install Latest QtCreator
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:
## 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 for Anthropic Claude
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Claude api key
3. Return to General tab and configure:
- Set "Claude" as the provider for code completion or/and chat assistant
- Set the Claude URL (https://api.anthropic.com)
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
- Choose the Claude template for code completion or/and chat
<details>
<summary>Example of Claude settings: (click to expand)</summary>
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
</details>
## Configure for OpenAI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure OpenAI api key
3. Return to General tab and configure:
- Set "OpenAI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.openai.com)
- Select your preferred model (e.g., gpt-4o)
- Choose the OpenAI template for code completion or/and chat
<details>
<summary>Example of OpenAI settings: (click to expand)</summary>
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
</details>
## Configure for Mistral AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Mistral AI api key
3. Return to General tab and configure:
- Set "Mistral AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.mistral.ai)
- Select your preferred model (e.g., mistral-large-latest)
- Choose the Mistral AI template for code completion or/and chat
<details>
<summary>Example of Mistral AI settings: (click to expand)</summary>
<img width="829" alt="Mistral AI Settings" src="https://github.com/user-attachments/assets/1c5ed13b-a29b-43f7-b33f-2e05fdea540c" />
</details>
## Configure for Google AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Google AI api key
3. Return to General tab and configure:
- Set "Google AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://generativelanguage.googleapis.com/v1beta)
- Select your preferred model (e.g., gemini-2.0-flash)
- Choose the Google AI template
<details>
<summary>Example of Google AI settings: (click to expand)</summary>
<img width="829" alt="Google AI Settings" src="https://github.com/user-attachments/assets/046ede65-a94d-496c-bc6c-41f3750be12a" />
</details>
## Configure for Ollama
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Install a language models in Ollama via terminal. For example, you can run:
For standard computers (minimum 8GB RAM):
```
@ -68,60 +172,90 @@ For high-end systems (32GB+ RAM):
```
ollama run qwen2.5-coder:32b
```
4. Download the QodeAssist plugin for your QtCreator.
5. Launch Qt Creator and install the plugin:
- Go to MacOS: Qt Creator -> About Plugins...
Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Configure Plugin
QodeAssist comes with default settings that should work immediately after installing a language model. The plugin is pre-configured to use Ollama with standard templates, so you may only need to verify the settings.
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
2. Navigate to the "Qode Assist" tab
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
- 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>
## Supported LLM Providers
QodeAssist currently supports the following LLM (Large Language Model) providers:
- [Ollama](https://ollama.com)
- [LM Studio](https://lmstudio.ai) (experimental)
- OpenAI compatible providers (experimental)
## 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>
## Recommended Models:
QodeAssist has been thoroughly tested and optimized for use with the following language models:
## System Prompt Configuration
- Qwen2.5-coder
- CodeLlama
- StarCoder2
- DeepSeek-Coder-V2
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.
### Ollama:
### For autocomplete(FIM)
```
ollama run codellama:7b-code
ollama run starcoder2:7b
ollama run qwen2.5-coder:7b-base
ollama run deepseek-coder-v2:16b-lite-base-q3_K_M
```
### For chat
```
ollama run codellama:7b-instruct
ollama run starcoder2:instruct
ollama run qwen2.5-coder:7b-instruct
ollama run deepseek-coder-v2
```
## 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
@ -133,16 +267,44 @@ ollama run deepseek-coder-v2
- [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
@ -153,20 +315,17 @@ If QodeAssist is having problems connecting to the LLM provider, please check th
- For Ollama, the default is usually http://localhost:11434
- For 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
@ -205,3 +364,7 @@ relative or absolute path to this plugin directory.
QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them
C++ code style: check use .clang-fortmat in project
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d)
![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41)

72
UpdateStatusWidget.cpp Normal file
View 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
View 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
View 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)

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -23,16 +23,15 @@
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

View File

@ -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

26
context/CMakeLists.txt Normal file
View 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})

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -18,9 +18,9 @@
*/
#include "ChangesManager.h"
#include "settings/CodeCompletionSettings.hpp"
#include "CodeCompletionSettings.hpp"
namespace QodeAssist {
namespace QodeAssist::Context {
ChangesManager &ChangesManager::instance()
{
@ -30,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];
@ -51,9 +46,10 @@ 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;
@ -79,4 +75,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *
return context;
}
} // namespace QodeAssist
} // namespace QodeAssist::Context

View File

@ -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
View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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

View 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

View 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
View 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
View 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
View 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

View File

@ -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

View File

@ -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 fileContext;
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

View 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

View 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
View File

@ -0,0 +1,102 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "OllamaMessage.hpp"
#include <QJsonArray>
#include <QJsonDocument>
namespace QodeAssist::LLMCore {
QJsonObject OllamaMessage::parseJsonFromData(const QByteArray &data)
{
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(line, &error);
if (!doc.isNull() && error.error == QJsonParseError::NoError) {
return doc.object();
}
}
return QJsonObject();
}
OllamaMessage OllamaMessage::fromJson(const QByteArray &data, Type type)
{
OllamaMessage msg;
QJsonObject obj = parseJsonFromData(data);
if (obj.isEmpty()) {
msg.error = "Invalid JSON response";
return msg;
}
msg.model = obj["model"].toString();
msg.createdAt = QDateTime::fromString(obj["created_at"].toString(), Qt::ISODate);
msg.done = obj["done"].toBool();
msg.doneReason = obj["done_reason"].toString();
msg.error = obj["error"].toString();
if (type == Type::Generate) {
auto &genResponse = msg.response.emplace<GenerateResponse>();
genResponse.response = obj["response"].toString();
if (msg.done && obj.contains("context")) {
const auto array = obj["context"].toArray();
genResponse.context.reserve(array.size());
for (const auto &val : array) {
genResponse.context.append(val.toInt());
}
}
} else {
auto &chatResponse = msg.response.emplace<ChatResponse>();
const auto msgObj = obj["message"].toObject();
chatResponse.role = msgObj["role"].toString();
chatResponse.content = msgObj["content"].toString();
}
if (msg.done) {
msg.metrics
= {obj["total_duration"].toVariant().toLongLong(),
obj["load_duration"].toVariant().toLongLong(),
obj["prompt_eval_count"].toVariant().toLongLong(),
obj["prompt_eval_duration"].toVariant().toLongLong(),
obj["eval_count"].toVariant().toLongLong(),
obj["eval_duration"].toVariant().toLongLong()};
}
return msg;
}
QString OllamaMessage::getContent() const
{
if (std::holds_alternative<GenerateResponse>(response)) {
return std::get<GenerateResponse>(response).response;
}
return std::get<ChatResponse>(response).content;
}
bool OllamaMessage::hasError() const
{
return !error.isEmpty();
}
} // namespace QodeAssist::LLMCore

71
llmcore/OllamaMessage.hpp Normal file
View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDateTime>
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::LLMCore {
class OllamaMessage
{
public:
enum class Type { Generate, Chat };
struct Metrics
{
qint64 totalDuration{0};
qint64 loadDuration{0};
qint64 promptEvalCount{0};
qint64 promptEvalDuration{0};
qint64 evalCount{0};
qint64 evalDuration{0};
};
struct GenerateResponse
{
QString response;
QVector<int> context;
};
struct ChatResponse
{
QString role;
QString content;
};
QString model;
QDateTime createdAt;
std::variant<GenerateResponse, ChatResponse> response;
bool done{false};
QString doneReason;
QString error;
Metrics metrics;
static OllamaMessage fromJson(const QByteArray &data, Type type);
QString getContent() const;
bool hasError() const;
private:
static QJsonObject parseJsonFromData(const QByteArray &data);
};
} // namespace QodeAssist::LLMCore

82
llmcore/OpenAIMessage.cpp Normal file
View File

@ -0,0 +1,82 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "OpenAIMessage.hpp"
#include <QJsonArray>
#include <QJsonDocument>
namespace QodeAssist::LLMCore {
OpenAIMessage OpenAIMessage::fromJson(const QJsonObject &obj)
{
OpenAIMessage msg;
if (obj.contains("error")) {
msg.error = obj["error"].toObject()["message"].toString();
return msg;
}
if (obj.contains("choices")) {
auto choices = obj["choices"].toArray();
if (!choices.isEmpty()) {
auto choiceObj = choices[0].toObject();
if (choiceObj.contains("delta")) {
QJsonObject delta = choiceObj["delta"].toObject();
msg.choice.content = delta["content"].toString();
} else if (choiceObj.contains("message")) {
QJsonObject message = choiceObj["message"].toObject();
msg.choice.content = message["content"].toString();
}
msg.choice.finishReason = choiceObj["finish_reason"].toString();
if (!msg.choice.finishReason.isEmpty()) {
msg.done = true;
}
}
}
if (obj.contains("usage")) {
QJsonObject usage = obj["usage"].toObject();
msg.usage.promptTokens = usage["prompt_tokens"].toInt();
msg.usage.completionTokens = usage["completion_tokens"].toInt();
msg.usage.totalTokens = usage["total_tokens"].toInt();
}
return msg;
}
QString OpenAIMessage::getContent() const
{
return choice.content;
}
bool OpenAIMessage::hasError() const
{
return !error.isEmpty();
}
bool OpenAIMessage::isDone() const
{
return done
|| (!choice.finishReason.isEmpty()
&& (choice.finishReason == "stop" || choice.finishReason == "length"));
}
} // namespace QodeAssist::LLMCore

56
llmcore/OpenAIMessage.hpp Normal file
View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QByteArray>
#include <QJsonObject>
#include <QString>
namespace QodeAssist::LLMCore {
class OpenAIMessage
{
public:
struct Choice
{
QString content;
QString finishReason;
};
struct Usage
{
int promptTokens{0};
int completionTokens{0};
int totalTokens{0};
};
Choice choice;
QString error;
bool done{false};
Usage usage;
QString getContent() const;
bool hasError() const;
bool isDone() const;
static OpenAIMessage fromJson(const QJsonObject &obj);
};
} // namespace QodeAssist::LLMCore

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplate.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::LLMCore {
class PromptProviderChat : public IPromptProvider
{
public:
explicit PromptProviderChat(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderChat() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getChatTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.chatTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getChatTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::LLMCore

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "IPromptProvider.hpp"
#include "PromptTemplateManager.hpp"
namespace QodeAssist::LLMCore {
class PromptProviderFim : public IPromptProvider
{
public:
explicit PromptProviderFim(PromptTemplateManager &templateManager)
: m_templateManager(templateManager)
{}
~PromptProviderFim() = default;
PromptTemplate *getTemplateByName(const QString &templateName) const override
{
return m_templateManager.getFimTemplateByName(templateName);
}
QStringList templatesNames() const override { return m_templateManager.fimTemplatesNames(); }
QStringList getTemplatesForProvider(ProviderID id) const override
{
return m_templateManager.getFimTemplatesForProvider(id);
}
private:
PromptTemplateManager &m_templateManager;
};
} // namespace QodeAssist::LLMCore

View File

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

View File

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

View File

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

View File

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

34
llmcore/ProviderID.hpp Normal file
View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
namespace QodeAssist::LLMCore {
enum class ProviderID {
Any,
Ollama,
LMStudio,
Claude,
OpenAI,
OpenAICompatible,
MistralAI,
OpenRouter,
GoogleAI,
LlamaCpp
};
}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -19,11 +19,11 @@
#pragma once
#include <QJsonObject>
#include <QUrl>
#include "PromptTemplate.hpp"
#include "Provider.hpp"
#include "RequestType.hpp"
#include <QJsonObject>
#include <QUrl>
namespace QodeAssist::LLMCore {
@ -35,6 +35,7 @@ struct LLMConfig
QJsonObject providerRequest;
RequestType requestType;
bool multiLineCompletion;
QString apiKey;
};
} // namespace QodeAssist::LLMCore

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