├── .clang-format ├── .github └── workflows │ ├── README.md │ ├── build_cmake.yml │ └── reuse.yml ├── .gitignore ├── .reuse └── dep5 ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── LICENSES ├── CC0-1.0.txt └── MIT.txt ├── README.md ├── examples └── qnvim.vim ├── external ├── CMakeLists.txt ├── neovim-qt │ └── CMakeLists.txt └── qtcreator │ ├── CMakeLists.txt │ └── version.cmake ├── src ├── CMakeLists.txt ├── QNVim.json.in ├── log.cpp ├── log.h ├── numbers_column.cpp ├── numbers_column.h ├── qnvim_global.h ├── qnvimconstants.h ├── qnvimcore.cpp ├── qnvimcore.h ├── qnvimplugin.cpp └── qnvimplugin.h └── tools ├── DownloadQtCreator.cmake └── ci ├── DownloadNinjaAndCMake.cmake ├── DownloadQt.cmake ├── InstallSystemLibs.cmake └── Package.cmake /.clang-format: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: None 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | --- 5 | BasedOnStyle: LLVM 6 | IndentWidth: 4 7 | ColumnLimit: 0 8 | --- 9 | Language: Cpp 10 | 11 | Standard: c++17 12 | --- 13 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # GitHub Actions & Workflows 5 | 6 | The `build_cmake.yml` in this directory adds a [GitHub action][1] and workflow that builds 7 | your plugin anytime you push commits to GitHub on Windows, Linux and macOS. 8 | 9 | The build artifacts can be downloaded from GitHub and be installed into an existing Qt Creator 10 | installation. 11 | 12 | When you push a tag, the workflow also creates a new release on GitHub. 13 | 14 | ## Keeping it up to date 15 | 16 | Near the top of the file you find a section starting with `env:`. 17 | 18 | The value for `QT_VERSION` specifies the Qt version to use for building the plugin. 19 | 20 | The value for `QT_CREATOR_VERSION` specifies the Qt Creator version to use for building the plugin. 21 | 22 | The value for `QT_CREATOR_SNAPSHOT` can either be `NO` or `latest` or the build ID of a specific 23 | snapshot build for the Qt Creator version that you specified. 24 | 25 | You need to keep these values updated for different versions of your plugin, and take care 26 | that the Qt version and Qt Creator version you specify are compatible. 27 | 28 | ## What it does 29 | 30 | The build job consists of several steps: 31 | 32 | * Install required packages on the build host 33 | * Download, unpack and install the binary for the Qt version 34 | * Download and unpack the binary for the Qt Creator version 35 | * Build the plugin and upload the plugin libraries to GitHub 36 | * If a tag is pushed, create a release on GitHub for the tag, including zipped plugin libraries 37 | for download 38 | 39 | ## Limitations 40 | 41 | If your plugin requires additional resources besides the plugin library, you need to adapt the 42 | script accordingly. 43 | 44 | [1]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-github-actions 45 | -------------------------------------------------------------------------------- /.github/workflows/build_cmake.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | name: Build plugin 5 | 6 | on: [ push, pull_request ] 7 | 8 | env: 9 | PLUGIN_NAME: QNVim 10 | QT_VERSION: 6.2.4 11 | CMAKE_VERSION: 3.22.4 12 | NINJA_VERSION: 1.10.1 13 | 14 | jobs: 15 | build: 16 | name: ${{ matrix.config.name }} 17 | runs-on: ${{ matrix.config.os }} 18 | strategy: 19 | matrix: 20 | config: 21 | - { 22 | name: "Windows MSVC 2022", artifact: "Windows-x64", 23 | os: windows-2022, 24 | cc: "cl", cxx: "cl", 25 | } 26 | - { 27 | name: "Ubuntu GCC 11", artifact: "Linux-x64", 28 | os: ubuntu-22.04, 29 | cc: "gcc", cxx: "g++" 30 | } 31 | - { 32 | name: "macOS Clang 14", artifact: "macOS-x64", 33 | os: macos-12, 34 | cc: "clang", cxx: "clang++" 35 | } 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | 40 | - name: Download Ninja and CMake 41 | run: cmake -P tools/ci/DownloadNinjaAndCMake.cmake 42 | 43 | - name: Install system libs 44 | run: cmake -P tools/ci/InstallSystemLibs.cmake 45 | 46 | - name: Download Qt 47 | id: qt 48 | run: cmake -P tools/ci/DownloadQt.cmake 49 | 50 | # To create a package, we need a script, that comes with Qt Creator 51 | # This is why we fetch it first, before invoking CMake 52 | - name: Download Qt Creator 53 | id: qt_creator 54 | run: cmake -P tools/DownloadQtCreator.cmake 55 | 56 | - name: Download Neovim 57 | uses: rhysd/action-setup-vim@v1 58 | with: 59 | neovim: true 60 | version: stable 61 | 62 | - name: Setup MSVC environment 63 | uses: ilammy/msvc-dev-cmd@v1 64 | if: ${{ matrix.config.cc == 'cl' }} 65 | 66 | - name: Build and Package 67 | run: cmake -P tools/ci/Package.cmake 68 | env: 69 | ARTIFACT_SUFFIX: ${{ matrix.config.artifact }} 70 | CC: ${{ matrix.config.cc }} 71 | CXX: ${{ matrix.config.cxx }} 72 | MACOSX_DEPLOYMENT_TARGET: "10.13" 73 | QT_DIR: ${{ steps.qt.outputs.qt_dir }} 74 | QT_CREATOR_VERSION: ${{ steps.qt_creator.outputs.qtc_ver }} 75 | 76 | - uses: actions/upload-artifact@v3 77 | id: upload_artifact 78 | with: 79 | path: ./${{ env.PLUGIN_NAME }}-${{ steps.qt_creator.outputs.qtc_ver }}-${{ matrix.config.artifact }}.7z 80 | name: ${{ env.PLUGIN_NAME}}-${{ steps.qt_creator.outputs.qtc_ver }}-${{ matrix.config.artifact }}.7z 81 | outputs: 82 | qtc_ver: ${{ steps.qt_creator.outputs.qtc_ver }} 83 | 84 | release: 85 | if: contains(github.ref, 'tags/v') 86 | runs-on: ubuntu-20.04 87 | needs: build 88 | 89 | steps: 90 | - name: Create Release 91 | id: create_release 92 | run: | 93 | gh release -R ${{ github.repository }} create ${{ github.ref_name }} -t "${{ env.NAME }}" 94 | env: 95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 96 | NAME: Release ${{ github.ref_name }} 97 | outputs: 98 | qtc_ver: ${{ needs.build.outputs.qtc_ver }} 99 | 100 | publish: 101 | if: contains(github.ref, 'tags/v') 102 | 103 | name: ${{ matrix.config.name }} 104 | runs-on: ${{ matrix.config.os }} 105 | strategy: 106 | matrix: 107 | config: 108 | - { 109 | name: "Windows Latest x64", artifact: "Windows-x64.7z", 110 | os: ubuntu-22.04 111 | } 112 | - { 113 | name: "Linux Latest x64", artifact: "Linux-x64.7z", 114 | os: ubuntu-22.04 115 | } 116 | - { 117 | name: "macOS Latest x64", artifact: "macOS-x64.7z", 118 | os: macos-12 119 | } 120 | needs: release 121 | 122 | steps: 123 | - name: Download artifact 124 | uses: actions/download-artifact@v3 125 | with: 126 | name: ${{ env.PLUGIN_NAME }}-${{ needs.release.outputs.qtc_ver }}-${{ matrix.config.artifact }} 127 | path: ./ 128 | 129 | - name: Upload to Release 130 | id: upload_to_release 131 | run: | 132 | gh release -R ${{ github.repository }} upload ${{ github.ref_name }} ${{ env.ASSET_PATH }} 133 | env: 134 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 135 | ASSET_PATH: ./${{ env.PLUGIN_NAME }}-${{ needs.release.outputs.qtc_ver }}-${{ matrix.config.artifact }} 136 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: REUSE Compliance Check 6 | 7 | on: [push, pull_request] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: REUSE Compliance Check 15 | uses: fsfe/reuse-action@v1 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: None 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | # C++ objects and libs 5 | 6 | *.slo 7 | *.lo 8 | *.o 9 | *.a 10 | *.la 11 | *.lai 12 | *.so 13 | *.dll 14 | *.dylib 15 | 16 | # Qt-es 17 | 18 | /.qmake.cache 19 | /.qmake.stash 20 | *.pro.user 21 | *.pro.user.* 22 | *.qbs.user 23 | *.qbs.user.* 24 | *.moc 25 | moc_*.cpp 26 | moc_*.h 27 | qrc_*.cpp 28 | ui_*.h 29 | Makefile* 30 | *build-* 31 | qnvim.pro.user* 32 | 33 | # QtCreator 34 | 35 | *.autosave 36 | 37 | # QtCtreator Qml 38 | *.qmlproject.user 39 | *.qmlproject.user.* 40 | 41 | # QtCtreator CMake 42 | CMakeLists.txt.user* 43 | 44 | tags 45 | 46 | build*/ 47 | compile_commands.json 48 | .cache 49 | .DS_Store 50 | 51 | # Downloaded QtCreator path 52 | external/qtcreator/dist*/ 53 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: QNVim 3 | Upstream-Contact: Sassan Haradji 4 | Source: https://github.com/sassanh/qnvim 5 | 6 | # Sample paragraph, commented out: 7 | # 8 | # Files: src/* 9 | # Copyright: $YEAR $NAME <$CONTACT> 10 | # License: ... 11 | 12 | Files: external/neovim-qt/patches/0001-fix-share-current-directory-to-interface-includes.patch 13 | Copyright: 2022 Mikhail Zolotukhin 14 | License: ISC 15 | 16 | Files: external/neovim-qt/patches/0002-fix-use-current-project-directory-for-icons-install.patch 17 | Copyright: 2022 Mikhail Zolotukhin 18 | License: ISC 19 | 20 | Files: src/QNVim.json.in 21 | Copyright: none 22 | License: CC0-1.0 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Changelog 5 | 6 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased](https://github.com/crow-translate/crow-translate/tree/HEAD) 9 | 10 | [Full Changelog](https://github.com/crow-translate/crow-translate/compare/2.3.1...HEAD) 11 | 12 | **Changed** 13 | 14 | - Fix `number` not showing correctly when `relativenumber` was enabled. 15 | - Remove cursor blinking. 16 | - Fix incorrect plugin toggling. 17 | 18 | ## [1.2.0](https://github.com/sassanh/qnvim/tree/1.2.0) (2019-09-28) 19 | 20 | [Full Changelog](https://github.com/sassanh/qnvim/compare/1.1.0...1.2.0) 21 | 22 | **Changed** 23 | 24 | - Zooming text editor (Ctrl + Mouse Wheel) updates block cursor width now. 25 | - Fix relative number column. 26 | - Zooming text editor doesn't ruin relative number column anymore. 27 | - Fix toggle action not removing command-line and not showing native Qt Creator status line. 28 | - Addressed all compilation warnings (except for diff_match_patch files). 29 | - `:e` won't break editor state anymore. 30 | - Support almost all buffer types (gina, fugitive, etc). 31 | - Fix lots of small bugs. 32 | 33 | ## [1.1.0](https://github.com/sassanh/qnvim/tree/1.1.0) (2019-07-16) 34 | 35 | [Full Changelog](https://github.com/sassanh/qnvim/compare/1.0.2...1.1.0) 36 | 37 | **Changed** 38 | 39 | - If `QNVIM_always_text` is set, it'll always open files opened by neovim with a text editor (avoid openning resource editor for example). 40 | - Fix a segmentation fault that happened when `Open With` menu was used. 41 | - Automatically run `cd` (change directory) in neovim when files changes in Qt Creator. It runs `cd` with the directory of the project (not the file). 42 | 43 | ## [1.0.2](https://github.com/sassanh/qnvim/tree/1.0.2) (2019-07-16) 44 | 45 | [Full Changelog](https://github.com/sassanh/qnvim/compare/1.0.1...1.0.2) 46 | 47 | **Changed** 48 | 49 | - Fix a segmentation fault which happened after exiting some special terminal buffers (like fzf). 50 | - Remove padding from patches in synching from neovim to Qt Creator, previously changes from neovim would make Qt Creator flicker colors around the change and would make Qt Creator spend CPU power to detect colors because it was rewriting texts around the patch (the patch had a padding) Now there should be no flickering, unnecessary CPU usage, etc when syncing from neovim to Qt Creator. 51 | 52 | ## [1.0.1](https://github.com/sassanh/qnvim/tree/1.0.1) (2019-07-15) 53 | 54 | [Full Changelog](https://github.com/sassanh/qnvim/compare/1.0.0...1.0.1) 55 | 56 | **Changed** 57 | 58 | - Use `nvim_buf_set_lines` for synching from Qt Creator to neovim which should make synching from Qt Creator to neovim much more stable. (We still need patching for synching from neovim to Qt Creator because setting the whole buffer in Qt Creator each time and edit happens in neovim will be super slow.) 59 | - Better handling status bar (showing multiline messages completely in multiple lines, avoiding scrollbar in any situation, etc). 60 | - Fix status bar when showing mutliline messages would make status bar go blank forever. 61 | 62 | ## [1.0.0](https://github.com/sassanh/qnvim/tree/1.0.0) (2019-06-05) 63 | 64 | [Full Changelog](https://github.com/sassanh/qnvim/compare/0.4.0...1.0.0) 65 | 66 | **Changed** 67 | 68 | - `$MYQVIMRC` is now a file named `qnvim.vim` in the same directory as `$MYVIMRC`. 69 | 70 | ## [0.4.0](https://github.com/sassanh/qnvim/tree/0.4.0) (2019-04-29) 71 | 72 | **Added** 73 | 74 | - `ext_messages` so now `echo` and its family are supported (considering we already had `ext_cmdline`, cmdline should be all supported). 75 | - Tooltip for cmdline so that in case of big messages user can see the whole message by hovering the mouse over it). 76 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | cmake_minimum_required(VERSION 3.22) 5 | 6 | if (POLICY CMP0135) 7 | cmake_policy(SET CMP0135 NEW) 8 | endif() 9 | 10 | project(QNVim) 11 | 12 | option(FETCH_QTC "Download Qt Creator development files automatically" ON) 13 | 14 | add_subdirectory(external) 15 | 16 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 17 | set(CMAKE_AUTOMOC ON) 18 | set(CMAKE_AUTORCC ON) 19 | set(CMAKE_AUTOUIC ON) 20 | set(CMAKE_CXX_STANDARD 17) 21 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 22 | set(CMAKE_CXX_EXTENSIONS OFF) 23 | 24 | if (MSVC) 25 | add_compile_options("/permissive-") 26 | endif() 27 | 28 | find_package(QtCreator REQUIRED COMPONENTS Core) 29 | find_package(Qt6 REQUIRED COMPONENTS Widgets Network) 30 | 31 | add_subdirectory(src) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2019 Sassan Haradji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # qnvim 5 | 6 | qnvim is a Qt Creator plugin for users who like editing text in Neovim/Vim and also want to use Qt Creator features. This plugin combines the power of Neovim and Qt Creator. 7 | 8 | With qnvim, you can run your `init.lua`/`init.vim`, all your Neovim plugins, and any tweaks made in `init.vim`/`init.lua`. 9 | 10 |

