Compare commits

...

164 Commits

Author SHA1 Message Date
fe84a2a303 chore: Upgrade plugin version to 0.5.6 2025-04-04 18:39:18 +02:00
62de53c306 chore: Update copyrights 2025-04-04 18:01:02 +02:00
7c6a10936c refactor: Simplify update mechanism (#159) 2025-04-04 17:55:36 +02:00
032c9bbbf3 fix: Add input_extra to llama.cpp validator 2025-04-04 17:16:05 +02:00
8906f98038 fix: Rework copyright searching (#158) 2025-04-04 15:29:36 +02:00
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
9d2d70fc63 feat: Add sharing opened files with code completion requests (#156) 2025-04-04 10:38:06 +02:00
ffaf6bd61b feat: Add code completion request progress animation (#153) 2025-04-02 21:00:45 +02:00
79218d8412 refactor: Replace singletone for context manager (#151) 2025-04-01 22:29:45 +02:00
7e6e526ac8 refactor: Removed deprecated api keys fields 2025-03-27 02:12:07 +01:00
80646e2af0 feat: Add additional language for handling to CodeCompletion settings (#150) 2025-03-27 02:07:09 +01:00
5808a892c1 fix: Change expected name in test 2025-03-27 00:41:28 +01:00
d58ff90458 fix: Fixed typo in the use of the project name 2025-03-27 00:34:10 +01:00
7d06ab04dc feat: Add RunQtCreator target 2025-03-27 00:29:23 +01:00
9d40e8ca25 chore: Upgrade plugin to 0.5.5 version 2025-03-20 19:02:11 +01:00
5b16c5403a fix: Add authorization while getting installed models (#142) (#147) 2025-03-20 11:33:38 +01:00
4ddbe0b8b9 feat: Support Ollama authorization via BaseAuth (#145) (#146) 2025-03-20 11:14:50 +01:00
f41e063c02 chore: Upgrade plugin version to 0.5.4 2025-03-17 02:51:31 +01:00
9d7d084448 fix: Wrong template replace to first template (#143) 2025-03-17 02:48:18 +01:00
1ca1ffc629 fix: Remove reading from replay leading to crash (#142) 2025-03-17 01:22:27 +01:00
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
91a6a88130 doc: Add default path for installed plugin 2025-03-14 11:35:37 +01:00
be38abc505 chore: upgrade plugin to 0.5.3 (#139) 2025-03-14 10:59:23 +01:00
f2e0afb6b8 fix: Add qml in code handler for processing model answers (#138) 2025-03-14 10:47:30 +01:00
3cf07238fd doc: Update QtC version to 16 2025-03-14 09:32:54 +01:00
b98f85a997 chore: Upgrade plugin to QtCreator 16 (#136) 2025-03-13 16:46:32 +01:00
085659483f doc: Add info about linux compatibility to README.md 2025-03-11 08:34:14 +01:00
8a1fd5438e chore: Add tests for LLMClientInterface (#131) 2025-03-10 21:54:17 +01:00
78f69e82a5 chore: Checkout submodules when building (#133) 2025-03-10 19:17:30 +01:00
3d770f91c7 refactor: Reduce dependency on TextDocument in ContextManager (#128) 2025-03-10 18:06:19 +01:00
c724bace06 refactor: Move document access out of prepareContext() (#129) 2025-03-10 17:54:03 +01:00
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
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
13cd12b00a chore: Add 3rdparty/inja dependency (#126) 2025-03-10 17:28:25 +01:00
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
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
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
6b86637dcb doc: Add llama.cpp description to README.md 2025-03-10 12:07:39 +01:00
58c3e26e7f refactor: Decouple LLMClientInterface from ProvidersManager (#120)
This will be needed for tests.
2025-03-10 10:40:51 +01:00
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
b6f36d61ae fix: Replace ubuntu-latest to ubuntu 22.04 (#119) 2025-03-10 01:33:03 +01:00
f2f453ccc8 chore: Upgrade plugin to 0.5.1 2025-03-10 01:00:33 +01:00
1bcfd749d5 fix: Change OpenAI model parser conditions 2025-03-10 00:47:16 +01:00
e66f467214 feat: Add llama.cpp provider and fim template (#118) 2025-03-09 22:57:33 +01:00
c9a3cdaf25 refactor: Reuse extractFilePathFromRequest() more (#117) 2025-03-08 16:18:44 +01:00
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
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
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
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
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
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
9a5047618d chore: Silence Qt warnings during tests (#110) 2025-03-07 01:58:09 +01:00
90beebf2ee Revert "refactor: Move all processing logic to CodeHandler::processText()" (#109) 2025-03-07 01:57:13 +01:00
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
5536de146c chore: Remove dead code in RequestHandler (#106) 2025-03-06 14:19:01 +01:00
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
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
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
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
0d3493e7f6 feat: Change linux build base to ubuntu 22.04 (#99) 2025-03-06 11:55:04 +01:00
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
5dceb5cd19 fix: Bring back old behavior of readStrings{After,Before}Cursor setting (#97) 2025-03-05 20:27:16 +01:00
69a8aa80d9 refactor: Make DocumentContextReader::prepareContext() testable (#96) 2025-03-05 20:18:59 +01:00
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
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
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
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
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
29a3939c64 chore: Add Github Actions job for checking formatting (#92) 2025-03-05 01:46:17 +01:00
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
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
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
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
0d22e1866e fix: Add tooltip about where log messages can be seen (#86) 2025-03-03 19:00:25 +01:00
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
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
e507d7ee17 doc: Add discord link with description 2025-02-27 11:39:57 +01:00
ed55c829af doc: Add Mistral AI and Google AI 2025-02-27 00:20:32 +01:00
d651a246de chore: Upgrade plugin to version 0.5.0 2025-02-26 23:25:19 +01:00
c8e0f3268e fix: End completion position in lsp answer 2025-02-26 23:12:26 +01:00
84025ec843 feat: Separate system prompt for fin and non-fim models 2025-02-26 22:51:38 +01:00
f6fd411b2d feat: Add moving to api key settings page if needed 2025-02-26 21:51:11 +01:00
903eb50e7a fix: Current template description in General Settings 2025-02-26 13:39:49 +01:00
e8f7f031b6 feat: Enabling Position Independent Code 2025-02-26 13:25:29 +01:00
90ae6cd1c0 feat: Add checkbox for disabling chat 2025-02-26 13:18:30 +01:00
2ad0117498 fix: Crash in open General Settings after another settings tab 2025-02-26 11:15:36 +01:00
9d58565de3 feat: Add template description in general settings 2025-02-25 21:58:39 +01:00
8dba9b4baa fix: Saving mistral and google keys to settings 2025-02-25 19:21:44 +01:00
7ba615a72d feat: Add Google AI provider and template 2025-02-25 14:26:56 +01:00
1b06a651f0 fix: Chat item code style 2025-02-24 19:37:29 +01:00
912c3d8c04 feat: Additional marker for user message in chat 2025-02-24 16:54:45 +01:00
e924029ec2 feat: Add filter templates for each provider 2025-02-23 01:41:47 +01:00
d96f44d42c refactor: Rework providers and templates logic 2025-02-22 19:39:28 +01:00
bd25736a55 refactor: Optimize SystemPrompt for code completion 2025-02-16 16:53:03 +01:00
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
7d23d0323f refactor: Improve system prompt and message 2025-02-12 01:47:52 +01:00
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
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
a2f15fc843 refactor: Remove experimental Ubuntu 22.04 build from releases 2025-02-02 23:03:51 +01:00
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
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
288fefebe5 feat: Add CodeLlama QML FIM 2025-02-01 09:36:14 +01:00
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
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
4bf955462f feat: Update dialog offer open plugin folder 2025-01-27 00:54:49 +01:00
5b99e68e53 doc: Added file context feature description 2025-01-27 00:14:38 +01:00
0f1b277ef7 chore: Upgrade plugin to 0.4.10 2025-01-26 23:11:58 +01:00
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
45aba6b6be fix: Sync editors and chat when sync enable 2025-01-26 22:35:43 +01:00
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
2c49d45297 fix: Keep name of chat after saving 2025-01-26 10:03:34 +01:00
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
9096adde6f fix: Remove installing plugin from update dialog 2025-01-24 18:23:11 +01:00
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
4e45774bce chore: Upgrade version to 0.4.7 2025-01-24 01:52:09 +01:00
928490d31f fix: small style changes 2025-01-24 01:50:56 +01:00
97163cf6c9 fix: Add calculate tokens after clean chat 2025-01-24 01:08:30 +01:00
f85c162692 fix: Improve scroll bar style 2025-01-24 00:59:26 +01:00
258053d826 feat: Add chat file name in top bar 2025-01-24 00:52:10 +01:00
bf63ae5714 refactor: Improve systemPrompt for code completion 2025-01-24 00:37:52 +01:00
ae76850e78 fix: Buttons order in urls dialog on general page 2025-01-24 00:29:15 +01:00
bf3c0b3aa0 feat: Add auto sync open files with model context 2025-01-24 00:22:44 +01:00
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
add86d2e67 chore: Upgrade version to 0.4.6 2025-01-21 15:05:48 +01:00
a6c909d34d exp: Add ubuntu 22.04 experimental builds 2025-01-21 14:58:44 +01:00
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
1b86b60de8 Add system prompt configuration to readme 2025-01-20 10:00:36 +01:00
4b7f638731 Add setup OpenAI provider 2025-01-19 20:28:06 +01:00
de046f0529 chore: Bump version to 0.4.5 2025-01-19 17:44:11 +01:00
e975e143b1 fix: Handling Ollama messages 2025-01-19 17:32:12 +01:00
c97c0f62e8 fix: Handling full input message from OpenAI compatible providers 2025-01-19 01:16:33 +01:00
61fded34ea feat: add OpenAI provider settings 2025-01-19 00:50:23 +01:00
289a19ac1a feat: Add OpenAI provider and template 2025-01-17 01:22:12 +01:00
43ac662671 fix: Text width in chat item 2025-01-17 00:46:11 +01:00
1d64d2afc9 refactor: Move to using colors from QtC theme palette 2025-01-15 00:05:12 +01:00
9db61119aa feat: Add check plugin update and dialog for update 2025-01-13 20:11:27 +01:00
70481b3116 Remove discord link from README.md 2025-01-08 16:03:00 +01:00
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
35012865c7 refactor: Claude user message and code completion system prompt 2024-12-25 17:50:47 +01:00
f27429aa66 refactor: Move context to separate lib 2024-12-24 22:45:20 +01:00
113d5adcf4 fix: Add description for deprecated settings 2024-12-24 22:00:53 +01:00
30ea89cdc2 chore: Bump version to 0.4.3 2024-12-23 23:36:47 +01:00
13469edce6 doc: Add Claude to README 2024-12-23 23:34:28 +01:00
ee2c3950e8 fix: path to chat file without project 2024-12-23 23:17:43 +01:00
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
d8ef9d0120 refactor: Update issue templates 2024-12-23 18:36:27 +01:00
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
63f0900511 fix: remove additional message in Ollama Auto Chat template 2024-12-23 16:43:37 +01:00
7dee6f62c0 feat: Add project settings panel 2024-12-21 14:11:45 +01:00
dc06ea2ed5 🔖 chore: Bump version to 0.4.2 2024-12-17 10:33:29 +01:00
fc5e1adc0d 🐛 fix: Fix context for MessageBuilder 2024-12-17 10:32:32 +01:00
93e59fb2dc Add multiline code completion description to README.md 2024-12-17 01:36:28 +01:00
cd2a56cde0 🔖 chore: Bump version to 0.4.1 2024-12-17 00:55:53 +01:00
09cde8fd3d ♻️ refactor: Multiline text suggestion 2024-12-17 00:47:15 +01:00
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
7376a11a05 feat: Add request validator 2024-12-15 02:08:35 +01:00
10e8b16caf 🐛 fix: Content description in network request 2024-12-12 21:02:23 +01:00
a38debb140 🐛 fix: Remove test buttons 2024-12-12 15:25:31 +01:00
844ac35a59 🐛 fix: Change name for OpenRouter provider 2024-12-12 15:09:21 +01:00
16b77a5722 feat: Add stream option to settings 2024-12-10 21:46:39 +01:00
c070fd5cfd feat: Add OpenRouter provider 2024-12-10 21:28:15 +01:00
882047d7b2 ♻️ refactor: Improve response handler for LMStudio 2024-12-10 17:13:56 +01:00
b692402897 ♻️ refactor: Improve Ollama response handler 2024-12-10 08:25:30 +01:00
8102ba95f9 Update README.md 2024-12-03 22:33:43 +01:00
183 changed files with 10413 additions and 1820 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.

View File

@ -6,22 +6,47 @@
"llm",
"ai"
],
"compatibility": "Qt 6.8.1",
"compatibility": "Qt 6.8.2",
"platforms": [
"Windows",
"macOS",
"Linux"
],
"license": "GPLv3",
"version": "0.4.0",
"version": "0.5.6",
"status": "draft",
"is_pack": false,
"released_at": null,
"version_history": [
{
"version": "0.4.0",
"is_latest": true,
"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": true,
"released_at": "2025-04-04T19:00:00Z"
}
],
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",

View File

@ -12,9 +12,9 @@ on:
env:
PLUGIN_NAME: QodeAssist
QT_VERSION: 6.8.1
QT_CREATOR_VERSION: 15.0.0
QT_CREATOR_VERSION_INTERNAL: 15.0.0
QT_VERSION: 6.8.2
QT_CREATOR_VERSION: 16.0.0
QT_CREATOR_VERSION_INTERNAL: 16.0.0
MACOS_DEPLOYMENT_TARGET: "11.0"
CMAKE_VERSION: "3.29.6"
NINJA_VERSION: "1.12.1"
@ -36,8 +36,8 @@ jobs:
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++"
}
@ -70,7 +70,7 @@ jobs:
cmakeVersion: ${{ env.CMAKE_VERSION }}
ninjaVersion: ${{ env.NINJA_VERSION }}
- name: Install system libs
- name: Install dependencies
shell: cmake -P {0}
run: |
if ("${{ runner.os }}" STREQUAL "Linux")
@ -78,7 +78,13 @@ jobs:
COMMAND sudo apt update
)
execute_process(
COMMAND sudo apt install libgl1-mesa-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)
@ -240,19 +246,26 @@ jobs:
# The json is the same for all platforms, but we need to save one
- name: Upload plugin json
if: matrix.config.os == 'ubuntu-latest'
if: startsWith(matrix.config.os, 'ubuntu')
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./build/build/${{ env.PLUGIN_NAME }}.json
- name: Run unit tests
if: startsWith(matrix.config.os, 'ubuntu')
run: |
xvfb-run ./build/build/test/QodeAssistTest
update_json:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: build
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: '20'

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

Submodule 3rdparty/inja added at 384a6bef3f

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
@ -42,28 +66,58 @@ add_qtc_plugin(QodeAssist
LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/StarCoder2Fim.hpp
templates/DeepSeekCoderFim.hpp
templates/CustomFimTemplate.hpp
templates/Qwen.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
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
)
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,17 @@ 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 +31,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 +44,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
@ -133,7 +128,8 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
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, ""});
}
@ -155,7 +151,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 +165,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();

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,28 @@ 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;
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,92 @@
*/
#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);
}
}
});
updateInputTokensCount();
}
ChatModel *ChatRootView::chatModel() const
@ -52,14 +111,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 +143,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 +186,355 @@ 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)) {
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();
}
}
} // 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,22 @@
#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)
QML_ELEMENT
public:
@ -49,38 +47,69 @@ 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);
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();
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.
*

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
{

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,40 +35,46 @@
#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();
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "");
auto attachFiles = m_contextManager->getContentFiles(attachments);
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
auto &chatAssistantSettings = Settings::chatAssistantSettings();
@ -81,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));
@ -90,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;
config.apiKey = Settings::chatAssistantSettings().apiKey();
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();
config.provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QJsonObject request{{"id", QUuid::createUuid().toString()}};
m_requestHandler->sendLLMRequest(config, request);
}
@ -146,9 +151,8 @@ 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)
{
const auto message = response.trimmed();
@ -159,6 +163,7 @@ void ClientInterface::handleLLMResponse(const QString &response,
if (isComplete) {
LOG_MESSAGE(
"Message completed. Final response for message " + messageId + ": " + response);
emit messageReceivedCompletely();
}
}
}
@ -178,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,29 @@
* 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 bool isUserMessage: false
height: msgColumn.height
height: msgColumn.implicitHeight + 10
radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
Column {
ColumnLayout {
id: msgColumn
x: 5
width: parent.width - x
anchors.verticalCenter: parent.verticalCenter
width: parent.width
spacing: 5
Repeater {
@ -49,7 +50,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
@ -80,6 +81,51 @@ Rectangle {
}
}
}
Flow {
id: attachmentsFlow
Layout.fillWidth: true
visible: attachmentsModel.model && attachmentsModel.model.length > 0
leftPadding: 10
rightPadding: 10
spacing: 5
Repeater {
id: attachmentsModel
delegate: Rectangle {
required property int index
required property var modelData
height: attachText.implicitHeight + 8
width: attachText.implicitWidth + 16
radius: 4
color: palette.button
border.width: 1
border.color: palette.mid
Text {
id: attachText
anchors.centerIn: parent
text: modelData
color: palette.text
}
}
}
}
}
Rectangle {
id: userMessageMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: "#92BD6C"
radius: root.radius
visible: root.isUserMessage
}
component TextComponent : TextBlock {
@ -88,8 +134,6 @@ Rectangle {
verticalAlignment: Text.AlignVCenter
leftPadding: 10
text: itemData.text
color: root.fontColor
selectionColor: root.selectionColor
}
@ -104,8 +148,5 @@ Rectangle {
code: itemData.text
language: itemData.language
color: root.codeBgColor
selectionColor: root.selectionColor
}
}

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,11 @@ ChatRootView {
delegate: ChatItem {
required property var model
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
}
header: Item {
@ -69,7 +104,7 @@ ChatRootView {
height: 30
}
ScrollBar.vertical: ScrollBar {
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
}
@ -95,15 +130,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 +161,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 +211,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,7 +26,6 @@ Rectangle {
property string code: ""
property string language: ""
property color selectionColor
readonly property string monospaceFont: {
switch (Qt.platform.os) {
@ -41,6 +40,7 @@ Rectangle {
}
}
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
@ -62,10 +62,10 @@ Rectangle {
readOnly: true
selectByMouse: true
font.family: root.monospaceFont
font.pointSize: 12
font.pointSize: Qt.application.font.pointSize
color: parent.color.hslLightness > 0.5 ? "black" : "white"
wrapMode: Text.WordWrap
selectionColor: root.selectionColor
selectionColor: palette.highlight
}
TextEdit {
@ -80,7 +80,7 @@ Rectangle {
font.pointSize: 8
}
Button {
QoAButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 5

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -26,4 +26,6 @@ TextEdit {
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,249 +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.append("\n ").append(getLanguageAndFileInfo());
if (Settings::codeCompletionSettings().useProjectChangesCache())
fileContext.append("\n ").append(
ChangesManager::instance().getRecentChangesContext(m_textDocument));
return {contextBefore, contextAfter, fileContext};
}
QString DocumentContextReader::getContextBefore(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileBefore(lineNumber, cursorPosition);
} else {
int effectiveStartLine;
int beforeCursor = Settings::codeCompletionSettings().readStringsBeforeCursor();
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - beforeCursor);
} else {
effectiveStartLine = qMax(0, lineNumber - beforeCursor);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
}
QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPosition) const
{
if (Settings::codeCompletionSettings().readFullFile()) {
return readWholeFileAfter(lineNumber, cursorPosition);
} else {
int endLine = qMin(m_document->blockCount() - 1,
lineNumber + Settings::codeCompletionSettings().readStringsAfterCursor());
return getContextBetween(lineNumber + 1, endLine, -1);
}
}
} // namespace QodeAssist

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,45 @@
#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);
}
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{
return "Qode Assist";
return "QodeAssist";
}
void LLMClientInterface::startImpl()
@ -73,7 +88,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,98 +161,168 @@ 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.apiKey = Settings::codeCompletionSettings().apiKey();
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,
promptTemplate->type() == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
: provider->chatEndpoint()));
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)
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();
range["end"] = end;
range["end"] = position;
completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem);
@ -249,37 +334,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,27 @@ 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;
protected:
void startImpl() override;
void sendData(const QByteArray &data) override;
void parseCurrentMessage() override;
private:
void handleInitialize(const QJsonObject &request);
@ -58,16 +73,18 @@ 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);
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,27 +23,25 @@
*/
#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 TextEditor::TextSuggestion::Data &data, QTextDocument *origin)
: TextEditor::TextSuggestion(data, origin)
, m_linesCount(0)
, m_suggestion(data)
LLMSuggestion::LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
{
int startPos = data.range.begin.toPositionInDocument(origin);
int endPos = data.range.end.toPositionInDocument(origin);
const auto &data = suggestions[currentCompletion];
startPos = qBound(0, startPos, origin->characterCount() - 1);
endPos = qBound(startPos, endPos, origin->characterCount() - 1);
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
int endPos = data.range.end.toPositionInDocument(sourceDocument);
QTextCursor cursor(origin);
startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
QTextCursor cursor(sourceDocument);
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
@ -49,64 +52,56 @@ LLMSuggestion::LLMSuggestion(const TextEditor::TextSuggestion::Data &data, QText
int endPosInBlock = endPos - block.position();
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
replacementDocument()->setPlainText(blockText);
}
bool LLMSuggestion::apply()
{
return TextEditor::TextSuggestion::apply();
}
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
{
return TextEditor::TextSuggestion::applyWord(widget);
return applyPart(Word, widget);
}
bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
{
return TextEditor::TextSuggestion::applyLine(widget);
return applyPart(Line, widget);
}
void LLMSuggestion::reset()
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
{
reset();
m_linesCount = 0;
}
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;
void LLMSuggestion::onCounterFinished(int count)
{
Utils::ToolTip::hide();
m_linesCount = 0;
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
cursor.beginEditBlock();
cursor.removeSelectedText();
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
+ (cursor.selectionEnd() - cursor.selectionStart());
QStringList lines = m_completion.text().split('\n');
QString textToInsert = lines.mid(0, count).join('\n');
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
cursor.insertText(textToInsert);
cursor.endEditBlock();
}
if (next == -1)
return apply();
// void LLMSuggestion::reset()
// {
// m_start.removeSelectedText();
// }
if (part == Line)
++next;
// int LLMSuggestion::position()
// {
// return m_start.position();
// }
QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty())
return false;
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);
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(subText.length() - seperatorPos - 1)};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
}
}
return false;
}
} // namespace QodeAssist

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,36 +24,21 @@
#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 TextEditor::TextSuggestion::Data &data, QTextDocument *origin);
enum Part { Word, Line };
LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
bool apply() override;
bool applyWord(TextEditor::TextEditorWidget *widget) override;
bool applyLine(TextEditor::TextEditorWidget *widget) override;
void reset();
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;
int m_startPosition;
TextEditor::TextSuggestion::Data m_suggestion;
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
};
} // 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,13 +1,13 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.4.0",
"Version" : "0.5.6",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"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" : "",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
${IDE_PLUGIN_DEPENDENCIES}
}

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
@ -31,9 +31,10 @@
#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>
using namespace LanguageServerProtocol;
using namespace TextEditor;
@ -43,11 +44,11 @@ using namespace Core;
namespace QodeAssist {
QodeAssistClient::QodeAssistClient()
: LanguageClient::Client(new LLMClientInterface())
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_recentCharCount(0)
{
setName("Qode Assist");
setName("QodeAssist");
LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter);
@ -70,48 +71,63 @@ 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);
}
});
}
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
@ -131,9 +147,13 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
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);
@ -172,8 +192,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 +213,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,9 +231,6 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
if (delta > 0)
completion.setText(completionText.chopped(delta));
}
if (completions.isEmpty())
return;
auto suggestions = Utils::transform(completions, [](const Completion &c) {
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
return Text::Position{pos.line() + 1, pos.character()};
@ -223,9 +240,10 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
editor->insertSuggestion(
std::make_unique<LLMSuggestion>(suggestions.first(), editor->document()));
m_progressHandler.hideProgress();
if (completions.isEmpty())
return;
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
}
}
@ -234,13 +252,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()
@ -250,18 +273,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);

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,19 @@
#pragma once
#include <languageclient/client.h>
#include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp"
#include "widgets/CompletionProgressHandler.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;
@ -43,8 +46,8 @@ public:
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;
@ -58,6 +61,7 @@ private:
QElapsedTimer m_typingTimer;
int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
};
} // 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.
*