11 | 12 | 13 | 14 |

15 | 16 | ## Status 17 | 18 | qnvim is under development, but it's mostly stable and usable. Currently, there are some known issues: 19 | 20 | - It doesn't support splits or windows 21 | - It should use vim highlights for buffers that Qt Creator doesn't support (like Vim helpfiles and many others) 22 | 23 | Most Neovim plugins should work fine, except for a few that rely on highlights or special buffers. Qt Creator already provides excellent highlighting for C++ and QML, so that's not a problem. Work is in progress to handle all types of buffers. 24 | 25 | Please report any issues you encounter and consider contributing to this project if you have the time. 26 | 27 | ## How does qnvim compare with Qt Creator Vim mode? 28 | 29 | qnvim provides a smoother integration of Neovim within Qt Creator by running an actual instance of Neovim. This allows you to use all your Neovim plugins and customizations directly in Qt Creator. On the other hand, Qt Creator Vim mode is a built-in feature that emulates Vim, offering basic Vim keybindings and functionality but does not support the full range of Neovim features and plugins. The main difference between the two is that qnvim runs a real instance of Neovim, while Qt Creator Vim mode is an emulation of Vim. 30 | 31 | ## Installation instructions 32 | 33 | ### From Releases section 34 | 35 | Go to the releases section and download the version of the plugin matching your Qt Creator version and operating system. Then: 36 | 37 | 1. Open Qt Creator > Help > About Plugins > Install Plugin... 38 | 2. Select the plugin you've downloaded earlier and relaunch Qt Creator. 39 | 40 | ### Building from source 41 | 42 | > ⚠️ **Warning** ⚠️ 43 | > 44 | > As per Qt policies, major and minor versions of Qt Creator Plugin APIs are not compatible. This means that there is no guarantee that the plugin version on the master branch is compatible with any version of Qt Creator not specified in the cmake/FetchQtCreator.cmake file. 45 | 46 | 1. Make sure you have Qt development files installed on your system. 47 | 2. Clone this repository and go to its directory. Checkout a Git tag that is compatible with your Qt Creator version. 48 | 3. `cmake -S . -B build/`. 49 | 4. `cmake --build build/`. The compiled plugin will be inside `build/lib/qtcreator/plugins`. 50 | 5. Open Qt Creator > Help > About Plugins > Install Plugin... Select the plugin you have built earlier. 51 | 52 | #### Updating 53 | 54 | Before updating from source, delete the `build` directory from earlier to avoid problems such as [this](https://github.com/sassanh/qnvim/issues/8#issuecomment-485456543). 55 | 56 | To update the plugin you need to recompile it: checkout a tag, that matches your Qt Creator version and execute the steps above again. 57 | 58 | ### Arch Linux 59 | 60 | Arch Linux users can install [qnvim-git](https://aur.archlinux.org/packages/qnvim-git) from AUR via AUR helper or with the following commands: 61 | 62 | ```bash 63 | git clone https://aur.archlinux.org/qnvim-git.git 64 | cd qnvim-git 65 | makepkg -si 66 | ``` 67 | 68 | ## Configuration 69 | 70 | You can add custom Vim commands for your Qt Creator environment in a `qnvim.vim` file located in the same directory as `init.vim` (`:help $MYVIMRC`). `$MYQVIMRC` is set to the path (mind the `Q` after `MY`). 71 | 72 | ### Sample `qnvim.vim` 73 | 74 | There's a sample `examples/qnvim.vim` file available in the repository. It provides most of the convenient keyboard shortcuts for building, deploying, running, switching buffers, switching tabs, and more. It will also help you understand how to create new keyboard shortcuts using Qt Creator commands. 75 | 76 | ## Credits 77 | 78 | - [Neovim](https://neovim.io) 79 | - [Qt](https://www.qt.io) 80 | - [Qt Creator](https://www.qt.io/product) 81 | - [Neovim Qt](https://github.com/equalsraf/neovim-qt) 82 | 83 | And the libraries used in above projects and are mentioned in their docs. 84 | -------------------------------------------------------------------------------- /examples/qnvim.vim: -------------------------------------------------------------------------------- 1 | " SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | " SPDX-License-Identifier: CC0-1.0 3 | 4 | nnoremap :Build 5 | nnoremap :Deploy 6 | nnoremap :Run 7 | nnoremap :Target 8 | 9 | command! B :Build 10 | command! BD :Build|Deploy 11 | command! BR :Build|Run 12 | command! BDR :Build|Deploy|Run 13 | command! D :Deploy 14 | command! DR :Deploy|Run 15 | command! R :Run 16 | command! Q :QMake 17 | nnoremap qq :Q 18 | nnoremap 1q :B 19 | nnoremap 2q :D 20 | nnoremap 3q :BD 21 | nnoremap 4q :R 22 | nnoremap 5q :BR 23 | nnoremap 6q :DR 24 | nnoremap 7q :BDR 25 | 26 | 27 | nnoremap == :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'TextEditor.AutoIndentSelection') 28 | vnoremap = :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'TextEditor.AutoIndentSelection') 29 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'TextEditor.FollowSymbolUnderCursor', 'TextEditor.JumpToFileUnderCursor') 30 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'CppTools.SwitchHeaderSource') 31 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Close', 'TextEditor.JumpToFileUnderCursor') 32 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.GotoPreviousInHistory') 33 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.GotoPreviousInHistory') 34 | inoremap rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'TextEditor.CompleteThis') ? '' : '' 35 | 36 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Pane.Issues') 37 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Pane.SearchResults') 38 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Pane.ApplicationOutput') 39 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Pane.CompileOutput') 40 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Pane.DebuggerConsole') 41 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Pane.To-DoEntries') 42 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Pane.GeneralMessages') 43 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Pane.VersionControl') 44 | 45 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Mode.Welcome') 46 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Mode.Edit') 47 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Mode.Design') 48 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Mode.Mode.Debug') 49 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Mode.Project') 50 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Mode.Help') 51 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Mode.Welcome') 52 | 53 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'Help.Context') 54 | 55 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.ToggleLeftSidebar') 56 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.ToggleModeSelector') 57 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.ToggleRightSidebar') 58 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.ToggleFullScreen') 59 | nnoremap :Target 60 | 61 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'TextEditor.QuickFix') 62 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QtCreator.Options') 63 | 64 | nnoremap :call rpcnotify(g:neovim_channel, 'Gui', 'triggerCommand', 'QNVim.Toggle') 65 | 66 | unmap 67 | 68 | set norelativenumber 69 | -------------------------------------------------------------------------------- /external/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | add_subdirectory(neovim-qt) 5 | 6 | if (FETCH_QTC) 7 | add_subdirectory(qtcreator) 8 | endif() 9 | 10 | set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH}" PARENT_SCOPE) 11 | -------------------------------------------------------------------------------- /external/neovim-qt/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | include(FetchContent) 5 | 6 | FetchContent_Declare( 7 | neovimqt 8 | GIT_REPOSITORY https://github.com/gikari/neovim-qt.git 9 | GIT_TAG c84f81712639140f910a1d38acb8edcb4509dc4d # qt6 branch 10 | ) 11 | 12 | FetchContent_GetProperties(neovimqt) 13 | if(NOT neovimqt_POPULATED) 14 | FetchContent_Populate(neovimqt) 15 | 16 | # We use this, instead of MakeAvailable, 17 | # so that neovimqt is not installed in CI artifacts 18 | add_subdirectory(${neovimqt_SOURCE_DIR} ${neovimqt_BINARY_DIR} EXCLUDE_FROM_ALL) 19 | endif() 20 | -------------------------------------------------------------------------------- /external/qtcreator/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | include("version.cmake") 5 | 6 | set(QTC_DIST_PATH 7 | "${PROJECT_SOURCE_DIR}/external/qtcreator/dist-${CMAKE_SYSTEM_NAME}-${QT_CREATOR_VERSION}") 8 | if (NOT EXISTS "${QTC_DIST_PATH}") 9 | execute_process(COMMAND 10 | ${CMAKE_COMMAND} -P "${PROJECT_SOURCE_DIR}/tools/DownloadQtCreator.cmake" 11 | WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" 12 | ) 13 | endif() 14 | 15 | if (APPLE) 16 | list(APPEND CMAKE_PREFIX_PATH "${QTC_DIST_PATH}/Qt Creator.app/Contents/Resources") 17 | endif() 18 | 19 | list(APPEND CMAKE_PREFIX_PATH "${QTC_DIST_PATH}") 20 | 21 | set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH}" PARENT_SCOPE) 22 | -------------------------------------------------------------------------------- /external/qtcreator/version.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: None 2 | # SPDX-License-Identifier: MIT 3 | 4 | set(QT_CREATOR_VERSION "10.0.0") 5 | set(QT_CREATOR_SNAPSHOT "") 6 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | add_qtc_plugin(QNVim 5 | PLUGIN_DEPENDS 6 | QtCreator::Core 7 | QtCreator::TextEditor 8 | QtCreator::ProjectExplorer 9 | DEPENDS 10 | Qt::Widgets 11 | QtCreator::ExtensionSystem 12 | QtCreator::Utils 13 | neovim-qt 14 | neovim-qt-gui 15 | SOURCES 16 | log.cpp 17 | log.h 18 | numbers_column.cpp 19 | numbers_column.h 20 | qnvim_global.h 21 | qnvimconstants.h 22 | qnvimplugin.cpp 23 | qnvimplugin.h 24 | qnvimcore.cpp 25 | qnvimcore.h 26 | ) 27 | -------------------------------------------------------------------------------- /src/QNVim.json.in: -------------------------------------------------------------------------------- 1 | { 2 | \"Name\" : \"QNVim\", 3 | \"Version\" : \"10.0.0_1\", 4 | \"Vendor\" : \"Sassan Haradji\", 5 | \"Copyright\" : \"(C) Sassan Haradji\", 6 | \"License\" : \"MIT\", 7 | \"Description\" : \"Neovim Backend for Qt Creator\", 8 | \"Url\" : \"https://github.com/sassanh/qnvim\", 9 | $$dependencyList 10 | } 11 | -------------------------------------------------------------------------------- /src/log.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include "log.h" 5 | 6 | Q_LOGGING_CATEGORY(Main, "qtcreator.plugin.qnvim.main") 7 | Q_LOGGING_CATEGORY(Buffer, "qtcreator.plugin.qnvim.buffer", QtWarningMsg) 8 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | Q_DECLARE_LOGGING_CATEGORY(Main); 9 | Q_DECLARE_LOGGING_CATEGORY(Buffer); 10 | -------------------------------------------------------------------------------- /src/numbers_column.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | // SPDX-License-Identifier: MIT 3 | 4 | #include "numbers_column.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace QNVim { 15 | namespace Internal { 16 | 17 | NumbersColumn::NumbersColumn() { 18 | setAttribute(Qt::WA_TransparentForMouseEvents, true); 19 | connect(TextEditor::TextEditorSettings::instance(), 20 | &TextEditor::TextEditorSettings::displaySettingsChanged, 21 | this, &NumbersColumn::updateGeometry); 22 | } 23 | 24 | void NumbersColumn::setEditor(TextEditor::TextEditorWidget *editor) { 25 | if (editor == mEditor) 26 | return; 27 | 28 | if (mEditor) { 29 | mEditor->removeEventFilter(this); 30 | disconnect(mEditor, &QPlainTextEdit::cursorPositionChanged, 31 | this, &NumbersColumn::updateGeometry); 32 | disconnect(mEditor->verticalScrollBar(), &QScrollBar::valueChanged, 33 | this, &NumbersColumn::updateGeometry); 34 | disconnect(mEditor->document(), &QTextDocument::contentsChanged, 35 | this, &NumbersColumn::updateGeometry); 36 | } 37 | 38 | mEditor = editor; 39 | setParent(mEditor); 40 | 41 | if (mEditor) { 42 | mEditor->installEventFilter(this); 43 | connect(mEditor, &QPlainTextEdit::cursorPositionChanged, 44 | this, &NumbersColumn::updateGeometry); 45 | connect(mEditor->verticalScrollBar(), &QScrollBar::valueChanged, 46 | this, &NumbersColumn::updateGeometry); 47 | connect(mEditor->document(), &QTextDocument::contentsChanged, 48 | this, &NumbersColumn::updateGeometry); 49 | show(); 50 | } else 51 | hide(); 52 | 53 | updateGeometry(); 54 | } 55 | 56 | void NumbersColumn::setNumber(bool number) { 57 | mNumber = number; 58 | updateGeometry(); 59 | } 60 | 61 | void NumbersColumn::paintEvent(QPaintEvent *event) { 62 | if (not mEditor) 63 | return; 64 | 65 | QTextCursor firstVisibleCursor = mEditor->cursorForPosition(QPoint(0, 0)); 66 | QTextBlock firstVisibleBlock = firstVisibleCursor.block(); 67 | 68 | if (firstVisibleCursor.positionInBlock() > 0) { 69 | firstVisibleBlock = firstVisibleBlock.next(); 70 | firstVisibleCursor.setPosition(firstVisibleBlock.position()); 71 | } 72 | 73 | QTextBlock block = mEditor->textCursor().block(); 74 | bool forward = firstVisibleBlock.blockNumber() > block.blockNumber(); 75 | int n = 0; 76 | 77 | while (block.isValid() and block != firstVisibleBlock) { 78 | block = forward ? block.next() : block.previous(); 79 | 80 | if (block.isVisible()) 81 | n += forward ? 1 : -1; 82 | } 83 | 84 | QPainter p(this); 85 | QPalette pal = mEditor->extraArea()->palette(); 86 | const QColor fg = pal.color(QPalette::WindowText); 87 | const QColor bg = pal.color(QPalette::Window); 88 | p.setPen(fg); 89 | 90 | qreal lineHeight = block.layout()->boundingRect().height(); 91 | QRectF rect(0, mEditor->cursorRect(firstVisibleCursor).y(), width(), lineHeight); 92 | bool hideLineNumbers = mEditor->lineNumbersVisible(); 93 | 94 | while (block.isValid()) { 95 | if (block.isVisible()) { 96 | if ((not mNumber or n != 0) and rect.intersects(event->rect())) { 97 | const int line = qAbs(n); 98 | const QString number = QString::number(line); 99 | 100 | if (hideLineNumbers) 101 | p.fillRect(rect, bg); 102 | if (hideLineNumbers or line < 100) 103 | p.drawText(rect, Qt::AlignRight | Qt::AlignVCenter, number); 104 | } 105 | 106 | rect.translate(0, lineHeight * block.lineCount()); 107 | if (rect.y() > height()) 108 | break; 109 | 110 | ++n; 111 | } 112 | 113 | block = block.next(); 114 | } 115 | } 116 | 117 | bool NumbersColumn::eventFilter(QObject *, QEvent *event) { 118 | if (event->type() == QEvent::Resize or event->type() == QEvent::Move) 119 | updateGeometry(); 120 | 121 | return false; 122 | } 123 | 124 | void NumbersColumn::updateGeometry() { 125 | if (not mEditor) 126 | return; 127 | 128 | QFontMetrics fm(mEditor->textDocument()->fontSettings().font()); 129 | int lineHeight = fm.lineSpacing(); 130 | setFont(mEditor->extraArea()->font()); 131 | 132 | QRect rect = mEditor->extraArea()->geometry().adjusted(0, 0, -3, 0); 133 | bool marksVisible = mEditor->marksVisible(); 134 | bool lineNumbersVisible = mEditor->lineNumbersVisible(); 135 | bool foldMarksVisible = mEditor->codeFoldingVisible(); 136 | 137 | if (marksVisible and lineNumbersVisible) 138 | rect.setLeft(lineHeight); 139 | 140 | if (foldMarksVisible and (marksVisible or lineNumbersVisible)) 141 | rect.setRight(rect.right() - (lineHeight + lineHeight % 2)); 142 | 143 | setGeometry(rect); 144 | 145 | update(); 146 | } 147 | 148 | } // namespace Internal 149 | } // namespace QNVim 150 | -------------------------------------------------------------------------------- /src/numbers_column.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace TextEditor { 9 | class TextEditorWidget; 10 | } 11 | 12 | namespace QNVim { 13 | namespace Internal { 14 | 15 | class NumbersColumn : public QWidget { 16 | Q_OBJECT 17 | bool mNumber = false; 18 | TextEditor::TextEditorWidget *mEditor = nullptr; 19 | 20 | public: 21 | NumbersColumn(); 22 | 23 | void setEditor(TextEditor::TextEditorWidget *); 24 | void setNumber(bool); 25 | void updateGeometry(); 26 | 27 | protected: 28 | void paintEvent(QPaintEvent *event); 29 | bool eventFilter(QObject *, QEvent *); 30 | }; 31 | 32 | } // namespace Internal 33 | } // namespace QNVim 34 | -------------------------------------------------------------------------------- /src/qnvim_global.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #if defined(QNVIM_LIBRARY) 9 | # define QNVIMSHARED_EXPORT Q_DECL_EXPORT 10 | #else 11 | # define QNVIMSHARED_EXPORT Q_DECL_IMPORT 12 | #endif 13 | -------------------------------------------------------------------------------- /src/qnvimconstants.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | 6 | namespace QNVim { 7 | namespace Constants { 8 | 9 | const char TOGGLE_ID[] = "QNVim.Toggle"; 10 | const char MENU_ID[] = "QNVim.Menu"; 11 | 12 | } // namespace Constants 13 | } // namespace QNVim 14 | -------------------------------------------------------------------------------- /src/qnvimcore.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | // SPDX-FileCopyrightText: 2023 Mikhail Zolotukhin 3 | // SPDX-License-Identifier: MIT 4 | #include "qnvimcore.h" 5 | 6 | #include "numbers_column.h" 7 | #include "log.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | 51 | namespace QNVim { 52 | namespace Internal { 53 | 54 | QNVimCore::QNVimCore(QObject *parent) 55 | : QObject{parent} { 56 | qDebug(Main) << "QNVimCore::constructor"; 57 | 58 | mCMDLine = new QPlainTextEdit; 59 | Core::StatusBarManager::addStatusBarWidget(mCMDLine, Core::StatusBarManager::First); 60 | mCMDLine->document()->setDocumentMargin(0); 61 | mCMDLine->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 62 | mCMDLine->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 63 | mCMDLine->setLineWrapMode(QPlainTextEdit::NoWrap); 64 | mCMDLine->setMinimumWidth(200); 65 | mCMDLine->setFocusPolicy(Qt::StrongFocus); 66 | mCMDLine->installEventFilter(this); 67 | mCMDLine->setFont(TextEditor::TextEditorSettings::instance()->fontSettings().font()); 68 | 69 | qobject_cast(mCMDLine->parentWidget()->children()[2])->hide(); 70 | 71 | saveCursorFlashTime(QApplication::cursorFlashTime()); 72 | 73 | connect(Core::EditorManager::instance(), &Core::EditorManager::editorAboutToClose, 74 | this, &QNVimCore::editorAboutToClose); 75 | connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged, 76 | this, &QNVimCore::editorOpened); 77 | 78 | mNumbersColumn = new NumbersColumn(); 79 | mNVim = NeovimQt::NeovimConnector::spawn({"--cmd", "let g:QNVIM=1"}); 80 | 81 | connect(mNVim, &NeovimQt::NeovimConnector::ready, this, [=]() { 82 | mNVim->api2()->nvim_command(QStringLiteral("\ 83 | let g:QNVIM_always_text=v:true\n\ 84 | let g:neovim_channel=%1\n\ 85 | execute \"command -bar Build call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Build')\"\n\ 86 | execute \"command -bar BuildProject call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Build')\"\n\ 87 | execute \"command -bar BuildAll call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.BuildSession')\"\n\ 88 | execute \"command -bar Rebuild call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Rebuild')\"\n\ 89 | execute \"command -bar RebuildProject call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Rebuild')\"\n\ 90 | execute \"command -bar RebuildAll call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.RebuildSession')\"\n\ 91 | execute \"command -bar Clean call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Clean')\"\n\ 92 | execute \"command -bar CleanProject call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Clean')\"\n\ 93 | execute \"command -bar CleanAll call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.CleanSession')\"\n\ 94 | execute \"command -bar Deploy call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Deploy')\"\n\ 95 | execute \"command -bar DeployProject call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Deploy')\"\n\ 96 | execute \"command -bar DeployAll call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.DeploySession')\"\n\ 97 | execute \"command -bar Run call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Run')\"\n\ 98 | execute \"command -bar Debug call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Debug')\"\n\ 99 | execute \"command -bar DebugStart call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Debug')\"\n\ 100 | execute \"command -bar DebugContinue call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.Continue')\"\n\ 101 | execute \"command -bar QMake call rpcnotify(%1, 'Gui', 'triggerCommand', 'Qt4Builder.RunQMake')\"\n\ 102 | execute \"command -bar Target call rpcnotify(%1, 'Gui', 'triggerCommand', 'ProjectExplorer.SelectTargetQuick')\"\n\ 103 | \ 104 | execute \"autocmd BufReadCmd * :call rpcnotify(%1, 'Gui', 'fileAutoCommand', 'BufReadCmd', expand(''), expand(':p'), &buftype, &buflisted, &bufhidden, g:QNVIM_always_text)\"\n\ 105 | execute \"autocmd TermOpen * :call rpcnotify(%1, 'Gui', 'fileAutoCommand', 'TermOpen', expand(''), expand(':p'), &buftype, &buflisted, &bufhidden, g:QNVIM_always_text)\"\n\ 106 | execute \"autocmd BufWriteCmd * :call rpcnotify(%1, 'Gui', 'fileAutoCommand', 'BufWriteCmd', expand(''), expand(':p'), &buftype, &buflisted, &bufhidden, g:QNVIM_always_text)|set nomodified\"\n\ 107 | execute \"autocmd BufEnter * nested :call rpcnotify(%1, 'Gui', 'fileAutoCommand', 'BufEnter', expand(''), expand(':p'), &buftype, &buflisted, &bufhidden, g:QNVIM_always_text)\"\n\ 108 | execute \"autocmd BufDelete * nested :call rpcnotify(%1, 'Gui', 'fileAutoCommand', 'BufDelete', expand(''), expand(':p'), &buftype, &buflisted, &bufhidden, g:QNVIM_always_text)\"\n\ 109 | execute \"autocmd BufHidden * nested :call rpcnotify(%1, 'Gui', 'fileAutoCommand', 'BufHidden', expand(''), expand(':p'), &buftype, &buflisted, &bufhidden, g:QNVIM_always_text)\"\n\ 110 | execute \"autocmd BufWipeout * nested :call rpcnotify(%1, 'Gui', 'fileAutoCommand', 'BufWipeout', expand(''), expand(':p'), &buftype, &buflisted, &bufhidden, g:QNVIM_always_text)\"\n\ 111 | execute \"autocmd FileType help set modifiable|read |set nomodifiable\"\n\ 112 | \ 113 | function! SetCursor(line, col)\n\ 114 | call cursor(a:line, a:col)\n\ 115 | if mode()[0] ==# 'i' or mode()[0] ==# 'R'\n\ 116 | normal! i\x07u\x03\n\ 117 | endif\n\ 118 | call cursor(a:line, a:col)\n\ 119 | endfunction\n\ 120 | autocmd VimEnter * let $MYQVIMRC=substitute(substitute($MYVIMRC, 'init.vim$', 'qnvim.vim', 'g'), 'init.lua$', 'qnvim.vim', 'g') | source $MYQVIMRC") 121 | .arg(mNVim->channel()).toUtf8()); 122 | connect(mNVim->api2(), &NeovimQt::NeovimApi2::neovimNotification, 123 | this, &QNVimCore::handleNotification); 124 | 125 | QVariantMap options; 126 | options.insert("ext_popupmenu", true); 127 | options.insert("ext_tabline", false); 128 | options.insert("ext_cmdline", true); 129 | options.insert("ext_wildmenu", true); 130 | options.insert("ext_messages", true); 131 | options.insert("ext_multigrid", true); 132 | options.insert("ext_hlstate", true); 133 | options.insert("rgb", true); 134 | NeovimQt::MsgpackRequest *request = mNVim->api2()->nvim_ui_attach(mWidth, mHeight, options); 135 | request->setTimeout(10000); 136 | connect(request, &NeovimQt::MsgpackRequest::timeout, mNVim, &NeovimQt::NeovimConnector::fatalTimeout); 137 | connect(request, &NeovimQt::MsgpackRequest::timeout, [=]() { 138 | qCritical(Main) << "Neovim: Connection timed out!"; 139 | }); 140 | connect(request, &NeovimQt::MsgpackRequest::finished, this, [=]() { 141 | qInfo(Main) << "Neovim: attached!"; 142 | 143 | auto pCurrentEditor = Core::EditorManager::currentEditor(); 144 | if (pCurrentEditor) 145 | QNVimCore::editorOpened(pCurrentEditor); 146 | }); 147 | 148 | mNVim->api2()->nvim_subscribe("Gui"); 149 | mNVim->api2()->nvim_subscribe("api-buffer-updates"); 150 | }); 151 | } 152 | 153 | QNVimCore::~QNVimCore() 154 | { 155 | qobject_cast(mCMDLine->parentWidget()->children()[2])->show(); 156 | mCMDLine->deleteLater(); 157 | 158 | disconnect(QApplication::styleHints(), &QStyleHints::cursorFlashTimeChanged, 159 | this, &QNVimCore::saveCursorFlashTime); 160 | QApplication::setCursorFlashTime(mSavedCursorFlashTime); 161 | 162 | mNumbersColumn->deleteLater(); 163 | auto request = mNVim->api2()->nvim_command("q!"); 164 | connect(request, &NeovimQt::MsgpackRequest::finished, this, [=]() { 165 | mNVim->deleteLater(); 166 | mNVim = nullptr; 167 | }); 168 | disconnect(Core::EditorManager::instance(), &Core::EditorManager::editorAboutToClose, 169 | this, &QNVimCore::editorAboutToClose); 170 | disconnect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged, 171 | this, &QNVimCore::editorOpened); 172 | const auto keys = mEditors.keys(); 173 | for (const auto key : keys) { 174 | Core::IEditor *editor = mEditors[key]; 175 | if (!editor) 176 | continue; 177 | 178 | QWidget *widget = editor->widget(); 179 | if (!widget) 180 | continue; 181 | 182 | if (!qobject_cast(widget)) 183 | continue; 184 | 185 | auto textEditor = qobject_cast(widget); 186 | textEditor->setCursorWidth(1); 187 | widget->removeEventFilter(this); 188 | mEditors.remove(key); 189 | } 190 | mBuffers.clear(); 191 | mChangedTicks.clear(); 192 | mBufferType.clear(); 193 | 194 | if (mNVim) 195 | mNVim->deleteLater(); 196 | 197 | if (mCMDLine) 198 | Core::StatusBarManager::destroyStatusBarWidget(mCMDLine); 199 | } 200 | 201 | QString QNVimCore::filename(Core::IEditor *editor) const { 202 | if (!editor) 203 | return QString(); 204 | 205 | auto filename = editor->document()->filePath().toString(); 206 | if (filename.isEmpty()) 207 | filename = editor->document()->displayName(); 208 | 209 | return filename; 210 | } 211 | 212 | void QNVimCore::fixSize(Core::IEditor *editor) { 213 | if (!editor) { 214 | return; 215 | } 216 | 217 | if (!mNVim or !mNVim->isReady()) 218 | return; 219 | 220 | auto textEditor = qobject_cast(editor->widget()); 221 | QFontMetricsF fm(textEditor->textDocument()->fontSettings().font()); 222 | 223 | // -1 is for the visual white spaces that Qt Creator adds (whether it renders them or not) 224 | // TODO: after ext_columns is implemented in neovim +6 should be removed 225 | const int width = qFloor(textEditor->viewport()->width() / fm.horizontalAdvance('A')) - 1 + 6; 226 | const int height = qFloor(textEditor->height() / fm.lineSpacing()); 227 | 228 | if (width != mWidth or height != mHeight) 229 | mNVim->api6()->nvim_ui_try_resize_grid(1, width, height); 230 | } 231 | 232 | void QNVimCore::syncCursorToVim(Core::IEditor *editor) { 233 | if (!editor) 234 | editor = Core::EditorManager::currentEditor(); 235 | 236 | if (!editor or !mBuffers.contains(editor)) 237 | return; 238 | 239 | auto textEditor = qobject_cast(editor->widget()); 240 | 241 | if (mMode == "v" or mMode == "V" or mMode == "\x16" or 242 | textEditor->textCursor().position() != textEditor->textCursor().anchor()) 243 | return; 244 | 245 | const auto text = textEditor->toPlainText(); 246 | int cursorPosition = textEditor->textCursor().position(); 247 | int line = QStringView(text).left(cursorPosition).count('\n') + 1; 248 | int col = text.left(cursorPosition).section('\n', -1).toUtf8().length() + 1; 249 | 250 | if (line == mCursor.y() and col == mCursor.x()) { 251 | return; 252 | } 253 | 254 | mCursor.setY(line); 255 | mCursor.setX(col); 256 | mNVim->api2()->nvim_command(QStringLiteral("buffer %1|call SetCursor(%2,%3)").arg(mBuffers[editor]).arg(line).arg(col).toUtf8()); 257 | } 258 | 259 | void QNVimCore::syncSelectionToVim(Core::IEditor *editor) { 260 | if (!editor) 261 | editor = Core::EditorManager::currentEditor(); 262 | 263 | if (!editor or !mBuffers.contains(editor)) 264 | return; 265 | 266 | auto textEditor = qobject_cast(editor->widget()); 267 | QString text = textEditor->toPlainText(); 268 | 269 | auto mtc = textEditor->multiTextCursor(); 270 | int line, col, vLine, vCol; 271 | 272 | QString visualCommand; 273 | if (mtc.hasMultipleCursors()) { 274 | auto mainCursor = mtc.mainCursor(); 275 | 276 | // We should always use main cursor pos here, 277 | // because it is the cursor user controls with hjkl 278 | auto nvimPos = mainCursor.position(); 279 | 280 | // NOTE: Theoretically, it is not always the case 281 | // that the main cursor is at the ends of mtc array, 282 | // but for creating our own block selections it works, 283 | // because we create cursors one after another, where 284 | // main cursor is at the end or in the beginning. 285 | // @see syncCursorFromVim 286 | auto lastCursor = mainCursor == *mtc.begin() ? *(mtc.end() - 1) : *mtc.begin(); 287 | auto nvimAnchor = lastCursor.anchor(); 288 | 289 | line = QStringView(text).left(nvimPos).count('\n') + 1; 290 | col = text.left(nvimPos).section('\n', -1).length() + 1; 291 | vLine = QStringView(text).left(nvimAnchor).count('\n') + 1; 292 | vCol = text.left(nvimAnchor).section('\n', -1).length() + 1; 293 | 294 | if (vCol < col) 295 | --col; 296 | else if (vCol > col) 297 | --vCol; 298 | 299 | visualCommand = "\x16"; 300 | } else if (mMode == "V") { 301 | return; 302 | } else { 303 | auto cursor = textEditor->textCursor(); 304 | int cursorPosition = cursor.position(); 305 | int anchorPosition = cursor.anchor(); 306 | 307 | if (anchorPosition == cursorPosition) 308 | return; 309 | 310 | if (anchorPosition < cursorPosition) 311 | --cursorPosition; 312 | else 313 | --anchorPosition; 314 | 315 | line = QStringView(text).left(cursorPosition).count('\n') + 1; 316 | col = text.left(cursorPosition).section('\n', -1).length() + 1; 317 | vLine = QStringView(text).left(anchorPosition).count('\n') + 1; 318 | vCol = text.left(anchorPosition).section('\n', -1).length() + 1; 319 | visualCommand = "v"; 320 | } 321 | 322 | if (line == mCursor.y() and col == mCursor.x() and vLine == mVCursor.y() and vCol == mVCursor.x()) 323 | return; 324 | 325 | mCursor.setY(line); 326 | mCursor.setX(col); 327 | mVCursor.setY(vLine); 328 | mVCursor.setX(vCol); 329 | mNVim->api2()->nvim_command(QStringLiteral("buffer %1|normal! \x03%3G%4|%2%5G%6|") 330 | .arg(mBuffers[editor]) 331 | .arg(visualCommand) 332 | .arg(vLine) 333 | .arg(vCol) 334 | .arg(line) 335 | .arg(col).toUtf8()); 336 | } 337 | 338 | void QNVimCore::syncCursorFromVim(const QVariantList &pos, const QVariantList &vPos, QByteArray mode) { 339 | auto editor = Core::EditorManager::currentEditor(); 340 | if (!editor or !mBuffers.contains(editor)) 341 | return; 342 | 343 | auto textEditor = qobject_cast(editor->widget()); 344 | int line = pos[0].toInt(); 345 | int col = pos[1].toInt(); 346 | col = QString::fromUtf8(mText.section('\n', line - 1, line - 1).toUtf8().left(col - 1)).length() + 1; 347 | 348 | int vLine = vPos[0].toInt(); 349 | int vCol = vPos[1].toInt(); 350 | vCol = QString::fromUtf8(mText.section('\n', vLine - 1, vLine - 1).toUtf8().left(vCol)).length(); 351 | 352 | mMode = mode; 353 | mCursor.setY(line); 354 | mCursor.setX(col); 355 | mVCursor.setY(vLine); 356 | mVCursor.setX(vCol); 357 | 358 | int anchor = QString("\n" + mText).section('\n', 0, vLine - 1).length() + vCol - 1; 359 | int position = QString("\n" + mText).section('\n', 0, line - 1).length() + col - 1; 360 | if (mMode == "V") { 361 | if (anchor < position) { 362 | anchor = QString("\n" + mText).section('\n', 0, vLine - 1).length(); 363 | position = QString("\n" + mText).section('\n', 0, line).length() - 1; 364 | } else { 365 | anchor = QString("\n" + mText).section('\n', 0, vLine).length() - 1; 366 | position = QString("\n" + mText).section('\n', 0, line - 1).length(); 367 | } 368 | 369 | QTextCursor cursor = textEditor->textCursor(); 370 | cursor.setPosition(anchor); 371 | cursor.setPosition(position, QTextCursor::KeepAnchor); 372 | 373 | if (textEditor->textCursor().anchor() != cursor.anchor() or 374 | textEditor->textCursor().position() != cursor.position()) 375 | textEditor->setTextCursor(cursor); 376 | 377 | } else if (mMode == "v") { 378 | if (anchor > position) 379 | ++anchor; 380 | else 381 | ++position; 382 | 383 | QTextCursor cursor = textEditor->textCursor(); 384 | cursor.setPosition(anchor); 385 | cursor.setPosition(position, QTextCursor::KeepAnchor); 386 | 387 | if (textEditor->textCursor().anchor() != cursor.anchor() or 388 | textEditor->textCursor().position() != cursor.position()) 389 | textEditor->setTextCursor(cursor); 390 | } else if (mMode == "\x16") { // VISUAL BLOCK 391 | if (vCol > col) 392 | ++anchor; 393 | else 394 | ++position; 395 | 396 | auto document = textEditor->textCursor().document(); 397 | const auto& tabs = textEditor->textDocument()->tabSettings(); 398 | 399 | const auto firstBlock = document->findBlock(anchor); 400 | const auto lastBlock = document->findBlock(position); 401 | const auto localAnchor = tabs.columnAt(firstBlock.text(), anchor - firstBlock.position()); 402 | const auto localPos = tabs.columnAt(lastBlock.text(), position - lastBlock.position()); 403 | 404 | // Get next block no matter the direction of selection 405 | auto after = [&](const auto& block) { 406 | if (anchor < position) 407 | return block.next(); 408 | else 409 | return block.previous(); 410 | }; 411 | 412 | auto mtc = Utils::MultiTextCursor(); 413 | for (auto curBlock = firstBlock; // 414 | curBlock.isValid() && curBlock != after(lastBlock); // 415 | curBlock = after(curBlock)) { 416 | 417 | auto columnsCountInCurBlock = tabs.columnCountForText(curBlock.text()); 418 | 419 | // Skip cursor, if it goes out of the block 420 | if (columnsCountInCurBlock < localAnchor && columnsCountInCurBlock < localPos) 421 | continue; 422 | 423 | auto newCursor = QTextCursor(curBlock); 424 | 425 | auto anchorBoundOffset = tabs.positionAtColumn(curBlock.text(), localAnchor); 426 | auto newCursorAnchor = curBlock.position() + anchorBoundOffset; 427 | newCursor.setPosition(newCursorAnchor); 428 | 429 | auto posBoundOffset = tabs.positionAtColumn(curBlock.text(), localPos); 430 | auto newCursorPosition = curBlock.position() + posBoundOffset; 431 | newCursor.setPosition(newCursorPosition, QTextCursor::KeepAnchor); 432 | 433 | mtc.addCursor(newCursor); 434 | } 435 | textEditor->setMultiTextCursor(mtc); 436 | } else { 437 | QTextCursor cursor = textEditor->textCursor(); 438 | cursor.clearSelection(); 439 | cursor.setPosition(position); 440 | 441 | if (textEditor->textCursor().position() != cursor.position() or 442 | textEditor->textCursor().hasSelection()) 443 | textEditor->setTextCursor(cursor); 444 | } 445 | } 446 | 447 | void QNVimCore::syncToVim(Core::IEditor *editor, std::function callback) { 448 | if (!editor) 449 | editor = Core::EditorManager::currentEditor(); 450 | 451 | if (!editor or !mBuffers.contains(editor)) 452 | return; 453 | 454 | auto textEditor = qobject_cast(editor->widget()); 455 | QString text = textEditor->toPlainText(); 456 | int cursorPosition = textEditor->textCursor().position(); 457 | int line = QStringView(text).left(cursorPosition).count('\n') + 1; 458 | int col = text.left(cursorPosition).section('\n', -1).toUtf8().length() + 1; 459 | 460 | if (mText != text) { 461 | int bufferNumber = mBuffers[editor]; 462 | auto request = mNVim->api2()->nvim_buf_set_lines(bufferNumber, 0, -1, true, text.toUtf8().split('\n')); 463 | connect(request, &NeovimQt::MsgpackRequest::finished, this, [=]() { 464 | connect(mNVim->api2()->nvim_command(QStringLiteral("call cursor(%1,%2)").arg(line).arg(col).toUtf8()), 465 | &NeovimQt::MsgpackRequest::finished, [=]() { 466 | if (callback) 467 | callback(); 468 | }); 469 | }); 470 | } else if (callback) 471 | callback(); 472 | } 473 | 474 | void QNVimCore::syncFromVim() { 475 | auto editor = Core::EditorManager::currentEditor(); 476 | 477 | if (!editor or !mBuffers.contains(editor)) 478 | return; 479 | 480 | auto textEditor = qobject_cast(editor->widget()); 481 | unsigned long long syncCoutner = ++mSyncCounter; 482 | 483 | auto request = mNVim->api2()->nvim_eval("[bufnr(''), b:changedtick, mode(1), &modified, getpos('.'), getpos('v'), &number, &relativenumber, &wrap]"); 484 | connect(request, &NeovimQt::MsgpackRequest::finished, this, [=](quint32, quint64, const QVariant &v) { 485 | QVariantList state = v.toList(); 486 | 487 | if (mSyncCounter != syncCoutner) 488 | return; 489 | 490 | if (!mBuffers.contains(editor)) { 491 | return; 492 | } 493 | 494 | int bufferNumber = mBuffers[editor]; 495 | if (state[0].toString().toLong() != bufferNumber) 496 | return; 497 | 498 | unsigned long long changedtick = state[1].toULongLong(); 499 | QByteArray mode = state[2].toByteArray(); 500 | bool modified = state[3].toBool(); 501 | QVariantList pos = state[4].toList().mid(1, 2); 502 | QVariantList vPos = state[5].toList().mid(1, 2); 503 | 504 | mNumber = state[6].toBool(); 505 | mRelativeNumber = state[7].toBool(); 506 | mWrap = state[8].toBool(); 507 | mNumbersColumn->setNumber(mNumber); 508 | mNumbersColumn->setEditor(mRelativeNumber ? textEditor : nullptr); 509 | 510 | if (textEditor->wordWrapMode() != (mWrap ? QTextOption::WrapAnywhere : QTextOption::NoWrap)) 511 | textEditor->setWordWrapMode(mWrap ? QTextOption::WrapAnywhere : QTextOption::NoWrap); 512 | 513 | if (mChangedTicks.value(bufferNumber, 0) == changedtick) { 514 | syncCursorFromVim(pos, vPos, mode); 515 | return; 516 | } 517 | 518 | mChangedTicks[bufferNumber] = changedtick; 519 | 520 | qDebug(Main) << "QNVimPlugin::syncFromVim"; 521 | 522 | auto request = mNVim->api2()->nvim_buf_get_lines(bufferNumber, 0, -1, true); 523 | connect(request, &NeovimQt::MsgpackRequest::finished, this, [=](quint32, quint64, const QVariant &lines) { 524 | if (!mBuffers.contains(editor)) { 525 | return; 526 | } 527 | 528 | mText.clear(); 529 | auto linesList = lines.toList(); 530 | for (const auto &t : linesList) 531 | mText += QString::fromUtf8(t.toByteArray()) + '\n'; 532 | mText.chop(1); 533 | 534 | QString oldText = textEditor->toPlainText(); 535 | 536 | Utils::Differ differ; 537 | auto diff = differ.diff(oldText, mText); 538 | 539 | if (diff.size()) { 540 | // Update changed lines and keep track of the cursor position 541 | QTextCursor cursor = textEditor->textCursor(); 542 | int charactersInfrontOfCursor = cursor.position(); 543 | int newCursorPos = charactersInfrontOfCursor; 544 | cursor.beginEditBlock(); 545 | cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 546 | 547 | for (const auto &d : diff) { 548 | switch (d.command) { 549 | case Utils::Diff::Insert: { 550 | // Adjust cursor position if we do work in front of the cursor. 551 | if (charactersInfrontOfCursor > 0) { 552 | const int size = d.text.size(); 553 | charactersInfrontOfCursor += size; 554 | newCursorPos += size; 555 | } 556 | cursor.insertText(d.text); 557 | break; 558 | } 559 | 560 | case Utils::Diff::Delete: { 561 | // Adjust cursor position if we do work in front of the cursor. 562 | if (charactersInfrontOfCursor > 0) { 563 | const int size = d.text.size(); 564 | charactersInfrontOfCursor -= size; 565 | newCursorPos -= size; 566 | // Cursor was inside the deleted text, so adjust the new cursor position 567 | if (charactersInfrontOfCursor < 0) 568 | newCursorPos -= charactersInfrontOfCursor; 569 | } 570 | cursor.setPosition(cursor.position() + d.text.length(), QTextCursor::KeepAnchor); 571 | cursor.removeSelectedText(); 572 | break; 573 | } 574 | 575 | case Utils::Diff::Equal: 576 | // Adjust cursor position 577 | charactersInfrontOfCursor -= d.text.size(); 578 | cursor.setPosition(cursor.position() + d.text.length(), QTextCursor::MoveAnchor); 579 | break; 580 | } 581 | } 582 | cursor.endEditBlock(); 583 | cursor.setPosition(newCursorPos); 584 | } 585 | 586 | if (textEditor->document()->isModified() != modified) 587 | textEditor->document()->setModified(modified); 588 | 589 | syncCursorFromVim(pos, vPos, mode); 590 | }); 591 | }); 592 | } 593 | 594 | void QNVimCore::triggerCommand(const QByteArray &commandId) { 595 | Core::ActionManager::command(commandId.constData())->action()->trigger(); 596 | } 597 | 598 | void QNVimCore::saveCursorFlashTime(int cursorFlashTime) { 599 | mSavedCursorFlashTime = cursorFlashTime; 600 | 601 | disconnect(QApplication::styleHints(), &QStyleHints::cursorFlashTimeChanged, 602 | this, &QNVimCore::saveCursorFlashTime); 603 | QApplication::setCursorFlashTime(0); 604 | connect(QApplication::styleHints(), &QStyleHints::cursorFlashTimeChanged, 605 | this, &QNVimCore::saveCursorFlashTime); 606 | } 607 | 608 | bool QNVimCore::eventFilter(QObject *object, QEvent *event) { 609 | /* if (qobject_cast(object)) */ 610 | if (qobject_cast(object) || 611 | qobject_cast(object)) { 612 | if (event->type() == QEvent::Resize) { 613 | QTimer::singleShot(100, this, [=]() { fixSize(); }); 614 | return false; 615 | } 616 | } 617 | 618 | if (event->type() == QEvent::KeyPress) { 619 | QKeyEvent *keyEvent = static_cast(event); 620 | QString key = NeovimQt::Input::convertKey(*keyEvent); 621 | mNVim->api2()->nvim_input(key.toUtf8()); 622 | return true; 623 | } else if (event->type() == QEvent::ShortcutOverride) { 624 | QKeyEvent *keyEvent = static_cast(event); 625 | QString key = NeovimQt::Input::convertKey(*keyEvent); 626 | if (keyEvent->key() == Qt::Key_Escape) { 627 | mNVim->api2()->nvim_input(key.toUtf8()); 628 | } else { 629 | keyEvent->accept(); 630 | } 631 | return true; 632 | } 633 | return false; 634 | } 635 | 636 | void QNVimCore::editorOpened(Core::IEditor *editor) { 637 | if (!mEnabled) 638 | return; 639 | 640 | if (!editor) 641 | return; 642 | 643 | QString filename(this->filename(editor)); 644 | mText.clear(); 645 | qDebug(Main) << "Opened " << filename << mSettingBufferFromVim; 646 | 647 | QWidget *widget = editor->widget(); 648 | if (!widget) 649 | return; 650 | 651 | auto project = ProjectExplorer::SessionManager::projectForFile( 652 | Utils::FilePath::fromString(filename)); 653 | qDebug(Main) << project; 654 | if (project) { 655 | QString projectDirectory = project->projectDirectory().toString(); 656 | if (!projectDirectory.isEmpty()) 657 | mNVim->api2()->nvim_command(QStringLiteral("cd %1").arg(projectDirectory).toUtf8()); 658 | } 659 | 660 | if (!qobject_cast(widget)) { 661 | mNumbersColumn->setEditor(nullptr); 662 | return; 663 | } 664 | auto textEditor = qobject_cast(editor->widget()); 665 | 666 | if (mBuffers.contains(editor)) { 667 | if (!mSettingBufferFromVim) 668 | mNVim->api2()->nvim_command(QStringLiteral("buffer %1").arg(mBuffers[editor]).toUtf8()); 669 | } else { 670 | if (mNVim and mNVim->isReady()) { 671 | if (mSettingBufferFromVim > 0) { 672 | mBuffers[editor] = mSettingBufferFromVim; 673 | mEditors[mSettingBufferFromVim] = editor; 674 | initializeBuffer(mSettingBufferFromVim); 675 | } else { 676 | QString f = filename; 677 | if (f.contains('\\') or f.contains('\'') or f.contains('"') or f.contains(' ')) { 678 | static const auto regExp = QRegularExpression("[\\\"' ]"); 679 | f = '"' + f.replace(regExp, "\\\1") + '"'; 680 | } 681 | 682 | auto request = mNVim->api2()->nvim_command(QStringLiteral("e %1").arg(f).toUtf8()); 683 | connect(request, &NeovimQt::MsgpackRequest::finished, this, [=]() { 684 | auto request = mNVim->api2()->nvim_eval(QStringLiteral("bufnr('')").toUtf8()); 685 | connect(request, &NeovimQt::MsgpackRequest::finished, this, [=](quint32, quint64, const QVariant &v) { 686 | mBuffers[editor] = v.toInt(); 687 | mEditors[v.toInt()] = editor; 688 | initializeBuffer(v.toInt()); 689 | }); 690 | }); 691 | } 692 | } 693 | 694 | Core::IDocument *document = editor->document(); 695 | 696 | connect(document, &Core::IDocument::contentsChanged, this, [=]() { 697 | auto buffer = mBuffers[editor]; 698 | QString bufferType = mBufferType[buffer]; 699 | if (!mEditors.contains(buffer) or (bufferType != "acwrite" and !bufferType.isEmpty())) 700 | return; 701 | syncToVim(editor); 702 | }, 703 | Qt::QueuedConnection); 704 | connect(textEditor, &TextEditor::TextEditorWidget::cursorPositionChanged, this, [=]() { 705 | if (Core::EditorManager::currentEditor() != editor) 706 | return; 707 | QString newText = textEditor->toPlainText(); 708 | if (newText != mText) 709 | return; 710 | syncCursorToVim(editor); 711 | }, 712 | Qt::QueuedConnection); 713 | connect(textEditor, &TextEditor::TextEditorWidget::selectionChanged, this, [=]() { 714 | if (Core::EditorManager::currentEditor() != editor) 715 | return; 716 | QString newText = textEditor->toPlainText(); 717 | if (newText != mText) 718 | return; 719 | syncSelectionToVim(editor); 720 | }, 721 | Qt::QueuedConnection); 722 | connect(textEditor->textDocument(), &TextEditor::TextDocument::fontSettingsChanged, 723 | this, &QNVimCore::updateCursorSize); 724 | } 725 | mSettingBufferFromVim = 0; 726 | 727 | mNumbersColumn->setEditor(textEditor); 728 | 729 | widget->setAttribute(Qt::WA_KeyCompression, false); 730 | widget->installEventFilter(this); 731 | 732 | QTimer::singleShot(100, this, [=]() { fixSize(editor); }); 733 | } 734 | 735 | void QNVimCore::editorAboutToClose(Core::IEditor *editor) { 736 | qDebug(Main) << "QNVimPlugin::editorAboutToClose"; 737 | if (!mBuffers.contains(editor)) 738 | return; 739 | 740 | if (Core::EditorManager::currentEditor() == editor) 741 | mNumbersColumn->setEditor(nullptr); 742 | 743 | int bufferNumber = mBuffers[editor]; 744 | mNVim->api2()->nvim_command(QStringLiteral("bd! %1").arg(mBuffers[editor]).toUtf8()); 745 | mBuffers.remove(editor); 746 | mEditors.remove(bufferNumber); 747 | mChangedTicks.remove(bufferNumber); 748 | mBufferType.remove(bufferNumber); 749 | } 750 | 751 | void QNVimCore::initializeBuffer(int buffer) { 752 | QString bufferType = mBufferType[buffer]; 753 | if (bufferType == "acwrite" or bufferType.isEmpty()) { 754 | connect( 755 | mNVim->api2()->nvim_buf_set_option(buffer, "undolevels", -1), 756 | &NeovimQt::MsgpackRequest::finished, this, [=]() { 757 | syncToVim(mEditors[buffer], [=]() { 758 | mNVim->api2()->nvim_buf_set_option(buffer, "undolevels", -123456); 759 | mNVim->api2()->nvim_buf_set_option(buffer, "modified", false); 760 | if (bufferType.isEmpty() && QFile::exists(filename(mEditors[buffer]))) 761 | mNVim->api2()->nvim_buf_set_option(buffer, "buftype", "acwrite"); 762 | }); 763 | }, 764 | Qt::DirectConnection); 765 | } else { 766 | mNVim->api2()->nvim_buf_set_option(buffer, "modified", false); 767 | syncFromVim(); 768 | } 769 | } 770 | 771 | void QNVimCore::handleNotification(const QByteArray &name, const QVariantList &args) { 772 | auto editor = Core::EditorManager::currentEditor(); 773 | 774 | if (!editor or !mBuffers.contains(editor)) 775 | return; 776 | 777 | if (name == "Gui") { 778 | QByteArray method = args.first().toByteArray(); 779 | QVariantList methodArgs = args.mid(1); 780 | if (method == "triggerCommand") { 781 | for (const auto& methodArg : methodArgs) 782 | triggerCommand(methodArg.toByteArray()); 783 | } else if (method == "fileAutoCommand") { 784 | QByteArray cmd = methodArgs.first().toByteArray(); 785 | int buffer = methodArgs[1].toByteArray().toInt(); 786 | QString filename = QString::fromUtf8(methodArgs[2].toByteArray()); 787 | QString bufferType = QString::fromUtf8(methodArgs[3].toByteArray()); 788 | bool bufferListed = methodArgs[4].toInt(); 789 | QString bufferHidden = QString::fromUtf8(methodArgs[5].toByteArray()); 790 | bool alwaysText = methodArgs[6].toInt(); 791 | 792 | if (cmd == "BufReadCmd" or cmd == "TermOpen") { 793 | mBufferType[buffer] = bufferType; 794 | if (mEditors.contains(buffer)) { 795 | mText.clear(); 796 | initializeBuffer(buffer); 797 | } else { 798 | if (cmd == "TermOpen") 799 | mNVim->api2()->nvim_command("doautocmd BufEnter"); 800 | } 801 | } else if (cmd == "BufWriteCmd") { 802 | if (mEditors.contains(buffer)) { 803 | QString currentFilename = this->filename(mEditors[buffer]); 804 | if (mEditors[buffer]->document()->save(nullptr, Utils::FilePath::fromString(filename))) { 805 | if (currentFilename != filename) { 806 | mEditors.remove(buffer); 807 | mChangedTicks.remove(buffer); 808 | mBuffers.remove(editor); 809 | 810 | auto request = mNVim->api2()->nvim_buf_set_name(buffer, filename.toUtf8()); 811 | connect(request, &NeovimQt::MsgpackRequest::finished, this, [=](quint32, quint64, const QVariant &) { 812 | mNVim->api2()->nvim_command("edit!"); 813 | }); 814 | } else { 815 | mNVim->api2()->nvim_buf_set_option(buffer, "modified", false); 816 | } 817 | } else { 818 | mNVim->api2()->nvim_buf_set_option(buffer, "modified", true); 819 | } 820 | } 821 | } else if (cmd == "BufEnter") { 822 | mBufferType[buffer] = bufferType; 823 | [[maybe_unused]] Core::IEditor *e = nullptr; 824 | mSettingBufferFromVim = buffer; 825 | if (!filename.isEmpty() and filename != this->filename(editor)) { 826 | if (mEditors.contains(buffer)) { 827 | if (editor != mEditors[buffer]) { 828 | Core::EditorManager::activateEditor( 829 | mEditors[buffer]); 830 | e = mEditors[buffer]; 831 | } 832 | } else { 833 | if (bufferType.isEmpty() && QFile::exists(filename)) { 834 | QFileInfo fileInfo = QFileInfo(filename); 835 | if (alwaysText) { 836 | if ((QStringList() << "js" 837 | << "qml" 838 | << "cpp" 839 | << "c" 840 | << "cc" 841 | << "hpp" 842 | << "h" 843 | << "pro") 844 | .contains(fileInfo.suffix(), Qt::CaseInsensitive)) 845 | e = Core::EditorManager::openEditor(Utils::FilePath::fromString(filename)); 846 | else 847 | e = Core::EditorManager::openEditor(Utils::FilePath::fromString(filename), "Core.PlainTextEditor"); 848 | } else { 849 | e = Core::EditorManager::openEditor(Utils::FilePath::fromString(filename)); 850 | } 851 | } else { 852 | qDebug(Main) << 123; 853 | if (bufferType == "terminal") { 854 | e = Core::EditorManager::openEditorWithContents("Terminal", &filename, QByteArray(), filename); 855 | } else if (bufferType == "help") { 856 | e = Core::EditorManager::openEditorWithContents("Help", &filename, QByteArray(), filename); 857 | e->document()->setFilePath(Utils::FilePath::fromString(filename)); 858 | e->document()->setPreferredDisplayName(filename); 859 | } else { 860 | e = Core::EditorManager::openEditorWithContents("Terminal", &filename, QByteArray(), filename); 861 | } 862 | } 863 | } 864 | } 865 | mSettingBufferFromVim = 0; 866 | // if (filename.isEmpty()) { 867 | // // callback(); 868 | // } else { 869 | // connect(mNVim->api2()->nvim_command("try | silent only! | catch | endtry"), &NeovimQt::MsgpackRequest::finished, callback); 870 | // } 871 | } else if (cmd == "BufDelete") { 872 | if (bufferListed and mEditors.contains(buffer) and mEditors[buffer]) { 873 | if (Core::EditorManager::currentEditor() == mEditors[buffer]) 874 | mSettingBufferFromVim = -1; 875 | Core::EditorManager::closeEditors({mEditors[buffer]}); 876 | } 877 | } else if (cmd == "BufHidden") { 878 | if ( 879 | (bufferHidden == "wipe" or bufferHidden == "delete" or bufferHidden == "unload" or bufferType == "help") and 880 | mEditors.contains(buffer) and mEditors[buffer]) { 881 | if (Core::EditorManager::currentEditor() == mEditors[buffer]) 882 | mSettingBufferFromVim = -1; 883 | Core::EditorManager::closeEditors({mEditors[buffer]}); 884 | } 885 | } else if (cmd == "BufWipeout") { 886 | if (!bufferListed and mEditors.contains(buffer) and mEditors[buffer]) { 887 | if (Core::EditorManager::currentEditor() == mEditors[buffer]) 888 | mSettingBufferFromVim = -1; 889 | Core::EditorManager::closeEditors({mEditors[buffer]}); 890 | } 891 | } 892 | } 893 | } else if (name == "redraw") 894 | redraw(args); 895 | } 896 | 897 | void QNVimCore::redraw(const QVariantList &args) { 898 | auto editor = Core::EditorManager::currentEditor(); 899 | auto textEditor = qobject_cast(editor->widget()); 900 | bool shouldSync = false; 901 | bool flush = false; 902 | 903 | for (const auto& arg : args) { 904 | QVariantList line = arg.toList(); 905 | QByteArray command = line.first().toByteArray(); 906 | QVariantList args = line.mid(1).constFirst().toList(); 907 | 908 | if (!command.startsWith("msg") and 909 | !command.startsWith("cmdline") and command != "flush") 910 | shouldSync = true; 911 | 912 | if (command == "flush") 913 | flush = true; 914 | 915 | if (command == "win_pos") { 916 | qDebug(Main) << line; 917 | } else if (command == "win_float_pos") { 918 | qDebug(Main) << line; 919 | } else if (command == "win_hide") { 920 | qDebug(Main) << line; 921 | } else if (command == "win_close") { 922 | qDebug(Main) << line; 923 | } else if (command == "bell") { 924 | QApplication::beep(); 925 | } else if (command == "mode_change") { 926 | mUIMode = args.first().toByteArray(); 927 | } else if (command == "busy_start") { 928 | mBusy = true; 929 | } else if (command == "busy_stop") { 930 | mBusy = false; 931 | } else if (command == "mouse_on") { 932 | mMouse = true; 933 | } else if (command == "mouse_off") { 934 | mMouse = false; 935 | } else if (command == "grid_resize") { 936 | if (line.first().toInt() == 1) { 937 | mWidth = args[0].toInt(); 938 | mHeight = args[1].toInt(); 939 | } 940 | } else if (command == "default_colors_set") { 941 | qint64 val = args[0].toLongLong(); 942 | if (val != -1) { 943 | mForegroundColor = QRgb(val); 944 | QPalette palette = textEditor->palette(); 945 | palette.setColor(QPalette::WindowText, mForegroundColor); 946 | textEditor->setPalette(palette); 947 | } 948 | 949 | val = args[1].toLongLong(); 950 | if (val != -1) { 951 | mBackgroundColor = QRgb(val); 952 | QPalette palette = textEditor->palette(); 953 | palette.setBrush(QPalette::Window, mBackgroundColor); 954 | textEditor->setPalette(palette); 955 | } 956 | 957 | val = args[2].toLongLong(); 958 | if (val != -1) { 959 | mSpecialColor = QRgb(val); 960 | } 961 | } else if (command == "cmdline_show") { 962 | mCMDLineVisible = true; 963 | QVariantList contentList = args[0].toList(); 964 | mCMDLineContent.clear(); 965 | 966 | for (const auto& contentItem : contentList) 967 | mCMDLineContent += QString::fromUtf8(contentItem.toList()[1].toByteArray()); 968 | 969 | mCMDLinePos = args[1].toInt(); 970 | mCMDLineFirstc = args[2].toString()[0]; 971 | mCMDLinePrompt = QString::fromUtf8(args[3].toByteArray()); 972 | mCMDLineIndent = args[4].toInt(); 973 | } else if (command == "cmdline_pos") { 974 | mCMDLinePos = args[0].toInt(); 975 | } else if (command == "cmdline_hide") { 976 | mCMDLineVisible = false; 977 | } else if (command == "msg_show") { 978 | QVariantList contentList = args[1].toList(); 979 | mMessageLineDisplay.clear(); 980 | for (const auto& contentItem : contentList) 981 | mMessageLineDisplay += QString::fromUtf8(contentItem.toList()[1].toByteArray()); 982 | } else if (command == "msg_clear") { 983 | mMessageLineDisplay.clear(); 984 | } else if (command == "msg_history_show") { 985 | QVariantList entries = args[1].toList(); 986 | mMessageLineDisplay.clear(); 987 | 988 | for (const auto& entry : entries) { 989 | QVariantList contentList = entry.toList()[1].toList(); 990 | 991 | for (const auto& contentItem : contentList) 992 | mMessageLineDisplay += QString::fromUtf8(contentItem.toList()[1].toByteArray()) + '\n'; 993 | } 994 | } 995 | } 996 | 997 | if (shouldSync and flush) 998 | syncFromVim(); 999 | 1000 | updateCursorSize(); 1001 | 1002 | QFontMetrics commandLineFontMetric(mCMDLine->font()); 1003 | if (mCMDLineVisible) { 1004 | QString text = mCMDLineFirstc + mCMDLinePrompt + QString(mCMDLineIndent, ' ') + mCMDLineContent; 1005 | 1006 | if (mCMDLine->toPlainText() != text) 1007 | mCMDLine->setPlainText(text); 1008 | 1009 | static const auto endLineRegExp = QRegularExpression("[\n\r]"); 1010 | 1011 | const auto height = (text.count(endLineRegExp) + 1) * commandLineFontMetric.height(); 1012 | auto width = 0; 1013 | 1014 | const auto lines = text.split(endLineRegExp); 1015 | for (const auto& line : lines) { 1016 | width += commandLineFontMetric.horizontalAdvance(line); 1017 | } 1018 | 1019 | if (mCMDLine->minimumWidth() != qMax(200, qMin(width + 10, 400))) 1020 | mCMDLine->setMinimumWidth(qMax(200, qMin(width + 10, 400))); 1021 | 1022 | if (mCMDLine->minimumHeight() != qMax(25, qMin(height + 4, 400))) { 1023 | mCMDLine->setMinimumHeight(qMax(25, qMin(height + 4, 400))); 1024 | mCMDLine->parentWidget()->setFixedHeight(qMax(25, qMin(height + 4, 400))); 1025 | mCMDLine->parentWidget()->parentWidget()->setFixedHeight(qMax(25, qMin(height + 4, 400))); 1026 | mCMDLine->parentWidget()->parentWidget()->parentWidget()->setFixedHeight(qMax(25, qMin(height + 4, 400))); 1027 | } 1028 | 1029 | if (!mCMDLine->hasFocus()) 1030 | mCMDLine->setFocus(); 1031 | 1032 | QTextCursor cursor = mCMDLine->textCursor(); 1033 | if (cursor.position() != (QString(mCMDLineFirstc + mCMDLinePrompt).length() + mCMDLineIndent + mCMDLinePos)) { 1034 | cursor.setPosition(QString(mCMDLineFirstc + mCMDLinePrompt).length() + mCMDLineIndent + mCMDLinePos); 1035 | mCMDLine->setTextCursor(cursor); 1036 | } 1037 | 1038 | if (mUIMode == "cmdline_normal") { 1039 | if (mCMDLine->cursorWidth() != 1) 1040 | mCMDLine->setCursorWidth(1); 1041 | } else if (mUIMode == "cmdline_insert") { 1042 | if (mCMDLine->cursorWidth() != 11) 1043 | mCMDLine->setCursorWidth(11); 1044 | } 1045 | } else { 1046 | mCMDLine->setPlainText(mMessageLineDisplay); 1047 | 1048 | if (mCMDLine->hasFocus()) 1049 | textEditor->setFocus(); 1050 | 1051 | auto height = commandLineFontMetric.height(); 1052 | mCMDLine->setMinimumHeight(qMax(25, qMin(height + 4, 400))); 1053 | mCMDLine->parentWidget()->setFixedHeight(qMax(25, qMin(height + 4, 400))); 1054 | mCMDLine->parentWidget()->parentWidget()->setFixedHeight(qMax(25, qMin(height + 4, 400))); 1055 | mCMDLine->parentWidget()->parentWidget()->parentWidget()->setFixedHeight(qMax(25, qMin(height + 4, 400))); 1056 | } 1057 | 1058 | mCMDLine->setToolTip(mCMDLine->toPlainText()); 1059 | } 1060 | 1061 | void QNVimCore::updateCursorSize() { 1062 | auto editor = Core::EditorManager::currentEditor(); 1063 | auto textEditor = qobject_cast(editor->widget()); 1064 | QFontMetricsF textEditorFontMetric(textEditor->textDocument()->fontSettings().font()); 1065 | 1066 | if (mBusy) { 1067 | textEditor->setCursorWidth(0); 1068 | } else if (mUIMode == "insert" or mUIMode == "visual") { 1069 | textEditor->setCursorWidth(1); 1070 | } else if (mUIMode == "normal" or mUIMode == "operator") { 1071 | textEditor->setCursorWidth(static_cast(textEditorFontMetric.horizontalAdvance('A') * textEditor->textDocument()->fontSettings().fontZoom() / 100)); 1072 | } 1073 | 1074 | mNumbersColumn->updateGeometry(); 1075 | } 1076 | 1077 | } // namespace Internal 1078 | } // namespace QNVim 1079 | -------------------------------------------------------------------------------- /src/qnvimcore.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | // SPDX-FileCopyrightText: 2023 Mikhail Zolotukhin 3 | // SPDX-License-Identifier: MIT 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | QT_BEGIN_NAMESPACE 12 | class QPlainTextEdit; 13 | QT_END_NAMESPACE 14 | 15 | namespace Core { 16 | class IEditor; 17 | } 18 | 19 | namespace ProjectExplorer { 20 | class Project; 21 | } 22 | 23 | namespace NeovimQt { 24 | class NeovimConnector; 25 | } 26 | 27 | namespace QNVim { 28 | namespace Internal { 29 | 30 | class NumbersColumn; 31 | 32 | /** 33 | * Encapsulates plugin's behavior with an assumption, that it is enabled. 34 | */ 35 | class QNVimCore : public QObject { 36 | Q_OBJECT 37 | public: 38 | explicit QNVimCore(QObject *parent = nullptr); 39 | virtual ~QNVimCore(); 40 | 41 | bool eventFilter(QObject *object, QEvent *event) override; 42 | 43 | protected: 44 | QString filename(Core::IEditor * = nullptr) const; 45 | 46 | void fixSize(Core::IEditor * = nullptr); 47 | void syncCursorToVim(Core::IEditor * = nullptr); 48 | void syncSelectionToVim(Core::IEditor * = nullptr); 49 | void syncModifiedToVim(Core::IEditor * = nullptr); 50 | void syncToVim(Core::IEditor * = nullptr, std::function = nullptr); 51 | void syncCursorFromVim(const QVariantList &, const QVariantList &, QByteArray mode); 52 | void syncFromVim(); 53 | 54 | void triggerCommand(const QByteArray &); 55 | 56 | private slots: 57 | // Save cursor flash time to variable instead of changing real value 58 | void saveCursorFlashTime(int cursorFlashTime); 59 | 60 | private: 61 | void editorOpened(Core::IEditor *); 62 | void editorAboutToClose(Core::IEditor *); 63 | 64 | void initializeBuffer(int); 65 | void handleNotification(const QByteArray &, const QVariantList &); 66 | void redraw(const QVariantList &); 67 | void updateCursorSize(); 68 | 69 | bool mEnabled = true; 70 | 71 | QPlainTextEdit *mCMDLine = nullptr; 72 | NumbersColumn *mNumbersColumn = nullptr; 73 | NeovimQt::NeovimConnector *mNVim = nullptr; 74 | unsigned mVimChanges = 0; 75 | QMap mBuffers; 76 | QMap mEditors; 77 | QMap mChangedTicks; 78 | QMap mBufferType; 79 | 80 | QString mText; 81 | int mWidth = 80; 82 | int mHeight = 35; 83 | QColor mForegroundColor = Qt::black; 84 | QColor mBackgroundColor = Qt::white; 85 | QColor mSpecialColor; 86 | QColor mCursorColor = Qt::white; 87 | bool mBusy = false; 88 | bool mMouse = false; 89 | bool mNumber = true; 90 | bool mRelativeNumber = true; 91 | bool mWrap = false; 92 | 93 | bool mCMDLineVisible = false; 94 | QString mCMDLineContent; 95 | QString mCMDLineDisplay; 96 | QString mMessageLineDisplay; 97 | int mCMDLinePos; 98 | QChar mCMDLineFirstc; 99 | QString mCMDLinePrompt; 100 | int mCMDLineIndent; 101 | 102 | QByteArray mUIMode = "normal"; 103 | QByteArray mMode = "n"; 104 | QPoint mCursor; 105 | QPoint mVCursor; 106 | 107 | int mSettingBufferFromVim = 0; 108 | unsigned long long mSyncCounter = 0; 109 | 110 | int mSavedCursorFlashTime = 0; 111 | 112 | signals: 113 | }; 114 | 115 | } // namespace Internal 116 | } // namespace QNVim 117 | -------------------------------------------------------------------------------- /src/qnvimplugin.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | // SPDX-FileCopyrightText: 2023 Mikhail Zolotukhin 3 | // SPDX-License-Identifier: MIT 4 | 5 | #include "qnvimplugin.h" 6 | 7 | #include "qnvimcore.h" 8 | #include "log.h" 9 | #include "qnvimconstants.h" 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | namespace QNVim { 18 | namespace Internal { 19 | 20 | bool QNVimPlugin::initialize(const QStringList &arguments, QString *errorString) { 21 | Q_UNUSED(arguments) 22 | Q_UNUSED(errorString) 23 | 24 | new HelpEditorFactory(); 25 | new TerminalEditorFactory(); 26 | 27 | auto action = new QAction(tr("Toggle QNVim"), this); 28 | Core::Command *cmd = Core::ActionManager::registerAction(action, Constants::TOGGLE_ID, 29 | Core::Context(Core::Constants::C_GLOBAL)); 30 | cmd->setDefaultKeySequence(QKeySequence(tr("Alt+Shift+V,Alt+Shift+V"))); 31 | connect(action, &QAction::triggered, this, &QNVimPlugin::toggleQNVim); 32 | 33 | Core::ActionContainer *menu = Core::ActionManager::createMenu(Constants::MENU_ID); 34 | menu->menu()->setTitle(tr("QNVim")); 35 | menu->addAction(cmd); 36 | Core::ActionManager::actionContainer(Core::Constants::M_TOOLS)->addMenu(menu); 37 | 38 | qunsetenv("NVIM_LISTEN_ADDRESS"); 39 | 40 | m_core = std::make_unique(); 41 | 42 | return true; 43 | } 44 | 45 | void QNVimPlugin::extensionsInitialized() { 46 | // Retrieve objects from the plugin manager's object pool 47 | // In the extensionsInitialized function, a plugin can be sure that all 48 | // plugins that depend on it are completely initialized. 49 | } 50 | 51 | ExtensionSystem::IPlugin::ShutdownFlag QNVimPlugin::aboutToShutdown() { 52 | m_core = nullptr; 53 | // Save settings 54 | // Disconnect from signals that are not needed during shutdown 55 | // Hide UI (if you add UI that is not in the main window directly) 56 | return SynchronousShutdown; 57 | } 58 | 59 | bool QNVimPlugin::eventFilter(QObject *object, QEvent *event) { 60 | if (m_core) 61 | return m_core->eventFilter(object, event); 62 | else 63 | return false; 64 | } 65 | 66 | void QNVimPlugin::toggleQNVim() { 67 | qDebug(Main) << "QNVimPlugin::toggleQNVim"; 68 | 69 | if (m_core) 70 | m_core = nullptr; 71 | else 72 | m_core = std::make_unique(); 73 | } 74 | 75 | HelpEditorFactory::HelpEditorFactory() : PlainTextEditorFactory() { 76 | setId("Help"); 77 | setDisplayName("Help"); 78 | addMimeType("text/plain"); 79 | } 80 | 81 | TerminalEditorFactory::TerminalEditorFactory() : PlainTextEditorFactory() { 82 | setId("Terminal"); 83 | setDisplayName("Terminal"); 84 | addMimeType("text/plain"); 85 | } 86 | 87 | } // namespace Internal 88 | } // namespace QNVim 89 | -------------------------------------------------------------------------------- /src/qnvimplugin.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Sassan Haradji 2 | // SPDX-License-Identifier: MIT 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | namespace QNVim { 10 | namespace Internal { 11 | 12 | class QNVimCore; 13 | class NumbersColumn; 14 | 15 | class QNVimPlugin : public ExtensionSystem::IPlugin { 16 | Q_OBJECT 17 | Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "QNVim.json") 18 | 19 | public: 20 | QNVimPlugin() = default; 21 | 22 | bool initialize(const QStringList &, QString *) override; 23 | void extensionsInitialized() override; 24 | ShutdownFlag aboutToShutdown() override; 25 | 26 | bool eventFilter(QObject *, QEvent *) override; 27 | 28 | void toggleQNVim(); 29 | 30 | private: 31 | std::unique_ptr m_core; 32 | }; 33 | 34 | class HelpEditorFactory : public TextEditor::PlainTextEditorFactory { 35 | Q_OBJECT 36 | 37 | public: 38 | explicit HelpEditorFactory(); 39 | }; 40 | 41 | class TerminalEditorFactory : public TextEditor::PlainTextEditorFactory { 42 | Q_OBJECT 43 | 44 | public: 45 | explicit TerminalEditorFactory(); 46 | }; 47 | 48 | } // namespace Internal 49 | } // namespace QNVim 50 | -------------------------------------------------------------------------------- /tools/DownloadQtCreator.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | set(QTC_EXT_DIR "${CMAKE_CURRENT_LIST_DIR}/../external/qtcreator") 5 | 6 | # Fetch Qt Creator Version 7 | include("${QTC_EXT_DIR}/version.cmake") 8 | 9 | # Notify CI about Qt Creator version 10 | file(APPEND $ENV{GITHUB_OUTPUT} "qtc_ver=${QT_CREATOR_VERSION}") 11 | 12 | string(REGEX MATCH "([0-9]+.[0-9]+).[0-9]+" outvar "${QT_CREATOR_VERSION}") 13 | 14 | set(qtc_base_url "https://download.qt.io/official_releases/qtcreator/\ 15 | ${CMAKE_MATCH_1}/${QT_CREATOR_VERSION}/installer_source") 16 | 17 | if (QT_CREATOR_SNAPSHOT) 18 | set(qtc_base_url "https://download.qt.io/snapshots/qtcreator/\ 19 | ${CMAKE_MATCH_1}/${QT_CREATOR_VERSION}/installer_source/${QT_CREATOR_SNAPSHOT}") 20 | endif() 21 | 22 | if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") 23 | set(qtc_platform "windows_x64") 24 | elseif ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Linux") 25 | set(qtc_platform "linux_x64") 26 | elseif ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Darwin") 27 | set(qtc_platform "mac_x64") 28 | endif() 29 | 30 | set(QTC_DIST_DIR "${QTC_EXT_DIR}/dist-${CMAKE_HOST_SYSTEM_NAME}-${QT_CREATOR_VERSION}") 31 | 32 | file(MAKE_DIRECTORY "${QTC_DIST_DIR}") 33 | 34 | message(STATUS "Downloading Qt Creator from ${qtc_base_url}/${qtc_platform}...") 35 | 36 | foreach(package qtcreator qtcreator_dev) 37 | message(STATUS "Downloading ${package}...") 38 | file(DOWNLOAD 39 | "${qtc_base_url}/${qtc_platform}/${package}.7z" 40 | "${CMAKE_CURRENT_BINARY_DIR}/${package}.7z" 41 | ) 42 | message(STATUS "Extracting ${package}...") 43 | execute_process( 44 | COMMAND ${CMAKE_COMMAND} -E tar xf "${CMAKE_CURRENT_BINARY_DIR}/${package}.7z" 45 | WORKING_DIRECTORY "${QTC_DIST_DIR}" 46 | ) 47 | endforeach() 48 | -------------------------------------------------------------------------------- /tools/ci/DownloadNinjaAndCMake.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | set(cmake_version "$ENV{CMAKE_VERSION}") 5 | set(ninja_version "$ENV{NINJA_VERSION}") 6 | 7 | if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") 8 | set(ninja_suffix "win.zip") 9 | set(cmake_suffix "win64-x64.zip") 10 | set(cmake_dir "cmake-${cmake_version}-win64-x64/bin") 11 | elseif ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Linux") 12 | set(ninja_suffix "linux.zip") 13 | set(cmake_suffix "Linux-x86_64.tar.gz") 14 | set(cmake_dir "cmake-${cmake_version}-Linux-x86_64/bin") 15 | elseif ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Darwin") 16 | set(ninja_suffix "mac.zip") 17 | set(cmake_suffix "Darwin-x86_64.tar.gz") 18 | set(cmake_dir "cmake-${cmake_version}-Darwin-x86_64/CMake.app/Contents/bin") 19 | endif() 20 | 21 | set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}") 22 | file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS) 23 | execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip) 24 | 25 | set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}") 26 | file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS) 27 | execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip) 28 | 29 | # Add to PATH environment variable 30 | file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir) 31 | set(path_separator ":") 32 | if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") 33 | set(path_separator ";") 34 | endif() 35 | file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}") 36 | 37 | if (NOT "${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") 38 | execute_process( 39 | COMMAND chmod +x ninja 40 | COMMAND chmod +x ${cmake_dir}/cmake 41 | ) 42 | endif() 43 | -------------------------------------------------------------------------------- /tools/ci/DownloadQt.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | set(qt_version "$ENV{QT_VERSION}") 5 | 6 | string(REGEX MATCH "^[0-9]+" qt_version_major "${qt_version}") 7 | string(REPLACE "." "" qt_version_dotless "${qt_version}") 8 | if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") 9 | set(url_os "windows_x86") 10 | set(qt_package_arch_suffix "win64_msvc2019_64") 11 | set(qt_dir_prefix "${qt_version}/msvc2019_64") 12 | if("${qt_version_major}" STREQUAL "5") 13 | set(qt_package_suffix "-Windows-Windows_10-MSVC2019-Windows-Windows_10-X86_64") 14 | else() 15 | set(qt_package_suffix "-Windows-Windows_10_21H2-MSVC2019-Windows-Windows_10_21H2-X86_64") 16 | endif() 17 | elseif ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Linux") 18 | set(url_os "linux_x64") 19 | set(qt_package_arch_suffix "gcc_64") 20 | set(qt_dir_prefix "${qt_version}/gcc_64") 21 | if("${qt_version_major}" STREQUAL "5") 22 | set(qt_package_suffix "-Linux-RHEL_7_6-GCC-Linux-RHEL_7_6-X86_64") 23 | else() 24 | set(qt_package_suffix "-Linux-RHEL_8_4-GCC-Linux-RHEL_8_4-X86_64") 25 | endif() 26 | elseif ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Darwin") 27 | set(url_os "mac_x64") 28 | set(qt_package_arch_suffix "clang_64") 29 | if("${qt_version_major}" STREQUAL "5") 30 | set(qt_dir_prefix "${qt_version}/clang_64") 31 | set(qt_package_suffix "-MacOS-MacOS_10_13-Clang-MacOS-MacOS_10_13-X86_64") 32 | else() 33 | set(qt_dir_prefix "${qt_version}/macos") 34 | set(qt_package_suffix "-MacOS-MacOS_12-Clang-MacOS-MacOS_12-X86_64-ARM64") 35 | endif() 36 | endif() 37 | 38 | set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt${qt_version_major}_${qt_version_dotless}") 39 | file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS) 40 | 41 | file(READ ./Updates.xml updates_xml) 42 | string(REGEX MATCH "qt.qt${qt_version_major}.*([0-9+-.]+)" updates_xml_output "${updates_xml}") 43 | set(qt_package_version ${CMAKE_MATCH_1}) 44 | 45 | file(MAKE_DIRECTORY qt) 46 | 47 | # Save the path for other steps 48 | file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt/${qt_dir_prefix}" qt_dir) 49 | file(APPEND $ENV{GITHUB_OUTPUT} "qt_dir=${qt_dir}") 50 | 51 | message("Downloading Qt to ${qt_dir}") 52 | function(downloadAndExtract url archive) 53 | message("Downloading ${url}") 54 | file(DOWNLOAD "${url}" ./${archive} SHOW_PROGRESS) 55 | execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt) 56 | endfunction() 57 | 58 | foreach(package qtbase qtdeclarative qtsvg qttools) 59 | downloadAndExtract( 60 | "${qt_base_url}/qt.qt${qt_version_major}.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z" 61 | ${package}.7z 62 | ) 63 | endforeach() 64 | 65 | if("${qt_version_major}" STREQUAL "6") 66 | foreach(package qt5compat qtshadertools) 67 | downloadAndExtract( 68 | "${qt_base_url}/qt.qt6.${qt_version_dotless}.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z" 69 | ${package}.7z 70 | ) 71 | endforeach() 72 | endif() 73 | 74 | # uic depends on libicu56.so 75 | if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Linux") 76 | downloadAndExtract( 77 | "${qt_base_url}/qt.qt${qt_version_major}.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-Rhel7.2-x64.7z" 78 | icu.7z 79 | ) 80 | endif() 81 | -------------------------------------------------------------------------------- /tools/ci/InstallSystemLibs.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Linux") 5 | execute_process( 6 | COMMAND sudo apt update 7 | ) 8 | execute_process( 9 | COMMAND sudo apt install libgl1-mesa-dev 10 | RESULT_VARIABLE result 11 | ) 12 | if (NOT result EQUAL 0) 13 | message(FATAL_ERROR "Failed to install dependencies") 14 | endif() 15 | endif() 16 | -------------------------------------------------------------------------------- /tools/ci/Package.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Mikhail Zolotukhin 2 | # SPDX-License-Identifier: MIT 3 | 4 | set(QTC_EXT_DIR "${CMAKE_CURRENT_LIST_DIR}/../../external/qtcreator") 5 | set(QTC_DIR "${QTC_EXT_DIR}/dist-${CMAKE_HOST_SYSTEM_NAME}-$ENV{QT_CREATOR_VERSION}") 6 | 7 | set(build_plugin_py "scripts/build_plugin.py") 8 | foreach(dir "share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts") 9 | if(EXISTS "${QTC_DIR}/${dir}/build_plugin.py") 10 | set(build_plugin_py "${dir}/build_plugin.py") 11 | break() 12 | endif() 13 | endforeach() 14 | 15 | execute_process( 16 | COMMAND python 17 | -u 18 | "${QTC_DIR}/${build_plugin_py}" 19 | --name "$ENV{PLUGIN_NAME}-$ENV{QT_CREATOR_VERSION}-$ENV{ARTIFACT_SUFFIX}" 20 | --src . 21 | --build build 22 | --qt-path "$ENV{QT_DIR}" 23 | --qtc-path "${QTC_DIR}" 24 | --output-path "$ENV{GITHUB_WORKSPACE}" 25 | RESULT_VARIABLE result 26 | ) 27 | if (NOT result EQUAL 0) 28 | string(REGEX MATCH "FAILED:.*$" error_message "${output}") 29 | string(REPLACE "\n" "%0A" error_message "${error_message}") 30 | message("::error::${error_message}") 31 | message(FATAL_ERROR "Build failed") 32 | endif() 33 | --------------------------------------------------------------------------------