271
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/donations:0-brightgreen)
![Static Badge](https://img.shields.io/badge/QtCreator-16.0.0-brightgreen)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
⚠️ **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
⚠️ **Commercial Support and Custom Development**
> The QodeAssist developer offers commercial services for:
> - Adapting the plugin for specific Qt Creator versions
> - Custom development for particular operating systems
> - Integration with specific language models
> - Implementing custom features and modifications
>
> For commercial inquiries, please contact: qodeassist.dev@pm.me
## 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)
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 Features](#file-context-features)
11. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
12. [Development Progress](#development-progress)
13. [Hotkeys](#hotkeys)
14. [Troubleshooting](#troubleshooting)
15. [Support the Development](#support-the-development-of-qodeassist)
16. [How to Build](#how-to-build)
## Overview
- AI-powered code completion
- Chat functionality:
- Side and Bottom panels
- Chat history autosave and restore
- Token usage monitoring and management
- Attach files for one-time code analysis
- Link files for persistent context with auto update in conversations
- Automatic syncing with open editor files (optional)
- Support for multiple LLM providers:
- Ollama
- llama.cpp
- OpenAI
- Anthropic Claude
- LM Studio
- OpenAI-compatible providers(eg. https://openrouter.ai)
- 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>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 @@
<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,83 +172,76 @@ 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 Features
### Template-Model Compatibility
QodeAssist provides two powerful ways to include source code files in your chat conversations: Attachments and Linked Files. Each serves a distinct purpose and helps provide better context for the AI assistant.
| Template | Compatible Models | Purpose |
|----------|------------------|----------|
| CodeLlama FIM | `codellama:code` | Code completion |
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
| Ollama Auto FIM | `Any Ollama base model` | Code completion |
| Qwen FIM | `Qwen 2.5 models` | Code completion |
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
| Alpaca | `starcoder2:instruct` | Chat assistance |
| Basic Chat| `Messages without tokens` | Chat assistance |
| ChatML | `Qwen 2.5 models` | Chat assistance |
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
| Llama3 | `llama3 model family` | Chat assistance |
| Ollama Auto Chat | `Any Ollama chat model` | Chat assistance |
### Attached Files
> Note:
> - FIM (Fill-in-Middle) templates are optimized for code completion
> - Chat templates are designed for interactive dialogue
> - The Ollama Auto templates automatically adapt to most Ollama models
> - Custom Template allows you to define your own prompt format
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
## QtCreator Version Compatibility
- QtCreator 15.0.0 - 0.4.x
- QtCreator 16.0.0 - 0.5.2 - 0.5.x
- 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
@ -162,10 +259,9 @@ ollama run deepseek-coder-v2
- 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
## Troubleshooting
@ -176,20 +272,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

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -19,22 +19,29 @@
#pragma once
#include <QJsonArray>
#include <QFrame>
#include <QLabel>
#include <QLayout>
#include <QPushButton>
#include <QToolButton>
#include "llmcore/PromptTemplate.hpp"
namespace QodeAssist {
namespace QodeAssist::Templates {
class BasicChat : public LLMCore::PromptTemplate
class UpdateStatusWidget : public QFrame
{
Q_OBJECT
public:
LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
QString name() const override { return "Basic Chat"; }
QString promptTemplate() const override { return {}; }
QStringList stopWords() const override { return QStringList(); }
void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
{}
QString description() const override { return "chat without tokens"; }
};
explicit UpdateStatusWidget(QWidget *parent = nullptr);
} // namespace QodeAssist::Templates
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

25
context/CMakeLists.txt Normal file
View File

@ -0,0 +1,25 @@
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
)
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

156
context/ContextManager.cpp Normal file
View File

@ -0,0 +1,156 @@
/*
* 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/projectnodes.h>
#include <texteditor/textdocument.h>
#include "Logger.hpp"
namespace QodeAssist::Context {
ContextManager::ContextManager(QObject *parent)
: QObject(parent)
{}
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) {
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();
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;
context += QString("File: %1\n").arg(filePath);
context += textDocument->plainText();
context += "\n";
}
return context;
}
} // namespace QodeAssist::Context

View File

@ -0,0 +1,54 @@
/*
* 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 "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{});
};
} // 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

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,9 +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 {

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -24,41 +24,31 @@
#include <QObject>
#include "RequestConfig.hpp"
#include "RequestHandlerBase.hpp"
class QNetworkReply;
namespace QodeAssist::LLMCore {
class RequestHandler : public QObject
class RequestHandler : public RequestHandlerBase
{
Q_OBJECT
public:
explicit RequestHandler(QObject *parent = nullptr);
void sendLLMRequest(const LLMConfig &config, const QJsonObject &request);
void handleLLMResponse(QNetworkReply *reply,
const QJsonObject &request,
const LLMConfig &config);
bool cancelRequest(const QString &id);
signals:
void completionReceived(const QString &completion, const QJsonObject &request, bool isComplete);
void requestFinished(const QString &requestId, bool success, const QString &errorString);
void requestCancelled(const QString &id);
void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) override;
bool cancelRequest(const QString &id) override;
void handleLLMResponse(QNetworkReply *reply, const QJsonObject &request, const LLMConfig &config);
private:
QNetworkAccessManager *m_manager;
QMap<QString, QNetworkReply *> m_activeRequests;
QMap<QNetworkReply *, QString> m_accumulatedResponses;
void prepareNetworkRequest(QNetworkRequest &networkRequest, const QString &apiKey) const;
bool processSingleLineCompletion(QNetworkReply *reply,
const QJsonObject &request,
const QString &accumulatedResponse,
const LLMConfig &config);
bool processSingleLineCompletion(
QNetworkReply *reply,
const QJsonObject &request,
const QString &accumulatedResponse,
const LLMConfig &config);
QString removeStopWords(const QStringView &completion, const QStringList &stopWords);
void removeCodeBlockWrappers(QString &response);
};
} // namespace QodeAssist::LLMCore

View File

@ -0,0 +1,30 @@
/*
* 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 "RequestHandlerBase.hpp"
namespace QodeAssist::LLMCore {
RequestHandlerBase::RequestHandlerBase(QObject *parent)
: QObject(parent)
{}
RequestHandlerBase::~RequestHandlerBase() = default;
} // namespace QodeAssist::LLMCore

View File

@ -0,0 +1,45 @@
/*
* 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 "RequestConfig.hpp"
#include <QJsonObject>
#include <QObject>
namespace QodeAssist::LLMCore {
class RequestHandlerBase : public QObject
{
Q_OBJECT
public:
explicit RequestHandlerBase(QObject *parent = nullptr);
~RequestHandlerBase() override;
virtual void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) = 0;
virtual bool cancelRequest(const QString &id) = 0;
signals:
void completionReceived(const QString &completion, const QJsonObject &request, bool isComplete);
void requestFinished(const QString &requestId, bool success, const QString &errorString);
void requestCancelled(const QString &id);
};
} // 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.
*
@ -21,5 +21,5 @@
namespace QodeAssist::LLMCore {
enum RequestType { Fim, Chat };
enum RequestType { CodeCompletion, Chat, Embedding };
}

View File

@ -0,0 +1,57 @@
/*
* 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 "ValidationUtils.hpp"
#include <QJsonArray>
namespace QodeAssist::LLMCore {
QStringList ValidationUtils::validateRequestFields(
const QJsonObject &request, const QJsonObject &templateObj)
{
QStringList errors;
validateFields(request, templateObj, errors);
validateNestedObjects(request, templateObj, errors);
return errors;
}
void ValidationUtils::validateFields(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors)
{
for (auto it = request.begin(); it != request.end(); ++it) {
if (!templateObj.contains(it.key())) {
errors << QString("unknown field '%1'").arg(it.key());
}
}
}
void ValidationUtils::validateNestedObjects(
const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors)
{
for (auto it = request.begin(); it != request.end(); ++it) {
if (templateObj.contains(it.key()) && it.value().isObject()
&& templateObj[it.key()].isObject()) {
validateFields(it.value().toObject(), templateObj[it.key()].toObject(), errors);
validateNestedObjects(it.value().toObject(), templateObj[it.key()].toObject(), errors);
}
}
}
} // namespace QodeAssist::LLMCore

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