diff --git a/CMakeLists.txt b/CMakeLists.txt index deade37d1a4999833a15faf8f8f62535d0964cf6..0b90ba619e018d0102484656f92623c26c04e6ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,7 +118,12 @@ target_compile_options(Vivy PRIVATE target_link_libraries(Vivy PRIVATE -fopenmp) # Prepare for Qt6 -target_compile_definitions(Vivy PRIVATE QT_DISABLE_DEPRECATED_BEFORE=0x050F00) +target_compile_definitions(Vivy PRIVATE + QT_DISABLE_DEPRECATED_BEFORE=0x050F00 + QT_NO_CAST_TO_ASCII + # QT_RESTRICTED_CAST_FROM_ASCII + QTCREATOR_UTILS_STATIC_LIB +) # Some compiler specific warnings and options if (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c614dc0d12264e326bb42721661fa4fa61e8c7c..28e21f9c4f9eec42856ef9a5d081961dd9594fe5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,11 +19,12 @@ rules to keep in mind: The dependencies between the src's sub-folders are as follows: -| folder | the folders dependencies | -| :----------- | :----------------------- | -| src | *everything* | -| src/Lib | *nothing* | -| src/UI | src/Lib | +| folder | the folders dependencies | +| :---------------- | :----------------------- | +| src | *everything* | +| src/Lib | *nothing* | +| src/UI | src/Lib | +| src/UI/FakeVim | src/Lib/*Utils* | The ASS lib has no dependency other than the Utils. The Document lib has the Utils and ASS dependencies. @@ -31,6 +32,11 @@ the Utils and ASS dependencies. The C++ source files are `.cc` files and their corresponding header files are `.hh` files. +The *FakeVim* part is imported from QtCreator and uses its own +namespace. Its code can picks things from the `Vivy::Utils` namespace. +The rest of the code should use the FakeVim as defined and included in +the `UI/FakeVim/FakeVimTr.hh` header. + ### C++ Standard Here we use C++20, or at least we try. Even if the stl has some good diff --git a/LICENSE b/LICENSE index facd1aa4c8ca67be93e92073bcd04c289bd0f22c..0d83951c8886edbb2460138a64af42deac72bbc7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,504 +1,164 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - Vivy - Copyright (C) 2021 Elliu, Kubat - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 - USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random - Hacker. - - <signature of Ty Coon>, 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md index ee2312464888bfaa77586c4d5e7b5c57191d8fc6..ab4f429f8d89ec471138afa5dab4be07aa38f61e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ compiler with `-DCMAKE_CXX_COMPILER` and you're good to go. ## Licence -This software is licenced under the LGPL v2.1 or latter. +This software is licenced under the LGPL v3.0 or latter. --- diff --git a/rsc/icons/lua.png b/rsc/icons/lua.png index 4bdf7a9109a548c3b1fd786d68fc0452008120fc..8812ad939896e48b3d8b8be51d8f84f4a05698b9 100644 Binary files a/rsc/icons/lua.png and b/rsc/icons/lua.png differ diff --git a/src/Lib/AbstractDocument.cc b/src/Lib/AbstractDocument.cc new file mode 100644 index 0000000000000000000000000000000000000000..4cf89ed4bb61bd50ba7149d0d6bb81bf2269a647 --- /dev/null +++ b/src/Lib/AbstractDocument.cc @@ -0,0 +1,7 @@ +#include "AbstractDocument.hh" + +bool +Vivy::operator==(const AbstractDocument &a, const AbstractDocument &b) noexcept +{ + return a.getUuid() == b.getUuid(); +} diff --git a/src/Lib/AbstractDocument.hh b/src/Lib/AbstractDocument.hh index b561acfc284733abcbc49acb077152db40054867..5a22f96aa7eb51b416dafc004d1b34b2d006e5be 100644 --- a/src/Lib/AbstractDocument.hh +++ b/src/Lib/AbstractDocument.hh @@ -66,6 +66,8 @@ public: signals: void documentChanged(); }; + +bool operator==(const AbstractDocument &a, const AbstractDocument &b) noexcept; } #endif // VIVY_ABSTRACT_DOCUMENT_H diff --git a/src/Lib/Document/VivyDocument.cc b/src/Lib/Document/VivyDocument.cc index 54c433721d31f10115ca7d3bd942adc88564e626..ae4c12355717d9ab4bb5246724259ed58a4cc389 100644 --- a/src/Lib/Document/VivyDocument.cc +++ b/src/Lib/Document/VivyDocument.cc @@ -152,11 +152,14 @@ VivyDocument::getDocumentCapabilitiesString() const noexcept { QStringList ret; if (documentType & AudioAble) - ret.push_back("AudioAble" + QString(audioDocument ? "" : "(ack)")); + ret.push_back("AudioAble" + + QString(audioDocument ? QStringLiteral("") : QStringLiteral("(ack)"))); if (documentType & VideoAble) - ret.push_back("VideoAble" + QString(videoDocument ? "" : "(ack)")); + ret.push_back("VideoAble" + + QString(videoDocument ? QStringLiteral("") : QStringLiteral("(ack)"))); if (documentType & AssAble) - ret.push_back("AssAble" + QString(assDocument ? "" : "(ack)")); + ret.push_back("AssAble" + + QString(assDocument ? QStringLiteral("") : QStringLiteral("(ack)"))); return ret.join(", "); } diff --git a/src/Lib/HostOsInfo.cc b/src/Lib/HostOsInfo.cc new file mode 100644 index 0000000000000000000000000000000000000000..4951b7c5ef605b150267f43a56c8e274b6463e06 --- /dev/null +++ b/src/Lib/HostOsInfo.cc @@ -0,0 +1,71 @@ +#include "HostOsInfo.hh" + +#include <QCoreApplication> + +#if !defined(QT_NO_OPENGL) && defined(QT_GUI_LIB) +#include <QOpenGLContext> +#endif + +#ifdef Q_OS_WIN +#include <qt_windows.h> +#endif + +using namespace Vivy::Utils; + +Qt::CaseSensitivity HostOsInfo::m_overrideFileNameCaseSensitivity = Qt::CaseSensitive; +bool HostOsInfo::m_useOverrideFileNameCaseSensitivity = false; + +#ifdef Q_OS_WIN +static WORD +hostProcessorArchitecture() +{ + SYSTEM_INFO info; + GetNativeSystemInfo(&info); + return info.wProcessorArchitecture; +} +#endif + +HostOsInfo::HostArchitecture +HostOsInfo::hostArchitecture() +{ +#ifdef Q_OS_WIN + static const WORD processorArchitecture = hostProcessorArchitecture(); + switch (processorArchitecture) { + case PROCESSOR_ARCHITECTURE_AMD64: return HostOsInfo::HostArchitectureAMD64; + case PROCESSOR_ARCHITECTURE_INTEL: return HostOsInfo::HostArchitectureX86; + case PROCESSOR_ARCHITECTURE_IA64: return HostOsInfo::HostArchitectureItanium; + case PROCESSOR_ARCHITECTURE_ARM: return HostOsInfo::HostArchitectureArm; + default: return HostOsInfo::HostArchitectureUnknown; + } +#else + return HostOsInfo::HostArchitectureUnknown; +#endif +} + +void +HostOsInfo::setOverrideFileNameCaseSensitivity(Qt::CaseSensitivity sensitivity) +{ + m_useOverrideFileNameCaseSensitivity = true; + m_overrideFileNameCaseSensitivity = sensitivity; +} + +void +HostOsInfo::unsetOverrideFileNameCaseSensitivity() +{ + m_useOverrideFileNameCaseSensitivity = false; +} + +bool +HostOsInfo::canCreateOpenGLContext(QString *errorMessage) +{ +#if defined(QT_NO_OPENGL) || !defined(QT_GUI_LIB) + Q_UNUSED(errorMessage) + return false; +#else + static const bool canCreate = QOpenGLContext().create(); + if (!canCreate) + *errorMessage = + QCoreApplication::translate("Utils::HostOsInfo", "Cannot create OpenGL context."); + return canCreate; +#endif +} diff --git a/src/Lib/HostOsInfo.hh b/src/Lib/HostOsInfo.hh new file mode 100644 index 0000000000000000000000000000000000000000..090135fcd2516c2c5c9f4e7e42d44b31dbf27daf --- /dev/null +++ b/src/Lib/HostOsInfo.hh @@ -0,0 +1,81 @@ +#pragma once + +#include "Utils.hh" +#include <QString> + +#ifdef Q_OS_WIN +#define VIVY_HOST_EXE_SUFFIX VIVY_WIN_EXE_SUFFIX +#else +#define VIVY_HOST_EXE_SUFFIX "" +#endif // Q_OS_WIN + +namespace Vivy::Utils +{ +class HostOsInfo { +public: + static constexpr OsType hostOs() + { +#if defined(Q_OS_WIN) + return OsTypeWindows; +#elif defined(Q_OS_LINUX) + return OsTypeLinux; +#elif defined(Q_OS_MAC) + return OsTypeMac; +#elif defined(Q_OS_UNIX) + return OsTypeOtherUnix; +#else + return OsTypeOther; +#endif + } + + enum HostArchitecture { + HostArchitectureX86, + HostArchitectureAMD64, + HostArchitectureItanium, + HostArchitectureArm, + HostArchitectureUnknown + }; + static HostArchitecture hostArchitecture(); + + static constexpr bool isWindowsHost() { return hostOs() == OsTypeWindows; } + static constexpr bool isLinuxHost() { return hostOs() == OsTypeLinux; } + static constexpr bool isMacHost() { return hostOs() == OsTypeMac; } + static constexpr bool isAnyUnixHost() + { +#ifdef Q_OS_UNIX + return true; +#else + return false; +#endif + } + + static QString withExecutableSuffix(const QString &executable) + { + return OsSpecificAspects::withExecutableSuffix(hostOs(), executable); + } + + static void setOverrideFileNameCaseSensitivity(Qt::CaseSensitivity sensitivity); + static void unsetOverrideFileNameCaseSensitivity(); + + static Qt::CaseSensitivity fileNameCaseSensitivity() + { + return m_useOverrideFileNameCaseSensitivity + ? m_overrideFileNameCaseSensitivity + : OsSpecificAspects::fileNameCaseSensitivity(hostOs()); + } + + static QChar pathListSeparator() { return OsSpecificAspects::pathListSeparator(hostOs()); } + + static Qt::KeyboardModifier controlModifier() + { + return OsSpecificAspects::controlModifier(hostOs()); + } + + static bool canCreateOpenGLContext(QString *errorMessage); + +private: + static Qt::CaseSensitivity m_overrideFileNameCaseSensitivity; + static bool m_useOverrideFileNameCaseSensitivity; +}; + +} // namespace Utils diff --git a/src/Lib/Script/CRTPLuaScriptObject/ModuleDeclaration.cc b/src/Lib/Script/CRTPLuaScriptObject/ModuleDeclaration.cc index d30eeee2c284e445eb5332e9db6ec1b4a1ae2656..b904ecae569de30341dc05f14799fe4d08400ecc 100644 --- a/src/Lib/Script/CRTPLuaScriptObject/ModuleDeclaration.cc +++ b/src/Lib/Script/CRTPLuaScriptObject/ModuleDeclaration.cc @@ -222,7 +222,7 @@ ModuleDeclaration::pushToRuntime(lua_State *const L) noexcept if (context->registerDeclaration(self) == LuaContext::Code::Error) context->setFailed("Failed to register module " + self->moduleName); -#pragma message("Import needed modules here!") + TODO(Import needed modules here !) LUA_RETURN_NOTHING(L); } diff --git a/src/Lib/Utils.cc b/src/Lib/Utils.cc index 5096a639cc0ba7a08d38423abca9c07172f7720f..cbce31685828d8155449a1de096616c39ef3899d 100644 --- a/src/Lib/Utils.cc +++ b/src/Lib/Utils.cc @@ -5,6 +5,73 @@ using namespace Vivy; +std::string & +Utils::ltrim(std::string &s, const char *t) +{ + s.erase(0, s.find_first_not_of(t)); + return s; +} + +std::string & +Utils::rtrim(std::string &s, const char *t) +{ + s.erase(s.find_last_not_of(t) + 1); + return s; +} + +std::string & +Utils::trim(std::string &s, const char *t) +{ + return Utils::ltrim(Utils::rtrim(s, t), t); +} + +QString +Utils::OsSpecificAspects::withExecutableSuffix(OsType osType, const QString &executable) +{ + QString finalName = executable; + if (osType == OsTypeWindows) + finalName += QLatin1String(VIVY_WIN_EXE_SUFFIX); + return finalName; +} + +Qt::CaseSensitivity +Utils::OsSpecificAspects::fileNameCaseSensitivity(OsType osType) +{ + return osType == OsTypeWindows || osType == OsTypeMac ? Qt::CaseInsensitive : Qt::CaseSensitive; +} + +Qt::CaseSensitivity +Utils::OsSpecificAspects::envVarCaseSensitivity(OsType osType) +{ + return fileNameCaseSensitivity(osType); +} + +QChar +Utils::OsSpecificAspects::pathListSeparator(OsType osType) +{ + return QLatin1Char(osType == OsTypeWindows ? ';' : ':'); +} + +Qt::KeyboardModifier +Utils::OsSpecificAspects::controlModifier(OsType osType) +{ + return osType == OsTypeMac ? Qt::MetaModifier : Qt::ControlModifier; +} + +QString +Utils::OsSpecificAspects::pathWithNativeSeparators(OsType osType, const QString &pathName) +{ + if (osType == OsTypeWindows) { + const int pos = pathName.indexOf('/'); + if (pos >= 0) { + QString n = pathName; + std::replace(std::begin(n) + pos, std::end(n), '/', '\\'); + return n; + } + } + return pathName; +} + bool Utils::detectDocumentType(const QFileInfo &file, DocumentType *const type) { @@ -198,3 +265,13 @@ Utils::getAnyDocumentFileSuffixFilter() noexcept getVideoFileSuffixFilter() + separator + getAssFileSuffixFilter() + separator; return ret; } + +void +Utils::writeAssertLocation(const char *msg) +{ + static bool goBoom = qEnvironmentVariableIsSet("QTC_FATAL_ASSERTS"); + if (goBoom) + qFatal("SOFT ASSERT made fatal: %s", msg); + else + qDebug("SOFT ASSERT: %s", msg); +} diff --git a/src/Lib/Utils.hh b/src/Lib/Utils.hh index bf61eedb9d13e2ee85a8d33e90f766b8c4e9b1bc..3283a2c8707f61376a5e96e0baa9ac92dbb57ce6 100644 --- a/src/Lib/Utils.hh +++ b/src/Lib/Utils.hh @@ -1,5 +1,4 @@ -#ifndef VIVY_UTILS_H -#define VIVY_UTILS_H +#pragma once #ifndef __cplusplus #error "This is a C++ header" @@ -15,6 +14,24 @@ #include <type_traits> #include <chrono> +#include <qglobal.h> + +#define VIVY_PRAGMA(x) _Pragma(#x) +#define TODO(x) VIVY_PRAGMA(message("\"TODO: " #x "\"")) + +#define VIVY_WIN_EXE_SUFFIX ".exe" +#define VIVY_ASSERT_STRINGIFY_HELPER(x) #x +#define VIVY_ASSERT_STRINGIFY(x) VIVY_ASSERT_STRINGIFY_HELPER(x) +#define VIVY_ASSERT_STRING(cond) \ + ::Vivy::Utils::writeAssertLocation("\"" cond "\" in file " __FILE__ \ + ", line " VIVY_ASSERT_STRINGIFY(__LINE__)) + +#define VIVY_GUARD(cond) ((Q_LIKELY(cond)) ? true : (VIVY_ASSERT_STRING(#cond), false)) +#define VIVY_UNUSED __attribute__((unused)) +#define VIVY_EXPORT __attribute__((visibility("default"))) +#define VIVY_NO_EXPORT __attribute__((visibility("hidden"))) +#define VIVY_DEPRECATED __attribute__((__deprecated__)) + // Use chrono instead of std::chrono... namespace chrono = std::chrono; @@ -48,6 +65,9 @@ concept PropertyConstViewable = requires(T element) namespace Vivy::Utils { +// Add more as needed. +enum OsType { OsTypeWindows, OsTypeLinux, OsTypeMac, OsTypeOtherUnix, OsTypeOther }; + static const QStringList audioFileSuffix = { "wave", "wav", "ogg", "mp3", "m4a", "opus", "mp2", "aiff", "flac", "alac" }; static const QStringList videoFileSuffix = { "mkv", "mp4", "mov", "avi", "av1", "m4v", "flv" }; @@ -64,25 +84,9 @@ const QString &getAnyDocumentFileSuffixFilter() noexcept; static constexpr std::size_t pointerAlignement = alignof(void *); -inline std::string & -ltrim(std::string &s, const char *t = " \t\n\r\f\v") -{ - s.erase(0, s.find_first_not_of(t)); - return s; -} - -inline std::string & -rtrim(std::string &s, const char *t = " \t\n\r\f\v") -{ - s.erase(s.find_last_not_of(t) + 1); - return s; -} - -inline std::string & -trim(std::string &s, const char *t = " \t\n\r\f\v") -{ - return ltrim(rtrim(s, t), t); -} +std::string <rim(std::string &s, const char *t = " \t\n\r\f\v"); +std::string &rtrim(std::string &s, const char *t = " \t\n\r\f\v"); +std::string &trim(std::string &s, const char *t = " \t\n\r\f\v"); template <typename T> inline void uniqAndSort(std::vector<T> &vec) noexcept @@ -162,6 +166,21 @@ bool detectDocumentType(const QFileInfo &, DocumentType *const); bool decodeLineToBoolean(const QString &item, const QString &error); int decodeLineToInteger(const QString &item, const QString &error); float decodeLineToFloating(const QString &item, const QString &error); + +void writeAssertLocation(const char *msg); + +struct OsSpecificAspects final { +private: + OsSpecificAspects() {} + +public: + static QString withExecutableSuffix(OsType osType, const QString &executable); + static Qt::CaseSensitivity fileNameCaseSensitivity(OsType osType); + static Qt::CaseSensitivity envVarCaseSensitivity(OsType osType); + static QChar pathListSeparator(OsType osType); + static Qt::KeyboardModifier controlModifier(OsType osType); + static QString pathWithNativeSeparators(OsType osType, const QString &pathName); +}; } namespace Vivy @@ -191,6 +210,7 @@ enum class VideoDocumentType : quint64 { // Ass document types enum class AssDocumentType : quint64 { ASS = Utils::toUnderlying(Utils::DocumentType::ASS) }; + } class QMenu; @@ -203,5 +223,3 @@ namespace Vivy class VivyApplication; class MainWindow; } - -#endif // VIVY_UTILS_H diff --git a/src/UI/AboutWindow.cc b/src/UI/AboutWindow.cc index 90b25a0bf343da6bbe464ab2879a4cc50611f54f..4bb7e87b24f480d6f45cb70eb4e6c33ac08cd0a9 100644 --- a/src/UI/AboutWindow.cc +++ b/src/UI/AboutWindow.cc @@ -17,7 +17,16 @@ using namespace Vivy; static const char *aboutContent = "<body>" - " <p>Vivy is a replacement for Aegisub, writen in Qt5+and with less segfaults - hopefully.</p>" + " <p>Vivy is a replacement for Aegisub, writen in Qt5+and with less" + " segfaults - hopefully.</p>" + " <p>This software is licenced under the LGPL v3.0 or latter. The" + " FakeVim part is imported from QtCreator, © 2016 The Qt" + " Company Ltd. and other contributors. Vivy is © Vivy" + " contributors.</p>" + " <p>Contributors:<ul>" + " <li>Elliu</li>" + " <li>Kubat</li>" + " </ul></p>" "</body>"; static const char *libContent = @@ -43,8 +52,9 @@ AboutWindow::SimpleLabel::SimpleLabel(QWidget *parent, const char *text) setTextFormat(Qt::RichText); setTextInteractionFlags(Qt::NoTextInteraction | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); - setText(text); + setText(QString(text)); setAlignment(Qt::AlignJustify | Qt::AlignTop); + setWordWrap(true); } AboutWindow::LicenceLabel::LicenceLabel(QWidget *parent, const QString &url, @@ -68,7 +78,7 @@ AboutWindow::LicenceLabel::LicenceLabel(QWidget *parent, const QString &url, switch (format) { case Qt::PlainText: - case Qt::RichText: setText(content.readAll()); break; + case Qt::RichText: setText(QString(content.readAll())); break; case Qt::MarkdownText: setMarkdown(content.readAll()); break; case Qt::AutoText: qCritical() << "Invalid text format for LicenceLabel" << format; } diff --git a/src/UI/DocumentViews/MpvContainer.cc b/src/UI/DocumentViews/MpvContainer.cc index d3fdd9668dda50999c43bf2f32edd6ab6cc2836d..ef64b08f76860e1aed2df00318ea67590e7ebdea 100644 --- a/src/UI/DocumentViews/MpvContainer.cc +++ b/src/UI/DocumentViews/MpvContainer.cc @@ -106,7 +106,7 @@ MpvContainer::handleMpvEvent(mpv_event *event) noexcept case MPV_EVENT_LOG_MESSAGE: msg = reinterpret_cast<mpv_event_log_message *>(event->data); - msgText = msg->text; + msgText = QString(msg->text); msgText.replace('\n', ""); qDebug().nospace().noquote() << "MPV - MSG [" << msg->prefix << "] " << msg->level << ": " << msgText; diff --git a/src/UI/FakeVim/FakeVimActions.cc b/src/UI/FakeVim/FakeVimActions.cc new file mode 100644 index 0000000000000000000000000000000000000000..1c36b444d5647feb830b9a31ad0ce16468ba748c --- /dev/null +++ b/src/UI/FakeVim/FakeVimActions.cc @@ -0,0 +1,180 @@ +#include "FakeVimActions.hh" +#include "FakeVimHandler.hh" + +#include "../../Lib/Utils.hh" +#include <QDebug> + +namespace FakeVim::Internal +{ +#ifdef FAKEVIM_STANDALONE +FvBaseAspect::FvBaseAspect() {} + +void +FvBaseAspect::setValue(const QVariant &value) +{ + m_value = value; +} + +QVariant +FvBaseAspect::value() const +{ + return m_value; +} + +void +FvBaseAspect::setDefaultValue(const QVariant &value) +{ + m_defaultValue = value; + m_value = value; +} + +QVariant +FvBaseAspect::defaultValue() const +{ + return m_defaultValue; +} + +void +FvBaseAspect::setSettingsKey(const QString &group, const QString &key) +{ + m_settingsGroup = group; + m_settingsKey = key; +} + +QString +FvBaseAspect::settingsKey() const +{ + return m_settingsKey; +} +#endif + +FakeVimSettings::FakeVimSettings() +{ +#ifndef FAKEVIM_STANDALONE + setup(&useFakeVim, false, "UseFakeVim", {}, tr("Use FakeVim")); +#endif + // Specific FakeVim settings + setup(&readVimRc, false, "ReadVimRc", {}, tr("Read .vimrc from location:")); + setup(&vimRcPath, QString(), "VimRcPath", {}, {}); // tr("Path to .vimrc") + setup(&showMarks, false, "ShowMarks", "sm", tr("Show position of text marks")); + setup(&passControlKey, false, "PassControlKey", "pck", tr("Pass control keys")); + setup(&passKeys, true, "PassKeys", "pk", tr("Pass keys in insert mode")); + + // Emulated Vsetting + setup(&startOfLine, true, "StartOfLine", "sol", tr("Start of line")); + setup(&tabStop, 8, "TabStop", "ts", tr("Tabulator size:")); + setup(&smartTab, false, "SmartTab", "sta", tr("Smart tabulators")); + setup(&hlSearch, true, "HlSearch", "hls", tr("Highlight search results")); + setup(&shiftWidth, 8, "ShiftWidth", "sw", tr("Shift width:")); + setup(&expandTab, false, "ExpandTab", "et", tr("Expand tabulators")); + setup(&autoIndent, false, "AutoIndent", "ai", tr("Automatic indentation")); + setup(&smartIndent, false, "SmartIndent", "si", tr("Smart tabulators")); + setup(&incSearch, true, "IncSearch", "is", tr("Incremental search")); + setup(&useCoreSearch, false, "UseCoreSearch", "ucs", tr("Use search dialog")); + setup(&smartCase, false, "SmartCase", "scs", tr("Use smartcase")); + setup(&ignoreCase, false, "IgnoreCase", "ic", tr("Use ignorecase")); + setup(&wrapScan, true, "WrapScan", "ws", tr("Use wrapscan")); + setup(&tildeOp, false, "TildeOp", "top", tr("Use tildeop")); + setup(&showCmd, true, "ShowCmd", "sc", tr("Show partial command")); + setup(&relativeNumber, false, "RelativeNumber", "rnu", + tr("Show line numbers relative to cursor")); + setup(&blinkingCursor, false, "BlinkingCursor", "bc", tr("Blinking cursor")); + setup(&scrollOff, 0, "ScrollOff", "so", tr("Scroll offset:")); + setup(&backspace, "indent,eol,start", "Backspace", "bs", tr("Backspace:")); + setup(&isKeyword, "@,48-57,_,192-255,a-z,A-Z", "IsKeyword", "isk", tr("Keyword characters:")); + setup(&clipboard, {}, "Clipboard", "cb", tr("")); + setup(&formatOptions, {}, "formatoptions", "fo", tr("")); + + // Emulated plugins + setup(&emulateVimCommentary, false, "commentary", {}, "vim-commentary"); + setup(&emulateReplaceWithRegister, false, "ReplaceWithRegister", {}, "ReplaceWithRegister"); + setup(&emulateExchange, false, "exchange", {}, "vim-exchange"); + setup(&emulateArgTextObj, false, "argtextobj", {}, "argtextobj.vim"); + setup(&emulateSurround, false, "surround", {}, "vim-surround"); + + // Some polish + useFakeVim.setDisplayName(tr("Use Vim-style Editing")); + + relativeNumber.setToolTip(tr("Displays line numbers relative to the line containing " + "text cursor.")); + + passControlKey.setToolTip( + tr("Does not interpret key sequences like Ctrl-S in FakeVim " + "but handles them as regular shortcuts. This gives easier access to core functionality " + "at the price of losing some features of FakeVim.")); + + passKeys.setToolTip(tr("Does not interpret some key presses in insert mode so that " + "code can be properly completed and expanded.")); + + tabStop.setToolTip(tr("Vim tabstop option.")); + +#ifndef FAKEVIM_STANDALONE + backspace.setDisplayStyle(FvStringAspect::LineEditDisplay); + isKeyword.setDisplayStyle(FvStringAspect::LineEditDisplay); + + const QString vimrcDefault = + QLatin1String(HostOsInfo::isAnyUnixHost() ? "$HOME/.vimrc" : "%USERPROFILE%\\_vimrc"); + vimRcPath.setExpectedKind(PathChooser::File); + vimRcPath.setToolTip(tr("Keep empty to use the default path, i.e. " + "%USERPROFILE%\\_vimrc on Windows, ~/.vimrc otherwise.")); + vimRcPath.setPlaceHolderText(tr("Default: %1").arg(vimrcDefault)); + vimRcPath.setDisplayStyle(FvStringAspect::PathChooserDisplay); +#endif +} + +FakeVimSettings::~FakeVimSettings() = default; + +FvBaseAspect * +FakeVimSettings::item(const QString &name) +{ + return m_nameToAspect.value(name, nullptr); +} + +QString +FakeVimSettings::trySetValue(const QString &name, const QString &value) +{ + FvBaseAspect *aspect = m_nameToAspect.value(name, nullptr); + if (!aspect) + return tr("Unknown option: %1").arg(name); + if (aspect == &tabStop || aspect == &shiftWidth) { + if (value.toInt() <= 0) + return tr("Argument must be positive: %1=%2").arg(name).arg(value); + } + aspect->setValue(value); + return QString(); +} + +void +FakeVimSettings::setup(FvBaseAspect *aspect, const QVariant &value, const QString &settingsKey, + const QString &shortName, const QString &labelText) +{ + aspect->setSettingsKey("FakeVim", settingsKey); + aspect->setDefaultValue(value); +#ifndef FAKEVIM_STANDALONE + aspect->setLabelText(labelText); + aspect->setAutoApply(false); + registerAspect(aspect); + + if (auto boolAspect = dynamic_cast<FvBoolAspect *>(aspect)) + boolAspect->setLabelPlacement(FvBoolAspect::LabelPlacement::AtCheckBoxWithoutDummyLabel); +#else + Q_UNUSED(labelText) +#endif + + const QString longName = settingsKey.toLower(); + if (!longName.isEmpty()) { + m_nameToAspect[longName] = aspect; + m_aspectToName[aspect] = longName; + } + if (!shortName.isEmpty()) + m_nameToAspect[shortName] = aspect; +} + +FakeVimSettings * +fakeVimSettings() +{ + static FakeVimSettings s; + return &s; +} + +} // namespace FakeVim::Internal diff --git a/src/UI/FakeVim/FakeVimActions.hh b/src/UI/FakeVim/FakeVimActions.hh new file mode 100644 index 0000000000000000000000000000000000000000..b1cea03d6697830e2637ca2efbbca89ff8ecdf0e --- /dev/null +++ b/src/UI/FakeVim/FakeVimActions.hh @@ -0,0 +1,123 @@ +#pragma once + +#define FAKEVIM_STANDALONE + +#include <QCoreApplication> +#include <QHash> +#include <QObject> +#include <QString> +#include <QVariant> + +namespace FakeVim::Internal +{ +class FvBaseAspect { +public: + FvBaseAspect(); + virtual ~FvBaseAspect() {} + + void setValue(const QVariant &value); + QVariant value() const; + void setDefaultValue(const QVariant &value); + QVariant defaultValue() const; + void setSettingsKey(const QString &group, const QString &key); + QString settingsKey() const; + void setCheckable(bool) {} + void setDisplayName(const QString &) {} + void setToolTip(const QString &) {} + +private: + QVariant m_value; + QVariant m_defaultValue; + QString m_settingsGroup; + QString m_settingsKey; +}; + +class FvBoolAspect : public FvBaseAspect { +public: + bool value() const { return FvBaseAspect::value().toBool(); } +}; + +class FvIntegerAspect : public FvBaseAspect { +public: + qint64 value() const { return FvBaseAspect::value().toLongLong(); } +}; + +class FvStringAspect : public FvBaseAspect { +public: + QString value() const { return FvBaseAspect::value().toString(); } +}; + +class FvAspectContainer : public FvBaseAspect { +public: +}; + +class FakeVimSettings final : public FvAspectContainer { + Q_DECLARE_TR_FUNCTIONS(FakeVim) + +public: + FakeVimSettings(); + ~FakeVimSettings() override; + + FvBaseAspect *item(const QString &name); + QString trySetValue(const QString &name, const QString &value); + + FvBoolAspect useFakeVim; + FvBoolAspect readVimRc; + FvStringAspect vimRcPath; + + FvBoolAspect startOfLine; + FvIntegerAspect tabStop; + FvBoolAspect hlSearch; + FvBoolAspect smartTab; + FvIntegerAspect shiftWidth; + FvBoolAspect expandTab; + FvBoolAspect autoIndent; + FvBoolAspect smartIndent; + + FvBoolAspect incSearch; + FvBoolAspect useCoreSearch; + FvBoolAspect smartCase; + FvBoolAspect ignoreCase; + FvBoolAspect wrapScan; + + // command ~ behaves as g~ + FvBoolAspect tildeOp; + + // indent allow backspacing over autoindent + // eol allow backspacing over line breaks (join lines) + // start allow backspacing over the start of insert; CTRL-W and CTRL-U + // stop once at the start of insert. + FvStringAspect backspace; + + // @,48-57,_,192-255 + FvStringAspect isKeyword; + + // other actions + FvBoolAspect showMarks; + FvBoolAspect passControlKey; + FvBoolAspect passKeys; + FvStringAspect clipboard; + FvBoolAspect showCmd; + FvIntegerAspect scrollOff; + FvBoolAspect relativeNumber; + FvStringAspect formatOptions; + + // Plugin emulation + FvBoolAspect emulateVimCommentary; + FvBoolAspect emulateReplaceWithRegister; + FvBoolAspect emulateExchange; + FvBoolAspect emulateArgTextObj; + FvBoolAspect emulateSurround; + + FvBoolAspect blinkingCursor; + +private: + void setup(FvBaseAspect *aspect, const QVariant &value, const QString &settingsKey, + const QString &shortName, const QString &label); + + QHash<QString, FvBaseAspect *> m_nameToAspect; + QHash<FvBaseAspect *, QString> m_aspectToName; +}; + +FakeVimSettings *fakeVimSettings(); +} // namespace FakeVim::Internal diff --git a/src/UI/FakeVim/FakeVimHandler.cc b/src/UI/FakeVim/FakeVimHandler.cc new file mode 100644 index 0000000000000000000000000000000000000000..153b5a53ddf3c9ba9695da5e4da3c554f133e930 --- /dev/null +++ b/src/UI/FakeVim/FakeVimHandler.cc @@ -0,0 +1,9766 @@ +// +// ATTENTION: +// +// 1 Please do not add any direct dependencies to other Qt Creator code here. +// Instead emit signals and let the FakeVimPlugin channel the information to +// Qt Creator. The idea is to keep this file here in a "clean" state that +// allows easy reuse with any QTextEdit or QPlainTextEdit derived class. +// +// 2 There are a few auto tests located in ../../../tests/auto/fakevim. +// Commands that are covered there are marked as "// tested" below. +// +// 3 Some conventions: +// +// Use 1 based line numbers and 0 based column numbers. Even though +// the 1 based line are not nice it matches vim's and QTextEdit's 'line' +// concepts. +// +// Do not pass QTextCursor etc around unless really needed. Convert +// early to line/column. +// +// A QTextCursor is always between characters, whereas vi's cursor is always +// over a character. FakeVim interprets the QTextCursor to be over the character +// to the right of the QTextCursor's position(). +// +// A current "region of interest" +// spans between anchor(), (i.e. the character below anchor()), and +// position(). The character below position() is not included +// if the last movement command was exclusive (MoveExclusive). +// + +#include "FakeVimHandler.hh" +#include "FakeVimActions.hh" +#include "FakeVimTr.hh" +#include "../../Lib/Utils.hh" + +#include <QDebug> +#include <QFile> +#include <QObject> +#include <QPointer> +#include <QProcess> +#include <QRegularExpression> +#include <QTextStream> +#include <QTimer> +#include <QStack> + +#include <QApplication> +#include <QClipboard> +#include <QInputMethodEvent> +#include <QKeyEvent> +#include <QLineEdit> +#include <QPlainTextEdit> +#include <QScrollBar> +#include <QTextBlock> +#include <QTextCursor> +#include <QTextDocumentFragment> +#include <QTextEdit> +#include <QMimeData> +#include <QSharedPointer> +#include <QDir> + +#include <algorithm> +#include <climits> +#include <ctype.h> +#include <functional> + +//#define DEBUG_KEY 1 +#if defined(DEBUG_KEY) && DEBUG_KEY +#define KEY_DEBUG(s) qDebug() << s +#else +#define KEY_DEBUG(s) +#endif + +//#define DEBUG_UNDO 1 +#if defined(DEBUG_UNDO) && DEBUG_UNDO +#define UNDO_DEBUG(s) qDebug() << "REV" << revision() << s +#else +#define UNDO_DEBUG(s) +#endif + +namespace FakeVim::Internal +{ +/////////////////////////////////////////////////////////////////////// +// +// FakeVimHandler +// +/////////////////////////////////////////////////////////////////////// + +#define EDITOR(s) (m_textedit ? m_textedit->s : m_plaintextedit->s) + +#ifdef Q_OS_DARWIN +static constexpr Qt::KeyboardModifier ControlModifier = Qt::MetaModifier; +#else +static constexpr Qt::KeyboardModifier ControlModifier = Qt::ControlModifier; +#endif + +// Clipboard MIME types used by Vim. +static const QString vimMimeText = "_VIM_TEXT"; +static const QString vimMimeTextEncoded = "_VIMENC_TEXT"; + +using namespace Qt; + +// A \e Mode represents one of the basic modes of operation of FakeVim. + +enum Mode { InsertMode, ReplaceMode, CommandMode, ExMode }; + +enum BlockInsertMode { + NoneBlockInsertMode, + AppendBlockInsertMode, + AppendToEndOfLineBlockInsertMode, + InsertBlockInsertMode, + ChangeBlockInsertMode +}; + +// A \e SubMode is used for things that require one more data item +// and are 'nested' behind a `\\l` Mode. +enum SubMode { + NoSubMode, + ChangeSubMode, // Used for c + DeleteSubMode, // Used for d + ExchangeSubMode, // Used for cx + DeleteSurroundingSubMode, // Used for ds + ChangeSurroundingSubMode, // Used for cs + AddSurroundingSubMode, // Used for ys + FilterSubMode, // Used for ! + IndentSubMode, // Used for = + RegisterSubMode, // Used for " + ShiftLeftSubMode, // Used for < + ShiftRightSubMode, // Used for > + CommentSubMode, // Used for gc + ReplaceWithRegisterSubMode, // Used for gr + InvertCaseSubMode, // Used for g~ + DownCaseSubMode, // Used for gu + UpCaseSubMode, // Used for gU + WindowSubMode, // Used for Ctrl-w + YankSubMode, // Used for y + ZSubMode, // Used for z + CapitalZSubMode, // Used for Z + ReplaceSubMode, // Used for r + MacroRecordSubMode, // Used for q + MacroExecuteSubMode, // Used for @ + CtrlVSubMode, // Used for Ctrl-v in insert mode + CtrlRSubMode // Used for Ctrl-r in insert mode +}; + +// A \e SubSubMode is used for things that require one more data item +// and are 'nested' behind a `\\l` SubMode. +enum SubSubMode { + NoSubSubMode, + FtSubSubMode, // Used for f, F, t, T. + MarkSubSubMode, // Used for m. + BackTickSubSubMode, // Used for `. + TickSubSubMode, // Used for '. + TextObjectSubSubMode, // Used for thing like iw, aW, as etc. + ZSubSubMode, // Used for zj, zk + OpenSquareSubSubMode, // Used for [{, {(, [z + CloseSquareSubSubMode, // Used for ]}, ]), ]z + SearchSubSubMode, // Used for /, ? + SurroundSubSubMode, // Used for cs, ds, ys + SurroundWithFunctionSubSubMode, // Used for ys{motion}f + CtrlVUnicodeSubSubMode // Used for Ctrl-v based unicode input +}; + +enum VisualMode { NoVisualMode, VisualCharMode, VisualLineMode, VisualBlockMode }; + +enum MoveType { MoveExclusive, MoveInclusive, MoveLineWise }; + +/* + \enum RangeMode + + The \e RangeMode serves as a means to define how the "Range" between + the \\l cursor and the \\l anchor position is to be interpreted. + + \\value RangeCharMode Entered by pressing \\key v. The range includes + all characters between cursor and anchor. + \\value RangeLineMode Entered by pressing \\key V. The range includes + all lines between the line of the cursor and + the line of the anchor. + \\value RangeLineModeExclusive Like \\l RangeLineMode, but keeps one + newline when deleting. + \\value RangeBlockMode Entered by pressing \\key Ctrl-v. The range includes + all characters with line and column coordinates + between line and columns coordinates of cursor and + anchor. + \\value RangeBlockAndTailMode Like \\l RangeBlockMode, but also includes + all characters in the affected lines up to the end + of these lines. +*/ + +enum EventResult { + EventHandled, + EventUnhandled, + EventCancelled, // Event is handled but a sub mode was cancelled. + EventPassedToCore +}; + +struct CursorPosition { + CursorPosition() = default; + CursorPosition(int block, int columnArg) + : line(block) + , column(columnArg) + { + } + explicit CursorPosition(const QTextCursor &tc) + : line(tc.block().blockNumber()) + , column(tc.positionInBlock()) + { + } + CursorPosition(const QTextDocument *document, int position) + { + QTextBlock block = document->findBlock(position); + line = block.blockNumber(); + column = position - block.position(); + } + bool isValid() const { return line >= 0 && column >= 0; } + bool operator>(const CursorPosition &other) const + { + return line > other.line || column > other.column; + } + bool operator==(const CursorPosition &other) const + { + return line == other.line && column == other.column; + } + bool operator!=(const CursorPosition &other) const { return !operator==(other); } + + int line = -1; // Line in document (from 0, folded lines included). + int column = -1; // Position on line. +}; + +VIVY_UNUSED static QDebug +operator<<(QDebug ts, const CursorPosition &pos) +{ + return ts << "(line: " << pos.line << ", column: " << pos.column << ")"; +} + +// vi style configuration + +class Mark { +public: + Mark(const CursorPosition &pos = CursorPosition(), const QString &fileName = QString()) + : m_position(pos) + , m_fileName(fileName) + { + } + + bool isValid() const { return m_position.isValid(); } + + bool isLocal(const QString &localFileName) const + { + return m_fileName.isEmpty() || m_fileName == localFileName; + } + + /* Return position of mark within given document. + * If saved line number is too big, mark position is at the end of document. + * If line number is in document but column is too big, mark position is at the end of line. + */ + CursorPosition position(const QTextDocument *document) const + { + QTextBlock block = document->findBlockByNumber(m_position.line); + CursorPosition pos; + if (block.isValid()) { + pos.line = m_position.line; + pos.column = qMax(0, qMin(m_position.column, block.length() - 2)); + } else if (document->isEmpty()) { + pos.line = 0; + pos.column = 0; + } else { + pos.line = document->blockCount() - 1; + pos.column = qMax(0, document->lastBlock().length() - 2); + } + return pos; + } + + const QString &fileName() const { return m_fileName; } + + void setFileName(const QString &fileName) { m_fileName = fileName; } + +private: + CursorPosition m_position; + QString m_fileName; +}; +using Marks = QHash<QChar, Mark>; + +struct State { + State() = default; + State(int revisionArg, const CursorPosition &positionArg, const Marks &marksArg, + VisualMode lastVisualModeArg, bool lastVisualModeInvertedArg) + : revision(revisionArg) + , position(positionArg) + , marks(marksArg) + , lastVisualMode(lastVisualModeArg) + , lastVisualModeInverted(lastVisualModeInvertedArg) + { + } + + bool isValid() const { return position.isValid(); } + + int revision = -1; + CursorPosition position; + Marks marks; + VisualMode lastVisualMode = NoVisualMode; + bool lastVisualModeInverted = false; +}; + +struct Column { + Column(int p, int l) + : physical(p) + , logical(l) + { + } + int physical; // Number of characters in the data. + int logical; // Column on screen. +}; + +VIVY_UNUSED static QDebug +operator<<(QDebug ts, const Column &col) +{ + return ts << "(p: " << col.physical << ", l: " << col.logical << ")"; +} + +struct Register { + Register() = default; + Register(const QString &c) + : contents(c) + { + } + Register(const QString &c, RangeMode m) + : contents(c) + , rangemode(m) + { + } + QString contents; + RangeMode rangemode = RangeCharMode; +}; + +VIVY_UNUSED static QDebug +operator<<(QDebug ts, const Register ®) +{ + return ts << reg.contents; +} + +struct SearchData { + QString needle; + bool forward = true; + bool highlightMatches = true; +}; + +static QString +replaceTildeWithHome(QString str) +{ + str.replace("~", QDir::homePath()); + return str; +} + +// If string begins with given prefix remove it with trailing spaces and return true. +static bool +eatString(const QString &prefix, QString *str) +{ + if (!str->startsWith(prefix)) + return false; + *str = str->mid(prefix.size()).trimmed(); + return true; +} + +static QRegularExpression +vimPatternToQtPattern(const QString &needle) +{ + /* Transformations (Vim regexp -> QRegularExpression): + * \a -> [A-Za-z] + * \A -> [^A-Za-z] + * \h -> [A-Za-z_] + * \H -> [^A-Za-z_] + * \l -> [a-z] + * \L -> [^a-z] + * \o -> [0-7] + * \O -> [^0-7] + * \u -> [A-Z] + * \U -> [^A-Z] + * \x -> [0-9A-Fa-f] + * \X -> [^0-9A-Fa-f] + * + * \< -> \b + * \> -> \b + * [] -> \[\] + * \= -> ? + * + * (...) <-> \(...\) + * {...} <-> \{...\} + * | <-> \| + * ? <-> \? + * + <-> \+ + * \{...} -> {...} + * + * \c - set ignorecase for rest + * \C - set noignorecase for rest + */ + + // FIXME: Option smartcase should be used only if search was typed by user. + const bool ignoreCaseOption = fakeVimSettings()->ignoreCase.value(); + const bool smartCaseOption = fakeVimSettings()->smartCase.value(); + const bool initialIgnoreCase = + ignoreCaseOption && !(smartCaseOption && needle.contains(QRegularExpression("[A-Z]"))); + + bool ignorecase = initialIgnoreCase; + + QString pattern; + pattern.reserve(2 * needle.size()); + + bool escape = false; + bool brace = false; + bool embraced = false; + bool range = false; + bool curly = false; + for (const QChar &c : needle) { + if (brace) { + brace = false; + if (c == ']') { + pattern.append("\\[\\]"); + continue; + } + pattern.append('['); + escape = true; + embraced = true; + } + if (embraced) { + if (range) { + QChar c2 = pattern[pattern.size() - 2]; + pattern.remove(pattern.size() - 2, 2); + pattern.append(c2.toUpper() + '-' + c.toUpper()); + pattern.append(c2.toLower() + '-' + c.toLower()); + range = false; + } else if (escape) { + escape = false; + pattern.append(c); + } else if (c == '\\') { + escape = true; + } else if (c == ']') { + pattern.append(']'); + embraced = false; + } else if (c == '-') { + range = ignorecase && pattern[pattern.size() - 1].isLetter(); + pattern.append('-'); + } else if (c.isLetter() && ignorecase) { + pattern.append(c.toLower()).append(c.toUpper()); + } else { + pattern.append(c); + } + } else if (QString("(){}+|?").indexOf(c) != -1) { + if (c == '{') { + curly = escape; + } else if (c == '}' && curly) { + curly = false; + escape = true; + } + + if (escape) + escape = false; + else + pattern.append('\\'); + pattern.append(c); + } else if (escape) { + // escape expression + escape = false; + if (c == '<' || c == '>') + pattern.append("\\b"); + else if (c == 'a') + pattern.append("[a-zA-Z]"); + else if (c == 'A') + pattern.append("[^a-zA-Z]"); + else if (c == 'h') + pattern.append("[A-Za-z_]"); + else if (c == 'H') + pattern.append("[^A-Za-z_]"); + else if (c == 'c' || c == 'C') + ignorecase = (c == 'c'); + else if (c == 'l') + pattern.append("[a-z]"); + else if (c == 'L') + pattern.append("[^a-z]"); + else if (c == 'o') + pattern.append("[0-7]"); + else if (c == 'O') + pattern.append("[^0-7]"); + else if (c == 'u') + pattern.append("[A-Z]"); + else if (c == 'U') + pattern.append("[^A-Z]"); + else if (c == 'x') + pattern.append("[0-9A-Fa-f]"); + else if (c == 'X') + pattern.append("[^0-9A-Fa-f]"); + else if (c == '=') + pattern.append("?"); + else { + pattern.append('\\'); + pattern.append(c); + } + } else { + // unescaped expression + if (c == '\\') + escape = true; + else if (c == '[') + brace = true; + else if (c.isLetter() && ignorecase) + pattern.append('[').append(c.toLower()).append(c.toUpper()).append(']'); + else + pattern.append(c); + } + } + if (escape) + pattern.append('\\'); + else if (brace) + pattern.append('['); + + return QRegularExpression(pattern, initialIgnoreCase ? QRegularExpression::CaseInsensitiveOption + : QRegularExpression::NoPatternOption); +} + +static bool +afterEndOfLine(const QTextDocument *doc, int position) +{ + return doc->characterAt(position) == QChar::ParagraphSeparator && + doc->findBlock(position).length() > 1; +} + +static void +searchForward(QTextCursor *tc, const QRegularExpression &needleExp, int *repeat) +{ + const QTextDocument *doc = tc->document(); + const int startPos = tc->position(); + + QTextDocument::FindFlags flags = {}; + if (!(needleExp.patternOptions() & QRegularExpression::CaseInsensitiveOption)) + flags |= QTextDocument::FindCaseSensitively; + + // Search from beginning of line so that matched text is the same. + tc->movePosition(QTextCursor::StartOfLine); + + // forward to current position + *tc = doc->find(needleExp, *tc, flags); + while (!tc->isNull() && tc->anchor() < startPos) { + if (!tc->hasSelection()) + tc->movePosition(QTextCursor::Right); + if (tc->atBlockEnd()) + tc->movePosition(QTextCursor::NextBlock); + *tc = doc->find(needleExp, *tc, flags); + } + + if (tc->isNull()) + return; + + --*repeat; + + while (*repeat > 0) { + if (!tc->hasSelection()) + tc->movePosition(QTextCursor::Right); + if (tc->atBlockEnd()) + tc->movePosition(QTextCursor::NextBlock); + *tc = doc->find(needleExp, *tc, flags); + if (tc->isNull()) + return; + --*repeat; + } + + if (!tc->isNull() && afterEndOfLine(doc, tc->anchor())) + tc->movePosition(QTextCursor::Left); +} + +static void +searchBackward(QTextCursor *tc, const QRegularExpression &needleExp, int *repeat) +{ + // Search from beginning of line so that matched text is the same. + QTextBlock block = tc->block(); + QString line = block.text(); + + QRegularExpressionMatch match; + int i = line.indexOf(needleExp, 0, &match); + while (i != -1 && i < tc->positionInBlock()) { + --*repeat; + const int offset = i + qMax(1, match.capturedLength()); + i = line.indexOf(needleExp, offset, &match); + if (i == line.size()) + i = -1; + } + + if (i == tc->positionInBlock()) + --*repeat; + + while (*repeat > 0) { + block = block.previous(); + if (!block.isValid()) + break; + line = block.text(); + i = line.indexOf(needleExp, 0, &match); + while (i != -1) { + --*repeat; + const int offset = i + qMax(1, match.capturedLength()); + i = line.indexOf(needleExp, offset, &match); + if (i == line.size()) + i = -1; + } + } + + if (!block.isValid()) { + *tc = QTextCursor(); + return; + } + + i = line.indexOf(needleExp, 0, &match); + while (*repeat < 0) { + const int offset = i + qMax(1, match.capturedLength()); + i = line.indexOf(needleExp, offset, &match); + ++*repeat; + } + tc->setPosition(block.position() + i); + tc->setPosition(tc->position() + match.capturedLength(), QTextCursor::KeepAnchor); +} + +// Commands [[, [] +static void +bracketSearchBackward(QTextCursor *tc, const QString &needleExp, int repeat) +{ + const QRegularExpression re(needleExp); + QTextCursor tc2 = *tc; + tc2.setPosition(tc2.position() - 1); + searchBackward(&tc2, re, &repeat); + if (repeat <= 1) + tc->setPosition(tc2.isNull() ? 0 : tc2.position(), QTextCursor::KeepAnchor); +} + +// Commands ][, ]] +// When ]] is used after an operator, then also stops below a '}' in the first column. +static void +bracketSearchForward(QTextCursor *tc, const QString &needleExp, int repeat, bool searchWithCommand) +{ + QRegularExpression re(searchWithCommand ? QString("^\\}|^\\{") : needleExp); + QTextCursor tc2 = *tc; + tc2.setPosition(tc2.position() + 1); + searchForward(&tc2, re, &repeat); + if (repeat <= 1) { + if (tc2.isNull()) { + tc->setPosition(tc->document()->characterCount() - 1, QTextCursor::KeepAnchor); + } else { + tc->setPosition(tc2.position() - 1, QTextCursor::KeepAnchor); + if (searchWithCommand && tc->document()->characterAt(tc->position()).unicode() == '}') { + QTextBlock block = tc->block().next(); + if (block.isValid()) + tc->setPosition(block.position(), QTextCursor::KeepAnchor); + } + } + } +} + +static char +backslashed(char t) +{ + switch (t) { + case 'e': return 27; + case 't': return '\t'; + case 'r': return '\r'; + case 'n': return '\n'; + case 'b': return 8; + } + return t; +} + +enum class Modifier { NONE, UPPERCASE, LOWERCASE }; + +static QString +applyReplacementLetterCases(QString repl, Modifier &toggledModifier, + Modifier &nextCharacterModifier) +{ + if (toggledModifier == Modifier::UPPERCASE) + repl = repl.toUpper(); + else if (toggledModifier == Modifier::LOWERCASE) + repl = repl.toLower(); + + if (nextCharacterModifier == Modifier::UPPERCASE) { + repl.replace(0, 1, repl.at(0).toUpper()); + nextCharacterModifier = Modifier::NONE; + } else if (nextCharacterModifier == Modifier::LOWERCASE) { + repl.replace(0, 1, repl.at(0).toLower()); + nextCharacterModifier = Modifier::NONE; + } + return repl; +} + +static QChar +applyReplacementLetterCases(QChar repl, Modifier &toggledModifier, Modifier &nextCharacterModifier) +{ + if (nextCharacterModifier == Modifier::UPPERCASE) { + nextCharacterModifier = Modifier::NONE; + return repl.toUpper(); + } else if (nextCharacterModifier == Modifier::LOWERCASE) { + nextCharacterModifier = Modifier::NONE; + return repl.toLower(); + } else if (toggledModifier == Modifier::UPPERCASE) + return repl.toUpper(); + else if (toggledModifier == Modifier::LOWERCASE) + return repl.toLower(); + else + return repl; +} + +static bool +substituteText(QString *text, const QRegularExpression &pattern, const QString &replacement, + bool global) +{ + bool substituted = false; + int pos = 0; + int right = -1; + while (true) { + const QRegularExpressionMatch match = pattern.match(*text, pos); + if (!match.hasMatch()) + break; + + pos = match.capturedStart(); + + // ensure that substitution is advancing towards end of line + if (right == text->size() - pos) { + ++pos; + if (pos == text->size()) + break; + continue; + } + + right = text->size() - pos; + + substituted = true; + QString matched = text->mid(pos, match.captured(0).size()); + QString repl; + bool escape = false; + Modifier toggledModifier = Modifier::NONE; + Modifier nextCharacterModifier = Modifier::NONE; + // insert captured texts + for (int i = 0; i < replacement.size(); ++i) { + const QChar &c = replacement[i]; + if (escape) { + escape = false; + if (c.isDigit()) { + if (c.digitValue() <= match.lastCapturedIndex()) { + repl += applyReplacementLetterCases(match.captured(c.digitValue()), + toggledModifier, nextCharacterModifier); + } + } else if (c == 'u') { + nextCharacterModifier = Modifier::UPPERCASE; + } else if (c == 'l') { + nextCharacterModifier = Modifier::LOWERCASE; + } else if (c == 'U') { + toggledModifier = Modifier::UPPERCASE; + } else if (c == 'L') { + toggledModifier = Modifier::LOWERCASE; + } else if (c == 'e' || c == 'E') { + nextCharacterModifier = Modifier::NONE; + toggledModifier = Modifier::NONE; + } else { + repl += backslashed(static_cast<char>(c.unicode())); + } + } else { + if (c == '\\') + escape = true; + else if (c == '&') + repl += applyReplacementLetterCases(match.captured(0), toggledModifier, + nextCharacterModifier); + else + repl += applyReplacementLetterCases(c, toggledModifier, nextCharacterModifier); + } + } + text->replace(pos, matched.size(), repl); + pos += (repl.isEmpty() && matched.isEmpty()) ? 1 : repl.size(); + + if (pos >= text->size() || !global) + break; + } + + return substituted; +} + +static int +findUnescaped(QChar c, const QString &line, int from) +{ + for (int i = from; i < line.size(); ++i) { + if (line.at(i) == c && (i == 0 || line.at(i - 1) != '\\')) + return i; + } + return -1; +} + +static void +setClipboardData(const QString &content, RangeMode mode, QClipboard::Mode clipboardMode) +{ + QClipboard *clipboard = QApplication::clipboard(); + char vimRangeMode = mode; + + QByteArray bytes1; + bytes1.append(vimRangeMode); + bytes1.append(content.toUtf8()); + + QByteArray bytes2; + bytes2.append(vimRangeMode); + bytes2.append("utf-8"); + bytes2.append('\0'); + bytes2.append(content.toUtf8()); + + auto data = new QMimeData; + data->setText(content); + data->setData(vimMimeText, bytes1); + data->setData(vimMimeTextEncoded, bytes2); + clipboard->setMimeData(data, clipboardMode); +} + +static QByteArray +toLocalEncoding(const QString &text) +{ +#if defined(Q_OS_WIN) + return QString(text).replace("\n", "\r\n").toLocal8Bit(); +#else + return text.toLocal8Bit(); +#endif +} + +static QString +fromLocalEncoding(const QByteArray &data) +{ +#if defined(Q_OS_WIN) + return QString::fromLocal8Bit(data).replace("\n", "\r\n"); +#else + return QString::fromLocal8Bit(data); +#endif +} + +static QString +getProcessOutput(const QString &command, const QString &input) +{ + QProcess proc; +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + QStringList arguments = QProcess::splitCommand(command); + QString executable = arguments.takeFirst(); + proc.start(executable, arguments); +#else + proc.start(command); +#endif + proc.waitForStarted(); + proc.write(toLocalEncoding(input)); + proc.closeWriteChannel(); + + // FIXME: Process should be interruptable by user. + // Solution is to create a QObject for each process and emit finished state. + proc.waitForFinished(); + + return fromLocalEncoding(proc.readAllStandardOutput()); +} + +static const QMap<QString, int> & +vimKeyNames() +{ + static const QMap<QString, int> k = { // FIXME: Should be value of mapleader. + { "LEADER", Key_Backslash }, + + { "SPACE", Key_Space }, + { "TAB", Key_Tab }, + { "NL", Key_Return }, + { "NEWLINE", Key_Return }, + { "LINEFEED", Key_Return }, + { "LF", Key_Return }, + { "CR", Key_Return }, + { "RETURN", Key_Return }, + { "ENTER", Key_Return }, + { "BS", Key_Backspace }, + { "BACKSPACE", Key_Backspace }, + { "ESC", Key_Escape }, + { "BAR", Key_Bar }, + { "BSLASH", Key_Backslash }, + { "DEL", Key_Delete }, + { "DELETE", Key_Delete }, + { "KDEL", Key_Delete }, + { "UP", Key_Up }, + { "DOWN", Key_Down }, + { "LEFT", Key_Left }, + { "RIGHT", Key_Right }, + + { "LT", Key_Less }, + { "GT", Key_Greater }, + + { "F1", Key_F1 }, + { "F2", Key_F2 }, + { "F3", Key_F3 }, + { "F4", Key_F4 }, + { "F5", Key_F5 }, + { "F6", Key_F6 }, + { "F7", Key_F7 }, + { "F8", Key_F8 }, + { "F9", Key_F9 }, + { "F10", Key_F10 }, + + { "F11", Key_F11 }, + { "F12", Key_F12 }, + { "F13", Key_F13 }, + { "F14", Key_F14 }, + { "F15", Key_F15 }, + { "F16", Key_F16 }, + { "F17", Key_F17 }, + { "F18", Key_F18 }, + { "F19", Key_F19 }, + { "F20", Key_F20 }, + + { "F21", Key_F21 }, + { "F22", Key_F22 }, + { "F23", Key_F23 }, + { "F24", Key_F24 }, + { "F25", Key_F25 }, + { "F26", Key_F26 }, + { "F27", Key_F27 }, + { "F28", Key_F28 }, + { "F29", Key_F29 }, + { "F30", Key_F30 }, + + { "F31", Key_F31 }, + { "F32", Key_F32 }, + { "F33", Key_F33 }, + { "F34", Key_F34 }, + { "F35", Key_F35 }, + + { "INSERT", Key_Insert }, + { "INS", Key_Insert }, + { "KINSERT", Key_Insert }, + { "HOME", Key_Home }, + { "END", Key_End }, + { "PAGEUP", Key_PageUp }, + { "PAGEDOWN", Key_PageDown }, + + { "KPLUS", Key_Plus }, + { "KMINUS", Key_Minus }, + { "KDIVIDE", Key_Slash }, + { "KMULTIPLY", Key_Asterisk }, + { "KENTER", Key_Enter }, + { "KPOINT", Key_Period }, + + { "CAPS", Key_CapsLock }, + { "NUM", Key_NumLock }, + { "SCROLL", Key_ScrollLock }, + { "ALTGR", Key_AltGr } + }; + + return k; +} + +static bool +isOnlyControlModifier(const Qt::KeyboardModifiers &mods) +{ + return (mods ^ ControlModifier) == Qt::NoModifier; +} + +static bool +isAcceptableModifier(const Qt::KeyboardModifiers &mods) +{ + if (mods & ControlModifier) { + // Generally, CTRL is not fine, except in combination with ALT. + // See QTCREATORBUG-24673 + return mods & AltModifier; + } + return true; +} + +Range::Range(int b, int e, RangeMode m) + : beginPos(qMin(b, e)) + , endPos(qMax(b, e)) + , rangemode(m) +{ +} + +QString +Range::toString() const +{ + return QString("%1-%2 (mode: %3)").arg(beginPos).arg(endPos).arg(rangemode); +} + +bool +Range::isValid() const +{ + return beginPos >= 0 && endPos >= 0; +} + +static QDebug +operator<<(QDebug ts, const Range &range) +{ + return ts << '[' << range.beginPos << ',' << range.endPos << ']'; +} + +ExCommand::ExCommand(const QString &c, const QString &a, const Range &r) + : cmd(c) + , args(a) + , range(r) +{ +} + +bool +ExCommand::matches(const QString &min, const QString &full) const +{ + return cmd.startsWith(min) && full.startsWith(cmd); +} + +VIVY_UNUSED static QDebug +operator<<(QDebug ts, const ExCommand &cmd) +{ + return ts << cmd.cmd << ' ' << cmd.args << ' ' << cmd.range; +} + +VIVY_UNUSED static QDebug +operator<<(QDebug ts, const QList<QTextEdit::ExtraSelection> &sels) +{ + foreach(const QTextEdit::ExtraSelection &sel, sels) ts << "SEL: " << sel.cursor.anchor() + << sel.cursor.position(); + return ts; +} + +static QString +quoteUnprintable(const QString &ba) +{ + QString res; + for (int i = 0, n = ba.size(); i != n; ++i) { + const QChar c = ba.at(i); + const int cc = c.unicode(); + if (c.isPrint()) + res += c; + else if (cc == '\n') + res += "<CR>"; + else + res += QString("\\x%1").arg(c.unicode(), 2, 16, QLatin1Char('0')); + } + return res; +} + +static bool +startsWithWhitespace(const QString &str, int col) +{ + if (col > str.size()) { + qWarning("Wrong column"); + return false; + } + for (int i = 0; i < col; ++i) { + uint u = str.at(i).unicode(); + if (u != ' ' && u != '\t') + return false; + } + return true; +} + +inline QString +msgMarkNotSet(const QString &text) +{ + return Tr::tr("Mark \"%1\" not set.").arg(text); +} + +static void +initSingleShotTimer(QTimer *timer, int interval, FakeVimHandler::Private *receiver, + void (FakeVimHandler::Private::*slot)()) +{ + timer->setSingleShot(true); + timer->setInterval(interval); + QObject::connect(timer, &QTimer::timeout, receiver, slot); +} + +class Input { +public: + // Remove some extra "information" on Mac. + static Qt::KeyboardModifiers cleanModifier(Qt::KeyboardModifiers m) + { + return m & ~Qt::KeypadModifier; + } + + Input() = default; + explicit Input(QChar x) + : m_key(x.unicode()) + , m_xkey(x.unicode()) + , m_text(x) + { + if (x.isUpper()) + m_modifiers = Qt::ShiftModifier; + else if (x.isLower()) + m_key = x.toUpper().unicode(); + } + + Input(int k, Qt::KeyboardModifiers m, const QString &t = QString()) + : m_key(k) + , m_modifiers(cleanModifier(m)) + , m_text(t) + { + if (m_text.size() == 1) { + QChar x = m_text.at(0); + + // On Mac, QKeyEvent::text() returns non-empty strings for + // cursor keys. This breaks some of the logic later on + // relying on text() being empty for "special" keys. + // FIXME: Check the real conditions. + if (x.unicode() < ' ' && x.unicode() != 27) + m_text.clear(); + else if (x.isLetter()) + m_key = x.toUpper().unicode(); + } + + // Set text only if input is ascii key without control modifier. + if (m_text.isEmpty() && k >= 0 && k <= 0x7f && (m & ControlModifier) == 0) { + QChar c = QChar(k); + if (c.isLetter()) + m_text = isShift() ? c.toUpper() : c; + else if (!isShift()) + m_text = c; + } + + // Normalize <S-TAB>. + if (m_key == Qt::Key_Backtab) { + m_key = Qt::Key_Tab; + m_modifiers |= Qt::ShiftModifier; + } + + // m_xkey is only a cache. + m_xkey = (m_text.size() == 1 ? m_text.at(0).unicode() : m_key); + } + + bool isValid() const { return m_key != 0 || !m_text.isNull(); } + + bool isDigit() const { return m_xkey >= '0' && m_xkey <= '9'; } + + bool isKey(int c) const { return !m_modifiers && m_key == c; } + + bool isBackspace() const { return m_key == Key_Backspace || isControl('h'); } + + bool isReturn() const { return m_key == '\n' || m_key == Key_Return || m_key == Key_Enter; } + + bool isEscape() const + { + return isKey(Key_Escape) || isShift(Key_Escape) || isKey(27) || isShift(27) || + isControl('c') || isControl(Key_BracketLeft); + } + + bool is(int c) const { return m_xkey == c && isAcceptableModifier(m_modifiers); } + + bool isControl() const { return isOnlyControlModifier(m_modifiers); } + + bool isControl(int c) const + { + return isControl() && + (m_xkey == c || m_xkey + 32 == c || m_xkey + 64 == c || m_xkey + 96 == c); + } + + bool isShift() const { return m_modifiers & Qt::ShiftModifier; } + + bool isShift(int c) const { return isShift() && m_xkey == c; } + + bool operator<(const Input &a) const + { + if (m_key != a.m_key) + return m_key < a.m_key; + // Text for some mapped key cannot be determined (e.g. <C-J>) so if text is not set for + // one of compared keys ignore it. + if (!m_text.isEmpty() && !a.m_text.isEmpty() && m_text != " ") + return m_text < a.m_text; + return m_modifiers < a.m_modifiers; + } + + bool operator==(const Input &a) const { return !(*this < a || a < *this); } + + bool operator!=(const Input &a) const { return !operator==(a); } + + QString text() const { return m_text; } + + QChar asChar() const { return (m_text.size() == 1 ? m_text.at(0) : QChar()); } + + int toInt(bool *ok, int base) const + { + const int uc = asChar().unicode(); + int res; + if ('0' <= uc && uc <= '9') + res = uc - '0'; + else if ('a' <= uc && uc <= 'z') + res = 10 + uc - 'a'; + else if ('A' <= uc && uc <= 'Z') + res = 10 + uc - 'A'; + else + res = base; + *ok = res < base; + return *ok ? res : 0; + } + + int key() const { return m_key; } + + Qt::KeyboardModifiers modifiers() const { return m_modifiers; } + + // Return raw character for macro recording or dot command. + QChar raw() const + { + if (m_key == Key_Tab) + return '\t'; + if (m_key == Key_Return) + return '\n'; + if (m_key == Key_Escape) + return QChar(27); + return QChar(m_xkey); + } + + QString toString() const + { + if (!m_text.isEmpty()) + return QString(m_text).replace("<", "<LT>"); + + QString key = vimKeyNames().key(m_key); + bool namedKey = !key.isEmpty(); + + if (!namedKey) { + if (m_xkey == '<') + key = "<LT>"; + else if (m_xkey == '>') + key = "<GT>"; + else + key = QChar(m_xkey); + } + + bool shift = isShift(); + bool ctrl = isControl(); + if (shift) + key.prepend("S-"); + if (ctrl) + key.prepend("C-"); + + if (namedKey || shift || ctrl) { + key.prepend('<'); + key.append('>'); + } + + return key; + } + + QDebug dump(QDebug ts) const + { + return ts << m_key << '-' << m_modifiers << '-' << quoteUnprintable(m_text); + } + +private: + int m_key = 0; + int m_xkey = 0; + Qt::KeyboardModifiers m_modifiers = NoModifier; + QString m_text; +}; + +// mapping to <Nop> (do nothing) +static const Input Nop(-1, Qt::KeyboardModifiers(-1), QString()); + +static SubMode +letterCaseModeFromInput(const Input &input) +{ + if (input.is('~')) + return InvertCaseSubMode; + if (input.is('u')) + return DownCaseSubMode; + if (input.is('U')) + return UpCaseSubMode; + + return NoSubMode; +} + +static SubMode +indentModeFromInput(const Input &input) +{ + if (input.is('<')) + return ShiftLeftSubMode; + if (input.is('>')) + return ShiftRightSubMode; + if (input.is('=')) + return IndentSubMode; + + return NoSubMode; +} + +static SubMode +changeDeleteYankModeFromInput(const Input &input) +{ + if (input.is('c')) + return ChangeSubMode; + if (input.is('d')) + return DeleteSubMode; + if (input.is('y')) + return YankSubMode; + + return NoSubMode; +} + +static QString +dotCommandFromSubMode(SubMode submode) +{ + if (submode == ChangeSubMode) + return QLatin1String("c"); + if (submode == DeleteSubMode) + return QLatin1String("d"); + if (submode == CommentSubMode) + return QLatin1String("gc"); + if (submode == DeleteSurroundingSubMode) + return QLatin1String("ds"); + if (submode == ChangeSurroundingSubMode) + return QLatin1String("c"); + if (submode == AddSurroundingSubMode) + return QLatin1String("y"); + if (submode == ExchangeSubMode) + return QLatin1String("cx"); + if (submode == ReplaceWithRegisterSubMode) + return QLatin1String("gr"); + if (submode == InvertCaseSubMode) + return QLatin1String("g~"); + if (submode == DownCaseSubMode) + return QLatin1String("gu"); + if (submode == UpCaseSubMode) + return QLatin1String("gU"); + if (submode == IndentSubMode) + return QLatin1String("="); + if (submode == ShiftRightSubMode) + return QLatin1String(">"); + if (submode == ShiftLeftSubMode) + return QLatin1String("<"); + + return QString(); +} + +VIVY_UNUSED static QDebug +operator<<(QDebug ts, const Input &input) +{ + return input.dump(ts); +} + +class Inputs : public QVector<Input> { +public: + Inputs() = default; + + explicit Inputs(const QString &str, bool noremap = true, bool silent = false) + : m_noremap(noremap) + , m_silent(silent) + { + parseFrom(str); + squeeze(); + } + + bool noremap() const { return m_noremap; } + + bool silent() const { return m_silent; } + +private: + void parseFrom(const QString &str); + + bool m_noremap = true; + bool m_silent = false; +}; + +static Input +parseVimKeyName(const QString &keyName) +{ + if (keyName.length() == 1) + return Input(keyName.at(0)); + + const QStringList keys = keyName.split('-'); + const int len = keys.length(); + + if (len == 1 && keys.at(0).toUpper() == "NOP") + return Nop; + + Qt::KeyboardModifiers mods = NoModifier; + for (int i = 0; i < len - 1; ++i) { + const QString &key = keys[i].toUpper(); + if (key == "S") + mods |= Qt::ShiftModifier; + else if (key == "C") + mods |= ControlModifier; + else + return Input(); + } + + if (!keys.isEmpty()) { + const QString key = keys.last(); + if (key.length() == 1) { + // simple character + QChar c = key.at(0).toUpper(); + return Input(c.unicode(), mods); + } + + // find key name + QMap<QString, int>::ConstIterator it = vimKeyNames().constFind(key.toUpper()); + if (it != vimKeyNames().end()) + return Input(*it, mods); + } + + return Input(); +} + +void +Inputs::parseFrom(const QString &str) +{ + const int n = str.size(); + for (int i = 0; i < n; ++i) { + const QChar c = str.at(i); + if (c == '<') { + int j = str.indexOf('>', i); + Input input; + if (j != -1) { + const QString key = str.mid(i + 1, j - i - 1); + if (!key.contains('<')) + input = parseVimKeyName(key); + } + if (input.isValid()) { + append(input); + i = j; + } else { + append(Input(c)); + } + } else { + append(Input(c)); + } + } +} + +class History { +public: + History() + : m_items(QString()) + { + } + void append(const QString &item); + const QString &move(QStringView prefix, int skip); + const QString ¤t() const { return m_items[m_index]; } + const QStringList &items() const { return m_items; } + void restart() { m_index = m_items.size() - 1; } + +private: + // Last item is always empty or current search prefix. + QStringList m_items; + int m_index = 0; +}; + +void +History::append(const QString &item) +{ + if (item.isEmpty()) + return; + m_items.pop_back(); + m_items.removeAll(item); + m_items << item << QString(); + restart(); +} + +const QString & +History::move(QStringView prefix, int skip) +{ + if (!current().startsWith(prefix)) + restart(); + + if (m_items.last() != prefix) + m_items[m_items.size() - 1] = prefix.toString(); + + int i = m_index + skip; + if (!prefix.isEmpty()) + for (; i >= 0 && i < m_items.size() && !m_items[i].startsWith(prefix); i += skip) + ; + if (i >= 0 && i < m_items.size()) + m_index = i; + + return current(); +} + +// Command line buffer with prompt (i.e. :, / or ? characters), text contents and cursor position. +class CommandBuffer { +public: + void setPrompt(const QChar &prompt) { m_prompt = prompt; } + void setContents(const QString &s) + { + m_buffer = s; + m_anchor = m_pos = s.size(); + } + + void setContents(const QString &s, int pos, int anchor = -1) + { + m_buffer = s; + m_pos = m_userPos = pos; + m_anchor = anchor >= 0 ? anchor : pos; + } + + QStringView userContents() const { return QStringView{ m_buffer }.left(m_userPos); } + const QChar &prompt() const { return m_prompt; } + const QString &contents() const { return m_buffer; } + bool isEmpty() const { return m_buffer.isEmpty(); } + int cursorPos() const { return m_pos; } + int anchorPos() const { return m_anchor; } + bool hasSelection() const { return m_pos != m_anchor; } + + void insertChar(QChar c) + { + m_buffer.insert(m_pos++, c); + m_anchor = m_userPos = m_pos; + } + void insertText(const QString &s) + { + m_buffer.insert(m_pos, s); + m_anchor = m_userPos = m_pos = m_pos + s.size(); + } + void deleteChar() + { + if (m_pos) + m_buffer.remove(--m_pos, 1); + m_anchor = m_userPos = m_pos; + } + + void moveLeft() + { + if (m_pos) + m_userPos = --m_pos; + } + void moveRight() + { + if (m_pos < m_buffer.size()) + m_userPos = ++m_pos; + } + void moveStart() { m_userPos = m_pos = 0; } + void moveEnd() { m_userPos = m_pos = m_buffer.size(); } + + void setHistoryAutoSave(bool autoSave) { m_historyAutoSave = autoSave; } + bool userContentsValid() const { return m_userPos >= 0 && m_userPos <= m_buffer.size(); } + void historyDown() + { + if (userContentsValid()) + setContents(m_history.move(userContents(), 1)); + } + void historyUp() + { + if (userContentsValid()) + setContents(m_history.move(userContents(), -1)); + } + const QStringList &historyItems() const { return m_history.items(); } + void historyPush(const QString &item = QString()) + { + m_history.append(item.isNull() ? contents() : item); + } + + void clear() + { + if (m_historyAutoSave) + historyPush(); + m_buffer.clear(); + m_anchor = m_userPos = m_pos = 0; + } + + QString display() const + { + QString msg(m_prompt); + for (int i = 0; i != m_buffer.size(); ++i) { + const QChar c = m_buffer.at(i); + if (c.unicode() < 32) { + msg += '^'; + msg += QChar(c.unicode() + 64); + } else { + msg += c; + } + } + return msg; + } + + void deleteSelected() + { + if (m_pos < m_anchor) { + m_buffer.remove(m_pos, m_anchor - m_pos); + m_anchor = m_pos; + } else { + m_buffer.remove(m_anchor, m_pos - m_anchor); + m_pos = m_anchor; + } + } + + bool handleInput(const Input &input) + { + if (input.isShift(Key_Left)) { + moveLeft(); + } else if (input.isShift(Key_Right)) { + moveRight(); + } else if (input.isShift(Key_Home)) { + moveStart(); + } else if (input.isShift(Key_End)) { + moveEnd(); + } else if (input.isKey(Key_Left)) { + moveLeft(); + m_anchor = m_pos; + } else if (input.isKey(Key_Right)) { + moveRight(); + m_anchor = m_pos; + } else if (input.isKey(Key_Home)) { + moveStart(); + m_anchor = m_pos; + } else if (input.isKey(Key_End)) { + moveEnd(); + m_anchor = m_pos; + } else if (input.isKey(Key_Up) || input.isKey(Key_PageUp)) { + historyUp(); + } else if (input.isKey(Key_Down) || input.isKey(Key_PageDown)) { + historyDown(); + } else if (input.isKey(Key_Delete)) { + if (hasSelection()) { + deleteSelected(); + } else { + if (m_pos < m_buffer.size()) + m_buffer.remove(m_pos, 1); + else + deleteChar(); + } + } else if (!input.text().isEmpty()) { + if (hasSelection()) + deleteSelected(); + insertText(input.text()); + } else { + return false; + } + return true; + } + +private: + QString m_buffer; + QChar m_prompt; + History m_history; + int m_pos = 0; + int m_anchor = 0; + int m_userPos = 0; // last position of inserted text (for retrieving history items) + bool m_historyAutoSave = true; // store items to history on clear()? +}; + +// Mappings for a specific mode (trie structure) +class ModeMapping : public QMap<Input, ModeMapping> { +public: + const Inputs &value() const { return m_value; } + void setValue(const Inputs &value) { m_value = value; } + +private: + Inputs m_value; +}; + +// Mappings for all modes +using Mappings = QHash<char, ModeMapping>; + +// Iterator for mappings +class MappingsIterator : public QVector<ModeMapping::Iterator> { +public: + MappingsIterator(Mappings *mappings, char mode = -1, const Inputs &inputs = Inputs()) + : m_parent(mappings) + { + reset(mode); + walk(inputs); + } + + // Reset iterator state. Keep previous mode if 0. + void reset(char mode = 0) + { + clear(); + m_lastValid = -1; + m_currentInputs.clear(); + if (mode != 0) { + m_mode = mode; + if (mode != -1) + m_modeMapping = m_parent->find(mode); + } + } + + bool isValid() const { return !empty(); } + + // Return true if mapping can be extended. + bool canExtend() const { return isValid() && !last()->empty(); } + + // Return true if this mapping can be used. + bool isComplete() const { return m_lastValid != -1; } + + // Return size of current map. + int mapLength() const { return m_lastValid + 1; } + + bool walk(const Input &input) + { + m_currentInputs.append(input); + + if (m_modeMapping == m_parent->end()) + return false; + + ModeMapping::Iterator it; + if (isValid()) { + it = last()->find(input); + if (it == last()->end()) + return false; + } else { + it = m_modeMapping->find(input); + if (it == m_modeMapping->end()) + return false; + } + + if (!it->value().isEmpty()) + m_lastValid = size(); + append(it); + + return true; + } + + bool walk(const Inputs &inputs) + { + for (const Input &input : inputs) { + if (!walk(input)) + return false; + } + return true; + } + + // Return current mapped value. Iterator must be valid. + const Inputs &inputs() const { return at(m_lastValid)->value(); } + + void remove() + { + if (isValid()) { + if (canExtend()) { + last()->setValue(Inputs()); + } else { + if (size() > 1) { + while (last()->empty()) { + at(size() - 2)->erase(last()); + pop_back(); + if (size() == 1 || !last()->value().isEmpty()) + break; + } + if (last()->empty() && last()->value().isEmpty()) + m_modeMapping->erase(last()); + } else if (last()->empty() && !last()->value().isEmpty()) { + m_modeMapping->erase(last()); + } + } + } + } + + void setInputs(const Inputs &key, const Inputs &inputs, bool unique = false) + { + ModeMapping *current = &(*m_parent)[m_mode]; + for (const Input &input : key) + current = &(*current)[input]; + if (!unique || current->value().isEmpty()) + current->setValue(inputs); + } + + const Inputs ¤tInputs() const { return m_currentInputs; } + +private: + Mappings *m_parent; + Mappings::Iterator m_modeMapping; + int m_lastValid = -1; + char m_mode = 0; + Inputs m_currentInputs; +}; + +// state of current mapping +struct MappingState { + MappingState() = default; + MappingState(bool noremapArg, bool silentArg, bool editBlockArg) + : noremap(noremapArg) + , silent(silentArg) + , editBlock(editBlockArg) + { + } + bool noremap = false; + bool silent = false; + bool editBlock = false; +}; + +class FakeVimHandler::Private : public QObject { +public: + Private(FakeVimHandler *parent, QWidget *widget); + ~Private() override; + + EventResult handleEvent(QKeyEvent *ev); + bool wantsOverride(QKeyEvent *ev); + bool parseExCommand(QString *line, ExCommand *cmd); + bool parseLineRange(QString *line, ExCommand *cmd); + int parseLineAddress(QString *cmd); + void parseRangeCount(const QString &line, Range *range) const; + void handleCommand(const QString &cmd); // Sets m_tc + handleExCommand + void handleExCommand(const QString &cmd); + + void installEventFilter(); + void removeEventFilter(); + void passShortcuts(bool enable); + void setupWidget(); + void restoreWidget(int tabSize); + + friend class FakeVimHandler; + + void init(); + void focus(); + void unfocus(); + void fixExternalCursor(bool focus); + void fixExternalCursorPosition(bool focus); + + // Call before any FakeVim processing (import cursor position from editor) + void enterFakeVim(); + // Call after any FakeVim processing + // (if needUpdate is true, export cursor position to editor and scroll) + void leaveFakeVim(bool needUpdate = true); + void leaveFakeVim(EventResult eventResult); + + EventResult handleKey(const Input &input); + EventResult handleDefaultKey(const Input &input); + bool handleCommandBufferPaste(const Input &input); + EventResult handleCurrentMapAsDefault(); + void prependInputs(const QVector<Input> &inputs); // Handle inputs. + void prependMapping(const Inputs &inputs); // Handle inputs as mapping. + bool expandCompleteMapping(); // Return false if current mapping is not complete. + bool extendMapping(const Input &input); // Return false if no suitable mappig found. + void endMapping(); + bool canHandleMapping(); + void clearPendingInput(); + void waitForMapping(); + EventResult stopWaitForMapping(bool hasInput); + EventResult handleInsertOrReplaceMode(const Input &); + void handleInsertMode(const Input &); + void handleReplaceMode(const Input &); + void finishInsertMode(); + + EventResult handleCommandMode(const Input &); + + // return true only if input in current mode and sub-mode was correctly handled + bool handleEscape(); + bool handleNoSubMode(const Input &); + bool handleChangeDeleteYankSubModes(const Input &); + void handleChangeDeleteYankSubModes(); + bool handleReplaceSubMode(const Input &); + bool handleCommentSubMode(const Input &); + bool handleReplaceWithRegisterSubMode(const Input &); + bool handleExchangeSubMode(const Input &); + bool handleDeleteChangeSurroundingSubMode(const Input &); + bool handleAddSurroundingSubMode(const Input &); + bool handleFilterSubMode(const Input &); + bool handleRegisterSubMode(const Input &); + bool handleShiftSubMode(const Input &); + bool handleChangeCaseSubMode(const Input &); + bool handleWindowSubMode(const Input &); + bool handleZSubMode(const Input &); + bool handleCapitalZSubMode(const Input &); + bool handleMacroRecordSubMode(const Input &); + bool handleMacroExecuteSubMode(const Input &); + + bool + handleCount(const Input &); // Handle count for commands (return false if input isn't count). + bool handleMovement(const Input &); + + EventResult handleExMode(const Input &); + EventResult handleSearchSubSubMode(const Input &); + bool handleCommandSubSubMode(const Input &); + void fixSelection(); // Fix selection according to current range, move and command modes. + bool finishSearch(); + void finishMovement(const QString &dotCommandMovement = QString()); + + // Returns to insert/replace mode after <C-O> command in insert mode, + // otherwise returns to command mode. + void leaveCurrentMode(); + + // Clear data for current (possibly incomplete) command in current mode. + // I.e. clears count, register, g flag, sub-modes etc. + void clearCurrentMode(); + + QTextCursor search(const SearchData &sd, int startPos, int count, bool showMessages); + void search(const SearchData &sd, bool showMessages = true); + bool searchNext(bool forward = true); + void searchBalanced(bool forward, QChar needle, QChar other); + void highlightMatches(const QString &needle); + void stopIncrementalFind(); + void updateFind(bool isComplete); + + void resetCount(); + bool + isInputCount(const Input &) const; // Return true if input can be used as count for commands. + int mvCount() const { return qMax(1, g.mvcount); } + int opCount() const { return qMax(1, g.opcount); } + int count() const { return mvCount() * opCount(); } + QTextBlock block() const { return m_cursor.block(); } + int leftDist() const { return position() - block().position(); } + int rightDist() const { return block().length() - leftDist() - (isVisualCharMode() ? 0 : 1); } + bool atBlockStart() const { return m_cursor.atBlockStart(); } + bool atBlockEnd() const { return m_cursor.atBlockEnd(); } + bool atEndOfLine() const { return atBlockEnd() && block().length() > 1; } + bool atDocumentEnd() const { return position() >= lastPositionInDocument(true); } + bool atDocumentStart() const { return m_cursor.atStart(); } + + bool atEmptyLine(int pos) const; + bool atEmptyLine(const QTextCursor &tc) const; + bool atEmptyLine() const; + bool atBoundary(bool end, bool simple, bool onlyWords = false, + const QTextCursor &tc = QTextCursor()) const; + bool atWordBoundary(bool end, bool simple, const QTextCursor &tc = QTextCursor()) const; + bool atWordStart(bool simple, const QTextCursor &tc = QTextCursor()) const; + bool atWordEnd(bool simple, const QTextCursor &tc = QTextCursor()) const; + bool isFirstNonBlankOnLine(int pos); + + int + lastPositionInDocument(bool ignoreMode = false) const; // Returns last valid position in doc. + int firstPositionInLine(int line, + bool onlyVisibleLines = true) const; // 1 based line, 0 based pos + int lastPositionInLine(int line, + bool onlyVisibleLines = true) const; // 1 based line, 0 based pos + int lineForPosition(int pos) const; // 1 based line, 0 based pos + QString lineContents(int line) const; // 1 based line + QString textAt(int from, int to) const; + void setLineContents(int line, const QString &contents); // 1 based line + int blockBoundary(const QString &left, const QString &right, bool end, + int count) const; // end or start position of current code block + int lineNumber(const QTextBlock &block) const; + + int columnAt(int pos) const; + int blockNumberAt(int pos) const; + QTextBlock blockAt(int pos) const; + QTextBlock nextLine(const QTextBlock &block) const; // following line (respects wrapped parts) + QTextBlock + previousLine(const QTextBlock &block) const; // previous line (respects wrapped parts) + + int linesOnScreen() const; + int linesInDocument() const; + + // The following use all zero-based counting. + int cursorLineOnScreen() const; + int cursorLine() const; + int cursorBlockNumber() const; // "." address + int physicalCursorColumn() const; // as stored in the data + int logicalCursorColumn() const; // as visible on screen + int physicalToLogicalColumn(int physical, const QString &text) const; + int logicalToPhysicalColumn(int logical, const QString &text) const; + int windowScrollOffset() const; // return scrolloffset but max half the current window height + Column cursorColumn() const; // as visible on screen + void updateFirstVisibleLine(); + int firstVisibleLine() const; + int lastVisibleLine() const; + int lineOnTop(int count = 1) const; // [count]-th line from top reachable without scrolling + int + lineOnBottom(int count = 1) const; // [count]-th line from bottom reachable without scrolling + void scrollToLine(int line); + void scrollUp(int count); + void scrollDown(int count) { scrollUp(-count); } + void updateScrollOffset(); + void alignViewportToCursor(Qt::AlignmentFlag align, int line = -1, bool moveToNonBlank = false); + + int lineToBlockNumber(int line) const; + + void setCursorPosition(const CursorPosition &p); + void setCursorPosition(QTextCursor *tc, const CursorPosition &p); + + // Helper functions for indenting/ + bool isElectricCharacter(QChar c) const; + void indentSelectedText(QChar lastTyped = QChar()); + void indentText(const Range &range, QChar lastTyped = QChar()); + void shiftRegionLeft(int repeat = 1); + void shiftRegionRight(int repeat = 1); + + void moveToFirstNonBlankOnLine(); + void moveToFirstNonBlankOnLine(QTextCursor *tc); + void moveToFirstNonBlankOnLineVisually(); + void moveToNonBlankOnLine(QTextCursor *tc); + void moveToTargetColumn(); + void setTargetColumn(); + void moveToMatchingParanthesis(); + void moveToBoundary(bool simple, bool forward = true); + void moveToNextBoundary(bool end, int count, bool simple, bool forward); + void moveToNextBoundaryStart(int count, bool simple, bool forward = true); + void moveToNextBoundaryEnd(int count, bool simple, bool forward = true); + void moveToBoundaryStart(int count, bool simple, bool forward = true); + void moveToBoundaryEnd(int count, bool simple, bool forward = true); + void moveToNextWord(bool end, int count, bool simple, bool forward, bool emptyLines); + void moveToNextWordStart(int count, bool simple, bool forward = true, bool emptyLines = true); + void moveToNextWordEnd(int count, bool simple, bool forward = true, bool emptyLines = true); + void moveToWordStart(int count, bool simple, bool forward = true, bool emptyLines = true); + void moveToWordEnd(int count, bool simple, bool forward = true, bool emptyLines = true); + + // Convenience wrappers to reduce line noise. + void moveToStartOfLine(); + void moveToStartOfLineVisually(); + void moveToEndOfLine(); + void moveToEndOfLineVisually(); + void moveToEndOfLineVisually(QTextCursor *tc); + void moveBehindEndOfLine(); + void moveUp(int n = 1) { moveDown(-n); } + void moveDown(int n = 1); + void moveUpVisually(int n = 1) { moveDownVisually(-n); } + void moveDownVisually(int n = 1); + void moveVertically(int n = 1) + { + if (g.gflag) { + g.movetype = MoveExclusive; + moveDownVisually(n); + } else { + g.movetype = MoveLineWise; + moveDown(n); + } + } + void movePageDown(int count = 1); + void movePageUp(int count = 1) { movePageDown(-count); } + void dump(const char *msg) const + { + qDebug() << msg << "POS: " << anchor() << position() << "VISUAL: " << g.visualMode; + } + void moveRight(int n = 1) + { + if (isVisualCharMode()) { + const QTextBlock currentBlock = block(); + const int max = currentBlock.position() + currentBlock.length() - 1; + const int pos = position() + n; + setPosition(qMin(pos, max)); + } else { + m_cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, n); + } + if (atEndOfLine()) + q->fold(1, false); + setTargetColumn(); + } + void moveLeft(int n = 1) + { + m_cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, n); + setTargetColumn(); + } + void moveToNextCharacter() + { + moveRight(); + if (atEndOfLine()) + moveRight(); + } + void moveToPreviousCharacter() + { + moveLeft(); + if (atBlockStart()) + moveLeft(); + } + void setAnchor() { m_cursor.setPosition(position(), QTextCursor::MoveAnchor); } + void setAnchor(int position) { m_cursor.setPosition(position, QTextCursor::KeepAnchor); } + void setPosition(int position) { m_cursor.setPosition(position, QTextCursor::KeepAnchor); } + void setAnchorAndPosition(int anchor, int position) + { + m_cursor.setPosition(anchor, QTextCursor::MoveAnchor); + m_cursor.setPosition(position, QTextCursor::KeepAnchor); + } + + // Set cursor in text editor widget. + void commitCursor(); + + // Restore cursor from editor widget. + // Update selection, record jump and target column if cursor position + // changes externally (e.g. by code completion). + void pullCursor(); + + QTextCursor editorCursor() const; + + // Values to save when starting FakeVim processing. + int m_firstVisibleLine; + QTextCursor m_cursor; + bool m_cursorNeedsUpdate; + + bool moveToPreviousParagraph(int count = 1) { return moveToNextParagraph(-count); } + bool moveToNextParagraph(int count = 1); + void moveToParagraphStartOrEnd(int direction = 1); + + bool handleFfTt(const QString &key, bool repeats = false); + + void enterVisualInsertMode(QChar command); + void enterReplaceMode(); + void enterInsertMode(); + void enterInsertOrReplaceMode(Mode mode); + void enterCommandMode(Mode returnToMode = CommandMode); + void enterExMode(const QString &contents = QString()); + void showMessage(MessageLevel level, const QString &msg); + void clearMessage() { showMessage(MessageInfo, QString()); } + void notImplementedYet(); + void updateMiniBuffer(); + void updateSelection(); + void updateHighlights(); + void updateCursorShape(); + void setThinCursor(bool enable = true); + bool hasThinCursor() const; + QWidget *editor() const; + QTextDocument *document() const { return EDITOR(document()); } + QChar characterAt(int pos) const { return document()->characterAt(pos); } + QChar characterAtCursor() const { return characterAt(position()); } + + void joinPreviousEditBlock(); + void beginEditBlock(bool largeEditBlock = false); + void beginLargeEditBlock() { beginEditBlock(true); } + void endEditBlock(); + void breakEditBlock() { m_buffer->breakEditBlock = true; } + + bool canModifyBufferData() const { return m_buffer->currentHandler.data() == this; } + + void onContentsChanged(int position, int charsRemoved, int charsAdded); + void onCursorPositionChanged(); + void onUndoCommandAdded(); + + void onInputTimeout(); + void onFixCursorTimeout(); + + bool isCommandLineMode() const { return g.mode == ExMode || g.subsubmode == SearchSubSubMode; } + bool isInsertMode() const { return g.mode == InsertMode || g.mode == ReplaceMode; } + // Waiting for movement operator. + bool isOperatorPending() const + { + return g.submode == ChangeSubMode || g.submode == DeleteSubMode || + g.submode == ExchangeSubMode || g.submode == CommentSubMode || + g.submode == ReplaceWithRegisterSubMode || g.submode == AddSurroundingSubMode || + g.submode == FilterSubMode || g.submode == IndentSubMode || + g.submode == ShiftLeftSubMode || g.submode == ShiftRightSubMode || + g.submode == InvertCaseSubMode || g.submode == DownCaseSubMode || + g.submode == UpCaseSubMode || g.submode == YankSubMode; + } + + bool isVisualMode() const { return g.visualMode != NoVisualMode; } + bool isNoVisualMode() const { return g.visualMode == NoVisualMode; } + bool isVisualCharMode() const { return g.visualMode == VisualCharMode; } + bool isVisualLineMode() const { return g.visualMode == VisualLineMode; } + bool isVisualBlockMode() const { return g.visualMode == VisualBlockMode; } + char currentModeCode() const; + void updateEditor(); + + void selectTextObject(bool simple, bool inner); + void selectWordTextObject(bool inner); + void selectWORDTextObject(bool inner); + void selectSentenceTextObject(bool inner); + void selectParagraphTextObject(bool inner); + bool changeNumberTextObject(int count); + // return true only if cursor is in a block delimited with correct characters + bool selectBlockTextObject(bool inner, QChar left, QChar right); + bool selectQuotedStringTextObject(bool inner, const QString "e); + bool selectArgumentTextObject(bool inner); + + void commitInsertState(); + void invalidateInsertState(); + bool isInsertStateValid() const; + void clearLastInsertion(); + void ensureCursorVisible(); + void insertInInsertMode(const QString &text); + + // Macro recording + bool startRecording(const Input &input); + void record(const Input &input); + void stopRecording(); + bool executeRegister(int reg); + + // Handle current command as synonym + void handleAs(const QString &command); + +public: + QTextEdit *m_textedit; + QPlainTextEdit *m_plaintextedit; + bool m_wasReadOnly; // saves read-only state of document + + bool m_inFakeVim; // true if currently processing a key press or a command + + FakeVimHandler *q; + int m_register; + BlockInsertMode m_visualBlockInsert; + + bool m_anchorPastEnd; + bool m_positionPastEnd; // '$' & 'l' in visual mode can move past eol + + QString m_currentFileName; + + int m_findStartPosition; + + int anchor() const { return m_cursor.anchor(); } + int position() const { return m_cursor.position(); } + + // Transform text selected by cursor in current visual mode. + using Transformation = std::function<QString(const QString &)>; + void transformText(const Range &range, QTextCursor &tc, + const std::function<void()> &transform) const; + void transformText(const Range &range, const Transformation &transform); + + void insertText(QTextCursor &tc, const QString &text); + void insertText(const Register ®); + void removeText(const Range &range); + + void invertCase(const Range &range); + + void toggleComment(const Range &range); + + void exchangeRange(const Range &range); + + void replaceWithRegister(const Range &range); + + void surroundCurrentRange(const Input &input, const QString &prefix = {}); + + void upCase(const Range &range); + + void downCase(const Range &range); + + void replaceText(const Range &range, const QString &str); + + QString selectText(const Range &range) const; + void setCurrentRange(const Range &range); + Range currentRange() const { return Range(position(), anchor(), g.rangemode); } + + void yankText(const Range &range, int toregister); + + void pasteText(bool afterCursor); + + void cutSelectedText(int reg = 0); + + void joinLines(int count, bool preserveSpace = false); + + void insertNewLine(); + + bool handleInsertInEditor(const Input &input); + bool passEventToEditor( + QEvent &event, + QTextCursor & + tc); // Pass event to editor widget without filtering. Returns true if event was processed. + + // undo handling + int revision() const { return document()->availableUndoSteps(); } + void undoRedo(bool undo); + void undo(); + void redo(); + void pushUndoState(bool overwrite = true); + + // extra data for '.' + void replay(const QString &text, int repeat = 1); + void setDotCommand(const QString &cmd) { g.dotCommand = cmd; } + void setDotCommand(const QString &cmd, int n) { g.dotCommand = cmd.arg(n); } + QString visualDotCommand() const; + + // visual modes + void toggleVisualMode(VisualMode visualMode); + void leaveVisualMode(); + void saveLastVisualMode(); + + // marks + Mark mark(QChar code) const; + void setMark(QChar code, CursorPosition position); + // jump to valid mark return true if mark is valid and local + bool jumpToMark(QChar mark, bool backTickMode); + // update marks on undo/redo + void updateMarks(const Marks &newMarks); + CursorPosition markLessPosition() const { return mark('<').position(document()); } + CursorPosition markGreaterPosition() const { return mark('>').position(document()); } + + int m_targetColumn; // -1 if past end of line + int m_visualTargetColumn; // 'l' can move past eol in visual mode only + int m_targetColumnWrapped; // column in current part of wrapped line + + // auto-indent + QString tabExpand(int len) const; + Column indentation(const QString &line) const; + void insertAutomaticIndentation(bool goingDown, bool forceAutoIndent = false); + // number of autoindented characters + void handleStartOfLine(); + + // register handling + QString registerContents(int reg) const; + void setRegister(int reg, const QString &contents, RangeMode mode); + RangeMode registerRangeMode(int reg) const; + void getRegisterType(int *reg, bool *isClipboard, bool *isSelection, + bool *append = nullptr) const; + + void recordJump(int position = -1); + void jump(int distance); + + QList<QTextEdit::ExtraSelection> m_extraSelections; + QTextCursor m_searchCursor; + int m_searchStartPosition; + int m_searchFromScreenLine; + QString m_highlighted; // currently highlighted text + + bool handleExCommandHelper(ExCommand &cmd); // Returns success. + bool handleExPluginCommand(const ExCommand &cmd); // Handled by plugin? + bool handleExBangCommand(const ExCommand &cmd); + bool handleExYankDeleteCommand(const ExCommand &cmd); + bool handleExChangeCommand(const ExCommand &cmd); + bool handleExMoveCommand(const ExCommand &cmd); + bool handleExJoinCommand(const ExCommand &cmd); + bool handleExGotoCommand(const ExCommand &cmd); + bool handleExHistoryCommand(const ExCommand &cmd); + bool handleExRegisterCommand(const ExCommand &cmd); + bool handleExMapCommand(const ExCommand &cmd); + bool handleExNohlsearchCommand(const ExCommand &cmd); + bool handleExNormalCommand(const ExCommand &cmd); + bool handleExReadCommand(const ExCommand &cmd); + bool handleExUndoRedoCommand(const ExCommand &cmd); + bool handleExSetCommand(const ExCommand &cmd); + bool handleExSortCommand(const ExCommand &cmd); + bool handleExShiftCommand(const ExCommand &cmd); + bool handleExSourceCommand(const ExCommand &cmd); + bool handleExSubstituteCommand(const ExCommand &cmd); + bool handleExTabNextCommand(const ExCommand &cmd); + bool handleExTabPreviousCommand(const ExCommand &cmd); + bool handleExWriteCommand(const ExCommand &cmd); + bool handleExEchoCommand(const ExCommand &cmd); + + void setTabSize(int tabSize); + void setupCharClass(); + int charClass(QChar c, bool simple) const; + signed char m_charClass[256]; + + int m_ctrlVAccumulator; + int m_ctrlVLength; + int m_ctrlVBase; + + QTimer m_fixCursorTimer; + QTimer m_inputTimer; + + void miniBufferTextEdited(const QString &text, int cursorPos, int anchorPos); + + // Data shared among editors with same document. + struct BufferData { + QStack<State> undo; + QStack<State> redo; + State undoState; + int lastRevision = 0; + + int editBlockLevel = 0; // current level of edit blocks + bool breakEditBlock = false; // if true, joinPreviousEditBlock() starts new edit block + + QStack<CursorPosition> jumpListUndo; + QStack<CursorPosition> jumpListRedo; + + VisualMode lastVisualMode = NoVisualMode; + bool lastVisualModeInverted = false; + + Marks marks; + + // Insert state to get last inserted text. + struct InsertState { + int pos1; + int pos2; + int backspaces; + int deletes; + QSet<int> spaces; + bool insertingSpaces; + QString textBeforeCursor; + bool newLineBefore; + bool newLineAfter; + } insertState; + + QString lastInsertion; + + // If there are multiple editors with same document, + // only the handler with last focused editor can change buffer data. + QPointer<FakeVimHandler::Private> currentHandler; + }; + + using BufferDataPtr = QSharedPointer<BufferData>; + void pullOrCreateBufferData(); + BufferDataPtr m_buffer; + + // Data shared among all editors. + static struct GlobalData { + GlobalData() + : mappings() + , currentMap(&mappings) + { + commandBuffer.setPrompt(':'); + } + + // Current state. + bool passing = false; // let the core see the next event + Mode mode = CommandMode; + SubMode submode = NoSubMode; + SubSubMode subsubmode = NoSubSubMode; + Input subsubdata; + VisualMode visualMode = NoVisualMode; + Input minibufferData; + + // [count] for current command, 0 if no [count] available + int mvcount = 0; + int opcount = 0; + + MoveType movetype = MoveInclusive; + RangeMode rangemode = RangeCharMode; + bool gflag = false; // whether current command started with 'g' + + // Extra data for ';'. + Input semicolonType; // 'f', 'F', 't', 'T' + QString semicolonKey; + + // Repetition. + QString dotCommand; + + QHash<int, Register> registers; + + // All mappings. + Mappings mappings; + + // Input. + QList<Input> pendingInput; + MappingsIterator currentMap; + QStack<MappingState> mapStates; + int mapDepth = 0; + + // Command line buffers. + CommandBuffer commandBuffer; + CommandBuffer searchBuffer; + + // Current mini buffer message. + QString currentMessage; + MessageLevel currentMessageLevel = MessageInfo; + QString currentCommand; + + // Search state. + QString lastSearch; // last search expression as entered by user + QString lastNeedle; // last search expression translated with vimPatternToQtPattern() + bool lastSearchForward = false; // last search command was '/' or '*' + bool highlightsCleared = false; // ':nohlsearch' command is active until next search + bool findPending = + false; // currently searching using external tool (until editor is focused again) + + // Last substitution command. + QString lastSubstituteFlags; + QString lastSubstitutePattern; + QString lastSubstituteReplacement; + + // Global marks. + Marks marks; + + // Return to insert/replace mode after single command (<C-O>). + Mode returnToMode = CommandMode; + + // Currently recorded macro + bool isRecording = false; + QString recorded; + int currentRegister = 0; + int lastExecutedRegister = 0; + + // If empty, cx{motion} will store the range defined by {motion} here. + // If non-empty, cx{motion} replaces the {motion} with selectText(*exchangeData) + Range exchangeRange; + + bool surroundUpperCaseS; // True for yS and cS, false otherwise + QString surroundFunction; // Used for storing the function name provided to ys{motion}f + } g; + + FakeVimSettings &s = *fakeVimSettings(); +}; + +FakeVimHandler::Private::~Private() {} // Avoit the weak table warning in clang + +FakeVimHandler::Private::GlobalData FakeVimHandler::Private::g; + +FakeVimHandler::Private::Private(FakeVimHandler *parent, QWidget *widget) +{ + q = parent; + m_textedit = qobject_cast<QTextEdit *>(widget); + m_plaintextedit = qobject_cast<QPlainTextEdit *>(widget); + + init(); + + if (editor()) { + connect(EDITOR(document()), &QTextDocument::contentsChange, this, + &Private::onContentsChanged); + connect(EDITOR(document()), &QTextDocument::undoCommandAdded, this, + &Private::onUndoCommandAdded); + m_buffer->lastRevision = revision(); + } +} + +void +FakeVimHandler::Private::init() +{ + m_cursor = QTextCursor(document()); + m_cursorNeedsUpdate = true; + m_inFakeVim = false; + m_findStartPosition = -1; + m_visualBlockInsert = NoneBlockInsertMode; + m_positionPastEnd = false; + m_anchorPastEnd = false; + m_register = '"'; + m_targetColumn = 0; + m_visualTargetColumn = 0; + m_targetColumnWrapped = 0; + m_searchStartPosition = 0; + m_searchFromScreenLine = 0; + m_firstVisibleLine = 0; + m_ctrlVAccumulator = 0; + m_ctrlVLength = 0; + m_ctrlVBase = 0; + + initSingleShotTimer(&m_fixCursorTimer, 0, this, &FakeVimHandler::Private::onFixCursorTimeout); + initSingleShotTimer(&m_inputTimer, 1000, this, &FakeVimHandler::Private::onInputTimeout); + + pullOrCreateBufferData(); + setupCharClass(); +} + +void +FakeVimHandler::Private::focus() +{ + m_buffer->currentHandler = this; + + enterFakeVim(); + + stopIncrementalFind(); + if (isCommandLineMode()) { + if (g.subsubmode == SearchSubSubMode) { + setPosition(m_searchStartPosition); + scrollToLine(m_searchFromScreenLine); + } else { + leaveVisualMode(); + setPosition(qMin(position(), anchor())); + } + leaveCurrentMode(); + setTargetColumn(); + setAnchor(); + commitCursor(); + } else { + clearCurrentMode(); + } + fixExternalCursor(true); + updateHighlights(); + + leaveFakeVim(false); +} + +void +FakeVimHandler::Private::unfocus() +{ + fixExternalCursor(false); +} + +void +FakeVimHandler::Private::fixExternalCursor(bool focus) +{ + m_fixCursorTimer.stop(); + + if (isVisualCharMode() && !focus && !hasThinCursor()) { + // Select the character under thick cursor for external operations with text selection. + fixExternalCursorPosition(false); + } else if (isVisualCharMode() && focus && hasThinCursor()) { + // Fix cursor position if changing its shape. + // The fix is postponed so context menu action can be finished. + m_fixCursorTimer.start(); + } else { + updateCursorShape(); + } +} + +void +FakeVimHandler::Private::fixExternalCursorPosition(bool focus) +{ + QTextCursor tc = editorCursor(); + if (tc.anchor() < tc.position()) { + tc.movePosition(focus ? QTextCursor::Left : QTextCursor::Right, QTextCursor::KeepAnchor); + EDITOR(setTextCursor(tc)); + } + + setThinCursor(!focus); +} + +void +FakeVimHandler::Private::enterFakeVim() +{ + if (m_inFakeVim) { + qWarning("enterFakeVim() shouldn't be called recursively!"); + return; + } + + if (!m_buffer->currentHandler) + m_buffer->currentHandler = this; + + pullOrCreateBufferData(); + + m_inFakeVim = true; + + removeEventFilter(); + + pullCursor(); + + updateFirstVisibleLine(); +} + +void +FakeVimHandler::Private::leaveFakeVim(bool needUpdate) +{ + if (!m_inFakeVim) { + qWarning("enterFakeVim() not called before leaveFakeVim()!"); + return; + } + + // The command might have destroyed the editor. + if (m_textedit || m_plaintextedit) { + if (s.showMarks.value()) + updateSelection(); + + updateMiniBuffer(); + + if (needUpdate) { + // Move cursor line to middle of screen if it's not visible. + const int line = cursorLine(); + if (line < firstVisibleLine() || line > firstVisibleLine() + linesOnScreen()) + scrollToLine(qMax(0, line - linesOnScreen() / 2)); + else + scrollToLine(firstVisibleLine()); + updateScrollOffset(); + + commitCursor(); + } + + installEventFilter(); + } + + m_inFakeVim = false; +} + +void +FakeVimHandler::Private::leaveFakeVim(EventResult eventResult) +{ + leaveFakeVim(eventResult == EventHandled || eventResult == EventCancelled); +} + +bool +FakeVimHandler::Private::wantsOverride(QKeyEvent *ev) +{ + const int key = ev->key(); + const Qt::KeyboardModifiers mods = ev->modifiers(); + KEY_DEBUG("SHORTCUT OVERRIDE" << key << " PASSING: " << g.passing); + + if (key == Key_Escape) { + if (g.subsubmode == SearchSubSubMode) + return true; + // Not sure this feels good. People often hit Esc several times. + if (isNoVisualMode() && g.mode == CommandMode && g.submode == NoSubMode && + g.currentCommand.isEmpty() && g.returnToMode == CommandMode) + return false; + return true; + } + + // We are interested in overriding most Ctrl key combinations. + if (isOnlyControlModifier(mods) && !s.passControlKey.value() && + ((key >= Key_A && key <= Key_Z && key != Key_K) || key == Key_BracketLeft || + key == Key_BracketRight)) { + // Ctrl-K is special as it is the Core's default notion of Locator + if (g.passing) { + KEY_DEBUG(" PASSING CTRL KEY"); + // We get called twice on the same key + //g.passing = false; + return false; + } + KEY_DEBUG(" NOT PASSING CTRL KEY"); + return true; + } + + // Let other shortcuts trigger. + return false; +} + +EventResult +FakeVimHandler::Private::handleEvent(QKeyEvent *ev) +{ + const int key = ev->key(); + const Qt::KeyboardModifiers mods = ev->modifiers(); + + if (key == Key_Shift || key == Key_Alt || key == Key_Control || key == Key_AltGr || + key == Key_Meta) { + KEY_DEBUG("PLAIN MODIFIER"); + return EventUnhandled; + } + + if (g.passing) { + passShortcuts(false); + KEY_DEBUG("PASSING PLAIN KEY..." << ev->key() << ev->text()); + //if (input.is(',')) { // use ',,' to leave, too. + // qDebug() << "FINISHED..."; + // return EventHandled; + //} + KEY_DEBUG(" PASS TO CORE"); + return EventPassedToCore; + } + +#ifndef FAKEVIM_STANDALONE + bool inSnippetMode = false; + QMetaObject::invokeMethod(editor(), "inSnippetMode", Q_ARG(bool *, &inSnippetMode)); + + if (inSnippetMode) + return EventPassedToCore; +#endif + + // Fake "End of line" + //m_tc = m_cursor; + + //bool hasBlock = false; + //q->requestHasBlockSelection(&hasBlock); + //qDebug() << "IMPORT BLOCK 2:" << hasBlock; + + //if (0 && hasBlock) { + // (pos > anc) ? --pos : --anc; + + //if ((mods & RealControlModifier) != 0) { + // if (key >= Key_A && key <= Key_Z) + // key = shift(key); // make it lower case + // key = control(key); + //} else if (key >= Key_A && key <= Key_Z && (mods & Qt::ShiftModifier) == 0) { + // key = shift(key); + //} + + //QTC_ASSERT(g.mode == InsertMode || g.mode == ReplaceMode + // || !atBlockEnd() || block().length() <= 1, + // qDebug() << "Cursor at EOL before key handler"); + + const Input input(key, mods, ev->text()); + if (!input.isValid()) + return EventUnhandled; + + enterFakeVim(); + EventResult result = handleKey(input); + leaveFakeVim(result); + return result; +} + +void +FakeVimHandler::Private::installEventFilter() +{ + EDITOR(installEventFilter(q)); +} + +void +FakeVimHandler::Private::removeEventFilter() +{ + EDITOR(removeEventFilter(q)); +} + +void +FakeVimHandler::Private::setupWidget() +{ + m_cursorNeedsUpdate = true; + if (m_textedit) { + connect(m_textedit, &QTextEdit::cursorPositionChanged, this, + &FakeVimHandler::Private::onCursorPositionChanged, Qt::UniqueConnection); + } else { + connect(m_plaintextedit, &QPlainTextEdit::cursorPositionChanged, this, + &FakeVimHandler::Private::onCursorPositionChanged, Qt::UniqueConnection); + } + + enterFakeVim(); + + leaveCurrentMode(); + m_wasReadOnly = EDITOR(isReadOnly()); + + updateEditor(); + + leaveFakeVim(); +} + +void +FakeVimHandler::Private::commitInsertState() +{ + if (!isInsertStateValid()) + return; + + QString &lastInsertion = m_buffer->lastInsertion; + BufferData::InsertState &insertState = m_buffer->insertState; + + // Get raw inserted text. + lastInsertion = textAt(insertState.pos1, insertState.pos2); + + // Escape special characters and spaces inserted by user (not by auto-indentation). + for (int i = lastInsertion.size() - 1; i >= 0; --i) { + const int pos = insertState.pos1 + i; + const QChar c = characterAt(pos); + if (c == '<') + lastInsertion.replace(i, 1, "<LT>"); + else if ((c == ' ' || c == '\t') && insertState.spaces.contains(pos)) + lastInsertion.replace(i, 1, QLatin1String(c == ' ' ? "<SPACE>" : "<TAB>")); + } + + // Remove unnecessary backspaces. + while (insertState.backspaces > 0 && !lastInsertion.isEmpty() && lastInsertion[0].isSpace()) + --insertState.backspaces; + + // backspaces in front of inserted text + lastInsertion.prepend(QString("<BS>").repeated(insertState.backspaces)); + // deletes after inserted text + lastInsertion.prepend(QString("<DELETE>").repeated(insertState.deletes)); + + // Remove indentation. + lastInsertion.replace(QRegularExpression("(^|\n)[\\t ]+"), "\\1"); +} + +void +FakeVimHandler::Private::invalidateInsertState() +{ + BufferData::InsertState &insertState = m_buffer->insertState; + insertState.pos1 = -1; + insertState.pos2 = position(); + insertState.backspaces = 0; + insertState.deletes = 0; + insertState.spaces.clear(); + insertState.insertingSpaces = false; + insertState.textBeforeCursor = textAt(block().position(), position()); + insertState.newLineBefore = false; + insertState.newLineAfter = false; +} + +bool +FakeVimHandler::Private::isInsertStateValid() const +{ + return m_buffer->insertState.pos1 != -1; +} + +void +FakeVimHandler::Private::clearLastInsertion() +{ + invalidateInsertState(); + m_buffer->lastInsertion.clear(); + m_buffer->insertState.pos1 = m_buffer->insertState.pos2; +} + +void +FakeVimHandler::Private::ensureCursorVisible() +{ + int pos = position(); + int anc = isVisualMode() ? anchor() : position(); + + // fix selection so it is outside folded block + int start = qMin(pos, anc); + int end = qMax(pos, anc) + 1; + QTextBlock block = blockAt(start); + QTextBlock block2 = blockAt(end); + if (!block.isVisible() || !block2.isVisible()) { + // FIXME: Moving cursor left/right or unfolding block immediately after block is folded + // should restore cursor position inside block. + // Changing cursor position after folding is not Vim behavior so at least record the jump. + if (block.isValid() && !block.isVisible()) + recordJump(); + + pos = start; + while (block.isValid() && !block.isVisible()) + block = block.previous(); + if (block.isValid()) + pos = block.position() + qMin(m_targetColumn, block.length() - 2); + + if (isVisualMode()) { + anc = end; + while (block2.isValid() && !block2.isVisible()) { + anc = block2.position() + block2.length() - 2; + block2 = block2.next(); + } + } + + setAnchorAndPosition(anc, pos); + } +} + +void +FakeVimHandler::Private::updateEditor() +{ + const int tabSize = + std::clamp(static_cast<int>(s.tabStop.value()), 1, std::numeric_limits<int>::max()); + setTabSize(tabSize); + setupCharClass(); +} + +void +FakeVimHandler::Private::setTabSize(int tabSize) +{ + const int charWidth = QFontMetrics(EDITOR(font())).horizontalAdvance(' '); + const int width = charWidth * tabSize; + EDITOR(setTabStopDistance(width)); +} + +void +FakeVimHandler::Private::restoreWidget(int tabSize) +{ + //EDITOR(removeEventFilter(q)); + //EDITOR(setReadOnly(m_wasReadOnly)); + setTabSize(tabSize); + g.visualMode = NoVisualMode; + // Force "ordinary" cursor. + setThinCursor(); + updateSelection(); + updateHighlights(); + if (m_textedit) { + disconnect(m_textedit, &QTextEdit::cursorPositionChanged, this, + &FakeVimHandler::Private::onCursorPositionChanged); + } else { + disconnect(m_plaintextedit, &QPlainTextEdit::cursorPositionChanged, this, + &FakeVimHandler::Private::onCursorPositionChanged); + } +} + +EventResult +FakeVimHandler::Private::handleKey(const Input &input) +{ + KEY_DEBUG("HANDLE INPUT: " << input); + + bool hasInput = input.isValid(); + + // Waiting on input to complete mapping? + EventResult r = stopWaitForMapping(hasInput); + + if (hasInput) { + record(input); + g.pendingInput.append(input); + } + + // Process pending input. + // Note: Pending input is global state and can be extended by: + // 1. handling a user input (though handleKey() is not called recursively), + // 2. expanding a user mapping or + // 3. executing a register. + while (!g.pendingInput.isEmpty() && r == EventHandled) { + const Input in = g.pendingInput.takeFirst(); + + // invalid input is used to pop mapping state + if (!in.isValid()) { + endMapping(); + } else { + // Handle user mapping. + if (canHandleMapping()) { + if (extendMapping(in)) { + if (!hasInput || !g.currentMap.canExtend()) + expandCompleteMapping(); + } else if (!expandCompleteMapping()) { + r = handleCurrentMapAsDefault(); + } + } else { + r = handleDefaultKey(in); + } + } + } + + if (g.currentMap.canExtend()) { + waitForMapping(); + return EventHandled; + } + + if (r != EventHandled) + clearPendingInput(); + + return r; +} + +bool +FakeVimHandler::Private::handleCommandBufferPaste(const Input &input) +{ + if (input.isControl('r') && (g.subsubmode == SearchSubSubMode || g.mode == ExMode)) { + g.minibufferData = input; + return true; + } + if (g.minibufferData.isControl('r')) { + g.minibufferData = Input(); + if (input.isEscape()) + return true; + CommandBuffer &buffer = + (g.subsubmode == SearchSubSubMode) ? g.searchBuffer : g.commandBuffer; + if (input.isControl('w')) { + QTextCursor tc = m_cursor; + tc.select(QTextCursor::WordUnderCursor); + QString word = tc.selectedText(); + buffer.insertText(word); + } else { + QString r = registerContents(input.asChar().unicode()); + buffer.insertText(r); + } + updateMiniBuffer(); + return true; + } + return false; +} + +EventResult +FakeVimHandler::Private::handleDefaultKey(const Input &input) +{ + if (g.passing) { + passShortcuts(false); + QKeyEvent event(QEvent::KeyPress, input.key(), input.modifiers(), input.text()); + bool accepted = QApplication::sendEvent(editor()->window(), &event); + if (accepted || (!m_textedit && !m_plaintextedit)) + return EventHandled; + } + + if (input == Nop) + return EventHandled; + else if (g.subsubmode == SearchSubSubMode) + return handleSearchSubSubMode(input); + else if (g.mode == CommandMode) + return handleCommandMode(input); + else if (g.mode == InsertMode || g.mode == ReplaceMode) + return handleInsertOrReplaceMode(input); + else if (g.mode == ExMode) + return handleExMode(input); + return EventUnhandled; +} + +EventResult +FakeVimHandler::Private::handleCurrentMapAsDefault() +{ + // If mapping has failed take the first input from it and try default command. + const Inputs &inputs = g.currentMap.currentInputs(); + if (inputs.isEmpty()) + return EventHandled; + + Input in = inputs.front(); + if (inputs.size() > 1) + prependInputs(inputs.mid(1)); + g.currentMap.reset(); + + return handleDefaultKey(in); +} + +void +FakeVimHandler::Private::prependInputs(const QVector<Input> &inputs) +{ + for (int i = inputs.size() - 1; i >= 0; --i) + g.pendingInput.prepend(inputs[i]); +} + +void +FakeVimHandler::Private::prependMapping(const Inputs &inputs) +{ + // FIXME: Implement Vim option maxmapdepth (default value is 1000). + if (g.mapDepth >= 1000) { + const int i = qMax(0, g.pendingInput.lastIndexOf(Input())); + const QList<Input> inputsLocal = g.pendingInput.mid(i); + clearPendingInput(); + g.pendingInput.append(inputsLocal); + showMessage(MessageError, Tr::tr("Recursive mapping")); + return; + } + + ++g.mapDepth; + g.pendingInput.prepend(Input()); + prependInputs(inputs); + g.commandBuffer.setHistoryAutoSave(false); + + // start new edit block (undo/redo) only if necessary + bool editBlock = m_buffer->editBlockLevel == 0 && !(isInsertMode() && isInsertStateValid()); + if (editBlock) + beginLargeEditBlock(); + g.mapStates << MappingState(inputs.noremap(), inputs.silent(), editBlock); +} + +bool +FakeVimHandler::Private::expandCompleteMapping() +{ + if (!g.currentMap.isComplete()) + return false; + + const Inputs &inputs = g.currentMap.inputs(); + int usedInputs = g.currentMap.mapLength(); + prependInputs(g.currentMap.currentInputs().mid(usedInputs)); + prependMapping(inputs); + g.currentMap.reset(); + + return true; +} + +bool +FakeVimHandler::Private::extendMapping(const Input &input) +{ + if (!g.currentMap.isValid()) + g.currentMap.reset(currentModeCode()); + return g.currentMap.walk(input); +} + +void +FakeVimHandler::Private::endMapping() +{ + if (!g.currentMap.canExtend()) + --g.mapDepth; + if (g.mapStates.isEmpty()) + return; + if (g.mapStates.last().editBlock) + endEditBlock(); + g.mapStates.pop_back(); + if (g.mapStates.isEmpty()) + g.commandBuffer.setHistoryAutoSave(true); +} + +bool +FakeVimHandler::Private::canHandleMapping() +{ + // Don't handle user mapping in sub-modes that cannot be followed by movement and in "noremap". + return g.subsubmode == NoSubSubMode && g.submode != RegisterSubMode && + g.submode != WindowSubMode && g.submode != ZSubMode && g.submode != CapitalZSubMode && + g.submode != ReplaceSubMode && g.submode != MacroRecordSubMode && + g.submode != MacroExecuteSubMode && + (g.mapStates.isEmpty() || !g.mapStates.last().noremap); +} + +void +FakeVimHandler::Private::clearPendingInput() +{ + // Clear pending input on interrupt or bad mapping. + g.pendingInput.clear(); + g.mapStates.clear(); + g.mapDepth = 0; + + // Clear all started edit blocks. + while (m_buffer->editBlockLevel > 0) + endEditBlock(); +} + +void +FakeVimHandler::Private::waitForMapping() +{ + g.currentCommand.clear(); + foreach(const Input &input, g.currentMap.currentInputs()) + g.currentCommand.append(input.toString()); + + // wait for user to press any key or trigger complete mapping after interval + m_inputTimer.start(); +} + +EventResult +FakeVimHandler::Private::stopWaitForMapping(bool hasInput) +{ + if (!hasInput || m_inputTimer.isActive()) { + m_inputTimer.stop(); + g.currentCommand.clear(); + if (!hasInput && !expandCompleteMapping()) { + // Cannot complete mapping so handle the first input from it as default command. + return handleCurrentMapAsDefault(); + } + } + + return EventHandled; +} + +void +FakeVimHandler::Private::stopIncrementalFind() +{ + if (g.findPending) { + g.findPending = false; + setAnchorAndPosition(m_findStartPosition, m_cursor.selectionStart()); + finishMovement(); + setAnchor(); + } +} + +void +FakeVimHandler::Private::updateFind(bool isComplete) +{ + if (!isComplete && !s.incSearch.value()) + return; + + g.currentMessage.clear(); + + const QString &needle = g.searchBuffer.contents(); + if (isComplete) { + setPosition(m_searchStartPosition); + if (!needle.isEmpty()) + recordJump(); + } + + SearchData sd; + sd.needle = needle; + sd.forward = g.lastSearchForward; + sd.highlightMatches = isComplete; + search(sd, isComplete); +} + +void +FakeVimHandler::Private::resetCount() +{ + g.mvcount = 0; + g.opcount = 0; +} + +bool +FakeVimHandler::Private::isInputCount(const Input &input) const +{ + return input.isDigit() && (!input.is('0') || g.mvcount > 0); +} + +bool +FakeVimHandler::Private::atEmptyLine(int pos) const +{ + return blockAt(pos).length() == 1; +} + +bool +FakeVimHandler::Private::atEmptyLine(const QTextCursor &tc) const +{ + return atEmptyLine(tc.position()); +} + +bool +FakeVimHandler::Private::atEmptyLine() const +{ + return atEmptyLine(position()); +} + +bool +FakeVimHandler::Private::atBoundary(bool end, bool simple, bool onlyWords, + const QTextCursor &tc) const +{ + if (tc.isNull()) + return atBoundary(end, simple, onlyWords, m_cursor); + if (atEmptyLine(tc)) + return true; + int pos = tc.position(); + QChar c1 = characterAt(pos); + QChar c2 = characterAt(pos + (end ? 1 : -1)); + int thisClass = charClass(c1, simple); + return (!onlyWords || thisClass != 0) && + (c2.isNull() || c2 == QChar::ParagraphSeparator || thisClass != charClass(c2, simple)); +} + +bool +FakeVimHandler::Private::atWordBoundary(bool end, bool simple, const QTextCursor &tc) const +{ + return atBoundary(end, simple, true, tc); +} + +bool +FakeVimHandler::Private::atWordStart(bool simple, const QTextCursor &tc) const +{ + return atWordBoundary(false, simple, tc); +} + +bool +FakeVimHandler::Private::atWordEnd(bool simple, const QTextCursor &tc) const +{ + return atWordBoundary(true, simple, tc); +} + +bool +FakeVimHandler::Private::isFirstNonBlankOnLine(int pos) +{ + for (int i = blockAt(pos).position(); i < pos; ++i) { + if (!document()->characterAt(i).isSpace()) + return false; + } + return true; +} + +void +FakeVimHandler::Private::pushUndoState(bool overwrite) +{ + if (m_buffer->editBlockLevel != 0 && m_buffer->undoState.isValid()) + return; // No need to save undo state for inner edit blocks. + + if (m_buffer->undoState.isValid() && !overwrite) + return; + + UNDO_DEBUG("PUSH UNDO"); + int pos = position(); + if (!isInsertMode()) { + if (isVisualMode() || g.submode == DeleteSubMode || + (g.submode == ChangeSubMode && g.movetype != MoveLineWise)) { + pos = qMin(pos, anchor()); + if (isVisualLineMode()) + pos = firstPositionInLine(lineForPosition(pos)); + else if (isVisualBlockMode()) + pos = blockAt(pos).position() + qMin(columnAt(anchor()), columnAt(position())); + } else if (g.movetype == MoveLineWise && s.startOfLine.value()) { + QTextCursor tc = m_cursor; + if (g.submode == ShiftLeftSubMode || g.submode == ShiftRightSubMode || + g.submode == IndentSubMode) { + pos = qMin(pos, anchor()); + } + tc.setPosition(pos); + moveToFirstNonBlankOnLine(&tc); + pos = qMin(pos, tc.position()); + } + } + + CursorPosition lastChangePosition(document(), pos); + setMark('.', lastChangePosition); + + m_buffer->redo.clear(); + m_buffer->undoState = State(revision(), lastChangePosition, m_buffer->marks, + m_buffer->lastVisualMode, m_buffer->lastVisualModeInverted); +} + +void +FakeVimHandler::Private::moveDown(int n) +{ + if (n == 0) + return; + + QTextBlock block = m_cursor.block(); + const int col = position() - block.position(); + + int lines = qAbs(n); + int position = 0; + while (block.isValid()) { + position = block.position() + qMax(0, qMin(block.length() - 2, col)); + if (block.isVisible()) { + --lines; + if (lines < 0) + break; + } + block = n > 0 ? nextLine(block) : previousLine(block); + } + + setPosition(position); + moveToTargetColumn(); + updateScrollOffset(); +} + +void +FakeVimHandler::Private::moveDownVisually(int n) +{ + const QTextCursor::MoveOperation moveOperation = (n > 0) ? QTextCursor::Down : QTextCursor::Up; + int count = qAbs(n); + int oldPos = m_cursor.position(); + + while (count > 0) { + m_cursor.movePosition(moveOperation, QTextCursor::KeepAnchor, 1); + if (oldPos == m_cursor.position()) + break; + oldPos = m_cursor.position(); + QTextBlock block = m_cursor.block(); + if (block.isVisible()) + --count; + } + + QTextCursor tc = m_cursor; + tc.movePosition(QTextCursor::StartOfLine); + const int minPos = tc.position(); + moveToEndOfLineVisually(&tc); + const int maxPos = tc.position(); + + if (m_targetColumn == -1) { + setPosition(maxPos); + } else { + setPosition(qMin(maxPos, minPos + m_targetColumnWrapped)); + const int targetColumn = m_targetColumnWrapped; + setTargetColumn(); + m_targetColumnWrapped = targetColumn; + } + + if (!isInsertMode() && atEndOfLine()) + m_cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor); + + updateScrollOffset(); +} + +void +FakeVimHandler::Private::movePageDown(int count) +{ + const int scrollOffset = windowScrollOffset(); + const int screenLines = linesOnScreen(); + const int offset = count > 0 ? scrollOffset - 2 : screenLines - scrollOffset + 2; + const int value = count * screenLines - cursorLineOnScreen() + offset; + moveDown(value); + + if (count > 0) + scrollToLine(cursorLine()); + else + scrollToLine(qMax(0, cursorLine() - screenLines + 1)); +} + +void +FakeVimHandler::Private::commitCursor() +{ + QTextCursor tc = m_cursor; + + if (isVisualMode()) { + int pos = tc.position(); + int anc = tc.anchor(); + + if (isVisualBlockMode()) { + const int col1 = columnAt(anc); + const int col2 = columnAt(pos); + if (col1 > col2) + ++anc; + else if (!tc.atBlockEnd()) + ++pos; + // FIXME: After '$' command (i.e. m_visualTargetColumn == -1), end of selected lines + // should be selected. + } else if (isVisualLineMode()) { + const int posLine = lineForPosition(pos); + const int ancLine = lineForPosition(anc); + if (anc < pos) { + pos = lastPositionInLine(posLine); + anc = firstPositionInLine(ancLine); + } else { + pos = firstPositionInLine(posLine); + anc = lastPositionInLine(ancLine) + 1; + } + // putting cursor on folded line will unfold the line, so move the cursor a bit + if (!blockAt(pos).isVisible()) + ++pos; + } else if (isVisualCharMode()) { + if (anc > pos) + ++anc; + else if (!editor()->hasFocus() || isCommandLineMode()) + m_fixCursorTimer.start(); + } + + tc.setPosition(anc); + tc.setPosition(pos, QTextCursor::KeepAnchor); + } else if (g.subsubmode == SearchSubSubMode && !m_searchCursor.isNull()) { + tc = m_searchCursor; + } else { + tc.clearSelection(); + } + + updateCursorShape(); + + if (isVisualBlockMode()) { + q->requestSetBlockSelection(tc); + } else { + q->requestDisableBlockSelection(); + if (editor()) + EDITOR(setTextCursor(tc)); + } +} + +void +FakeVimHandler::Private::pullCursor() +{ + if (!m_cursorNeedsUpdate) + return; + + m_cursorNeedsUpdate = false; + + QTextCursor oldCursor = m_cursor; + + bool visualBlockMode = false; + q->requestHasBlockSelection(&visualBlockMode); + + if (visualBlockMode) + q->requestBlockSelection(&m_cursor); + else if (editor()) + m_cursor = editorCursor(); + + // Cursor should be always valid. + if (m_cursor.isNull()) + m_cursor = QTextCursor(document()); + + if (visualBlockMode) + g.visualMode = VisualBlockMode; + else if (m_cursor.hasSelection()) + g.visualMode = VisualCharMode; + else + g.visualMode = NoVisualMode; + + // Keep visually the text selection same. + // With thick text cursor, the character under cursor is treated as selected. + if (isVisualCharMode() && hasThinCursor()) + moveLeft(); + + // Cursor position can be after the end of line only in some modes. + if (atEndOfLine() && !isVisualMode() && !isInsertMode()) + moveLeft(); + + // Record external jump to different line. + if (lineForPosition(position()) != lineForPosition(oldCursor.position())) + recordJump(oldCursor.position()); + + setTargetColumn(); +} + +QTextCursor +FakeVimHandler::Private::editorCursor() const +{ + QTextCursor tc = EDITOR(textCursor()); + tc.setVisualNavigation(false); + return tc; +} + +bool +FakeVimHandler::Private::moveToNextParagraph(int count) +{ + const bool forward = count > 0; + int repeat = forward ? count : -count; + QTextBlock block = this->block(); + + if (block.isValid() && block.length() == 1) + ++repeat; + + for (; block.isValid(); block = forward ? block.next() : block.previous()) { + if (block.length() == 1) { + if (--repeat == 0) + break; + while (block.isValid() && block.length() == 1) + block = forward ? block.next() : block.previous(); + if (!block.isValid()) + break; + } + } + + if (!block.isValid()) + --repeat; + + if (repeat > 0) + return false; + + if (block.isValid()) + setPosition(block.position()); + else + setPosition(forward ? lastPositionInDocument() : 0); + + return true; +} + +void +FakeVimHandler::Private::moveToParagraphStartOrEnd(int direction) +{ + bool emptyLine = atEmptyLine(); + int oldPos = -1; + + while (atEmptyLine() == emptyLine && oldPos != position()) { + oldPos = position(); + moveDown(direction); + } + + if (oldPos != position()) + moveUp(direction); +} + +void +FakeVimHandler::Private::moveToEndOfLine() +{ + // Additionally select (in visual mode) or apply current command on hidden lines following + // the current line. + bool onlyVisibleLines = isVisualMode() || g.submode != NoSubMode; + const int id = onlyVisibleLines ? lineNumber(block()) : block().blockNumber() + 1; + setPosition(lastPositionInLine(id, onlyVisibleLines)); + setTargetColumn(); +} + +void +FakeVimHandler::Private::moveToEndOfLineVisually() +{ + moveToEndOfLineVisually(&m_cursor); + setTargetColumn(); +} + +void +FakeVimHandler::Private::moveToEndOfLineVisually(QTextCursor *tc) +{ + // Moving to end of line ends up on following line if the line is wrapped. + tc->movePosition(QTextCursor::StartOfLine); + const int minPos = tc->position(); + tc->movePosition(QTextCursor::EndOfLine); + int maxPos = tc->position(); + tc->movePosition(QTextCursor::StartOfLine); + if (minPos != tc->position()) + --maxPos; + tc->setPosition(maxPos); +} + +void +FakeVimHandler::Private::moveBehindEndOfLine() +{ + q->fold(1, false); + int pos = qMin(block().position() + block().length() - 1, lastPositionInDocument() + 1); + setPosition(pos); + setTargetColumn(); +} + +void +FakeVimHandler::Private::moveToStartOfLine() +{ + setPosition(block().position()); + setTargetColumn(); +} + +void +FakeVimHandler::Private::moveToStartOfLineVisually() +{ + m_cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor); + setTargetColumn(); +} + +void +FakeVimHandler::Private::fixSelection() +{ + if (g.rangemode == RangeBlockMode) + return; + + if (g.movetype == MoveInclusive) { + // If position or anchor is after end of non-empty line, include line break in selection. + if (characterAtCursor() == QChar::ParagraphSeparator) { + if (!atEmptyLine() && !atDocumentEnd()) { + setPosition(position() + 1); + return; + } + } else if (characterAt(anchor()) == QChar::ParagraphSeparator) { + QTextCursor tc = m_cursor; + tc.setPosition(anchor()); + if (!atEmptyLine(tc)) { + setAnchorAndPosition(anchor() + 1, position()); + return; + } + } + } + + if (g.movetype == MoveExclusive && g.subsubmode == NoSubSubMode) { + if (anchor() < position() && atBlockStart()) { + // Exclusive motion ending at the beginning of line + // becomes inclusive and end is moved to end of previous line. + g.movetype = MoveInclusive; + moveToStartOfLine(); + moveLeft(); + + // Exclusive motion ending at the beginning of line and + // starting at or before first non-blank on a line becomes linewise. + if (anchor() < block().position() && isFirstNonBlankOnLine(anchor())) + g.movetype = MoveLineWise; + } + } + + if (g.movetype == MoveLineWise) + g.rangemode = (g.submode == ChangeSubMode) ? RangeLineModeExclusive : RangeLineMode; + + if (g.movetype == MoveInclusive) { + if (anchor() <= position()) { + if (!atBlockEnd()) + setPosition(position() + 1); // correction + + // Omit first character in selection if it's line break on non-empty line. + int start = anchor(); + int end = position(); + if (afterEndOfLine(document(), start) && start > 0) { + start = qMin(start + 1, end); + if (g.submode == DeleteSubMode && !atDocumentEnd()) + setAnchorAndPosition(start, end + 1); + else + setAnchorAndPosition(start, end); + } + + // If more than one line is selected and all are selected completely + // movement becomes linewise. + if (start < block().position() && isFirstNonBlankOnLine(start) && atBlockEnd()) { + if (g.submode != ChangeSubMode) { + moveRight(); + if (atEmptyLine()) + moveRight(); + } + g.movetype = MoveLineWise; + } + } else if (!m_anchorPastEnd) { + setAnchorAndPosition(anchor() + 1, position()); + } + } + + if (m_positionPastEnd) { + moveBehindEndOfLine(); + moveRight(); + setAnchorAndPosition(anchor(), position()); + } + + if (m_anchorPastEnd) { + const int pos = position(); + setPosition(anchor()); + moveBehindEndOfLine(); + moveRight(); + setAnchorAndPosition(position(), pos); + } +} + +bool +FakeVimHandler::Private::finishSearch() +{ + if (g.lastSearch.isEmpty() || + (!g.currentMessage.isEmpty() && g.currentMessageLevel == MessageError)) { + return false; + } + if (g.submode != NoSubMode) + setAnchorAndPosition(m_searchStartPosition, position()); + return true; +} + +void +FakeVimHandler::Private::finishMovement(const QString &dotCommandMovement) +{ + //dump("FINISH MOVEMENT"); + if (g.submode == FilterSubMode) { + int beginLine = lineForPosition(anchor()); + int endLine = lineForPosition(position()); + setPosition(qMin(anchor(), position())); + enterExMode(QString(".,+%1!").arg(qAbs(endLine - beginLine))); + return; + } + + if (g.submode == ChangeSubMode || g.submode == DeleteSubMode || g.submode == CommentSubMode || + g.submode == ExchangeSubMode || g.submode == ReplaceWithRegisterSubMode || + g.submode == AddSurroundingSubMode || g.submode == YankSubMode || + g.submode == InvertCaseSubMode || g.submode == DownCaseSubMode || + g.submode == UpCaseSubMode || g.submode == IndentSubMode || g.submode == ShiftLeftSubMode || + g.submode == ShiftRightSubMode) { + fixSelection(); + + if (g.submode == ChangeSubMode || g.submode == DeleteSubMode || g.submode == YankSubMode) { + yankText(currentRange(), m_register); + } + } + + if (g.submode == ChangeSubMode) { + pushUndoState(false); + beginEditBlock(); + removeText(currentRange()); + if (g.movetype == MoveLineWise) + insertAutomaticIndentation(true); + endEditBlock(); + setTargetColumn(); + } else if (g.submode == CommentSubMode) { + pushUndoState(false); + beginEditBlock(); + toggleComment(currentRange()); + endEditBlock(); + } else if (g.submode == AddSurroundingSubMode) { + g.subsubmode = SurroundSubSubMode; + g.dotCommand = dotCommandMovement; + + // We now only know the region that should be surrounded, but not the actual + // character that should surround it. We thus do NOT want to finish the + // movement yet here, so we return early. + // The next character entered will be used by the SurroundSubSubMode. + return; + } else if (g.submode == ExchangeSubMode) { + exchangeRange(currentRange()); + } else if (g.submode == ReplaceWithRegisterSubMode && s.emulateReplaceWithRegister.value()) { + pushUndoState(false); + beginEditBlock(); + replaceWithRegister(currentRange()); + endEditBlock(); + } else if (g.submode == DeleteSubMode) { + pushUndoState(false); + beginEditBlock(); + const int pos = position(); + // Always delete something (e.g. 'dw' on an empty line deletes the line). + if (pos == anchor() && g.movetype == MoveInclusive) + removeText(Range(pos, pos + 1)); + else + removeText(currentRange()); + if (g.movetype == MoveLineWise) + handleStartOfLine(); + endEditBlock(); + } else if (g.submode == YankSubMode) { + bool isVisualModeYank = isVisualMode(); + leaveVisualMode(); + const QTextCursor tc = m_cursor; + if (g.rangemode == RangeBlockMode) { + const int pos1 = tc.block().position(); + const int pos2 = blockAt(tc.anchor()).position(); + const int col = qMin(tc.position() - pos1, tc.anchor() - pos2); + setPosition(qMin(pos1, pos2) + col); + } else { + setPosition(qMin(position(), anchor())); + if (g.rangemode == RangeLineMode) { + if (isVisualModeYank) + moveToStartOfLine(); + else + moveToTargetColumn(); + } + } + setTargetColumn(); + } else if (g.submode == InvertCaseSubMode || g.submode == UpCaseSubMode || + g.submode == DownCaseSubMode) { + beginEditBlock(); + if (g.submode == InvertCaseSubMode) + invertCase(currentRange()); + else if (g.submode == DownCaseSubMode) + downCase(currentRange()); + else if (g.submode == UpCaseSubMode) + upCase(currentRange()); + if (g.movetype == MoveLineWise) + handleStartOfLine(); + endEditBlock(); + } else if (g.submode == IndentSubMode || g.submode == ShiftRightSubMode || + g.submode == ShiftLeftSubMode) { + recordJump(); + pushUndoState(false); + if (g.submode == IndentSubMode) + indentSelectedText(); + else if (g.submode == ShiftRightSubMode) + shiftRegionRight(1); + else if (g.submode == ShiftLeftSubMode) + shiftRegionLeft(1); + } + + if (!dotCommandMovement.isEmpty()) { + QString dotCommand = dotCommandFromSubMode(g.submode); + if (!dotCommand.isEmpty()) { + if (g.submode == ReplaceWithRegisterSubMode) + dotCommand = QString("\"%1%2").arg(QChar(m_register)).arg(dotCommand); + + setDotCommand(dotCommand + dotCommandMovement); + } + } + + // Change command continues in insert mode. + if (g.submode == ChangeSubMode) { + clearCurrentMode(); + enterInsertMode(); + } else { + leaveCurrentMode(); + } +} + +void +FakeVimHandler::Private::leaveCurrentMode() +{ + if (isVisualMode()) + enterCommandMode(g.returnToMode); + else if (g.returnToMode == CommandMode) + enterCommandMode(); + else if (g.returnToMode == InsertMode) + enterInsertMode(); + else + enterReplaceMode(); + + if (isNoVisualMode()) + setAnchor(); +} + +void +FakeVimHandler::Private::clearCurrentMode() +{ + g.submode = NoSubMode; + g.subsubmode = NoSubSubMode; + g.movetype = MoveInclusive; + g.gflag = false; + g.surroundUpperCaseS = false; + g.surroundFunction.clear(); + m_register = '"'; + g.rangemode = RangeCharMode; + g.currentCommand.clear(); + resetCount(); +} + +void +FakeVimHandler::Private::updateSelection() +{ + QList<QTextEdit::ExtraSelection> selections = m_extraSelections; + if (s.showMarks.value()) { + for (auto it = m_buffer->marks.cbegin(), end = m_buffer->marks.cend(); it != end; ++it) { + QTextEdit::ExtraSelection sel; + sel.cursor = m_cursor; + setCursorPosition(&sel.cursor, it.value().position(document())); + sel.cursor.setPosition(sel.cursor.position(), QTextCursor::MoveAnchor); + sel.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); + sel.format = m_cursor.blockCharFormat(); + sel.format.setForeground(Qt::blue); + sel.format.setBackground(Qt::green); + selections.append(sel); + } + } + //qDebug() << "SELECTION: " << selections; + q->selectionChanged(selections); +} + +void +FakeVimHandler::Private::updateHighlights() +{ + if (s.useCoreSearch.value() || !s.hlSearch.value() || g.highlightsCleared) { + if (m_highlighted.isEmpty()) + return; + m_highlighted.clear(); + } else if (m_highlighted != g.lastNeedle) { + m_highlighted = g.lastNeedle; + } else { + return; + } + + q->highlightMatches(m_highlighted); +} + +void +FakeVimHandler::Private::updateMiniBuffer() +{ + if (!m_textedit && !m_plaintextedit) + return; + + QString msg; + int cursorPos = -1; + int anchorPos = -1; + MessageLevel messageLevel = MessageMode; + + if (!g.mapStates.isEmpty() && g.mapStates.last().silent && g.currentMessageLevel < MessageInfo) + g.currentMessage.clear(); + + if (g.passing) { + msg = "PASSING"; + } else if (g.subsubmode == SearchSubSubMode) { + msg = g.searchBuffer.display(); + if (g.mapStates.isEmpty()) { + cursorPos = g.searchBuffer.cursorPos() + 1; + anchorPos = g.searchBuffer.anchorPos() + 1; + } + } else if (g.mode == ExMode) { + msg = g.commandBuffer.display(); + if (g.mapStates.isEmpty()) { + cursorPos = g.commandBuffer.cursorPos() + 1; + anchorPos = g.commandBuffer.anchorPos() + 1; + } + } else if (!g.currentMessage.isEmpty()) { + msg = g.currentMessage; + g.currentMessage.clear(); + messageLevel = g.currentMessageLevel; + } else if (!g.mapStates.isEmpty() && !g.mapStates.last().silent) { + // Do not reset previous message when after running a mapped command. + return; + } else if (g.mode == CommandMode && !g.currentCommand.isEmpty() && s.showCmd.value()) { + msg = g.currentCommand; + messageLevel = MessageShowCmd; + } else if (g.mode == CommandMode && isVisualMode()) { + if (isVisualCharMode()) + msg = "-- VISUAL --"; + else if (isVisualLineMode()) + msg = "-- VISUAL LINE --"; + else if (isVisualBlockMode()) + msg = "VISUAL BLOCK"; + } else if (g.mode == InsertMode) { + msg = "-- INSERT --"; + if (g.submode == CtrlRSubMode) + msg += " ^R"; + else if (g.submode == CtrlVSubMode) + msg += " ^V"; + } else if (g.mode == ReplaceMode) { + msg = "-- REPLACE --"; + } else { + if (g.returnToMode == CommandMode) + msg = "-- COMMAND --"; + else if (g.returnToMode == InsertMode) + msg = "-- (insert) --"; + else + msg = "-- (replace) --"; + } + + if (g.isRecording && msg.startsWith("--")) + msg.append(' ').append("Recording"); + + q->commandBufferChanged(msg, cursorPos, anchorPos, messageLevel); + + int linesInDoc = linesInDocument(); + int l = cursorLine(); + QString status; + const QString pos = QString("%1,%2").arg(l + 1).arg(physicalCursorColumn() + 1); + // FIXME: physical "-" logical + if (linesInDoc != 0) + status = Tr::tr("%1%2%").arg(pos, -10).arg(l * 100 / linesInDoc, 4); + else + status = Tr::tr("%1All").arg(pos, -10); + q->statusDataChanged(status); +} + +void +FakeVimHandler::Private::showMessage(MessageLevel level, const QString &msg) +{ + //qDebug() << "MSG: " << msg; + g.currentMessage = msg; + g.currentMessageLevel = level; +} + +void +FakeVimHandler::Private::notImplementedYet() +{ + qDebug() << "Not implemented in FakeVim"; + showMessage(MessageError, Tr::tr("Not implemented in FakeVim.")); +} + +void +FakeVimHandler::Private::passShortcuts(bool enable) +{ + g.passing = enable; + updateMiniBuffer(); + if (enable) + QCoreApplication::instance()->installEventFilter(q); + else + QCoreApplication::instance()->removeEventFilter(q); +} + +bool +FakeVimHandler::Private::handleCommandSubSubMode(const Input &input) +{ + bool handled = true; + + if (g.subsubmode == FtSubSubMode) { + g.semicolonType = g.subsubdata; + g.semicolonKey = input.text(); + handled = handleFfTt(g.semicolonKey); + g.subsubmode = NoSubSubMode; + if (handled) { + finishMovement( + QString("%1%2%3").arg(count()).arg(g.semicolonType.text()).arg(g.semicolonKey)); + } + } else if (g.subsubmode == TextObjectSubSubMode) { + // vim-surround treats aw and aW the same as iw and iW, respectively + if ((input.is('w') || input.is('W')) && g.submode == AddSurroundingSubMode && + g.subsubdata.is('a')) + g.subsubdata = Input('i'); + + if (input.is('w')) + selectWordTextObject(g.subsubdata.is('i')); + else if (input.is('W')) + selectWORDTextObject(g.subsubdata.is('i')); + else if (input.is('s')) + selectSentenceTextObject(g.subsubdata.is('i')); + else if (input.is('p')) + selectParagraphTextObject(g.subsubdata.is('i')); + else if (input.is('[') || input.is(']')) + handled = selectBlockTextObject(g.subsubdata.is('i'), '[', ']'); + else if (input.is('(') || input.is(')') || input.is('b')) + handled = selectBlockTextObject(g.subsubdata.is('i'), '(', ')'); + else if (input.is('<') || input.is('>')) + handled = selectBlockTextObject(g.subsubdata.is('i'), '<', '>'); + else if (input.is('{') || input.is('}') || input.is('B')) + handled = selectBlockTextObject(g.subsubdata.is('i'), '{', '}'); + else if (input.is('"') || input.is('\'') || input.is('`')) + handled = selectQuotedStringTextObject(g.subsubdata.is('i'), input.asChar()); + else if (input.is('a') && s.emulateArgTextObj.value()) + handled = selectArgumentTextObject(g.subsubdata.is('i')); + else + handled = false; + g.subsubmode = NoSubSubMode; + if (handled) { + finishMovement( + QString("%1%2%3").arg(count()).arg(g.subsubdata.text()).arg(input.text())); + } + } else if (g.subsubmode == MarkSubSubMode) { + setMark(input.asChar(), CursorPosition(m_cursor)); + g.subsubmode = NoSubSubMode; + } else if (g.subsubmode == BackTickSubSubMode || g.subsubmode == TickSubSubMode) { + handled = jumpToMark(input.asChar(), g.subsubmode == BackTickSubSubMode); + if (handled) + finishMovement(); + g.subsubmode = NoSubSubMode; + } else if (g.subsubmode == ZSubSubMode) { + handled = false; + if (input.is('j') || input.is('k')) { + int pos = position(); + q->foldGoTo(input.is('j') ? count() : -count(), false); + if (pos != position()) { + handled = true; + finishMovement(QString("%1z%2").arg(count()).arg(input.text())); + } + } + } else if (g.subsubmode == OpenSquareSubSubMode || g.subsubmode == CloseSquareSubSubMode) { + int pos = position(); + if (input.is('{') && g.subsubmode == OpenSquareSubSubMode) + searchBalanced(false, '{', '}'); + else if (input.is('}') && g.subsubmode == CloseSquareSubSubMode) + searchBalanced(true, '}', '{'); + else if (input.is('(') && g.subsubmode == OpenSquareSubSubMode) + searchBalanced(false, '(', ')'); + else if (input.is(')') && g.subsubmode == CloseSquareSubSubMode) + searchBalanced(true, ')', '('); + else if (input.is('[') && g.subsubmode == OpenSquareSubSubMode) + bracketSearchBackward(&m_cursor, "^\\{", count()); + else if (input.is('[') && g.subsubmode == CloseSquareSubSubMode) + bracketSearchForward(&m_cursor, "^\\}", count(), false); + else if (input.is(']') && g.subsubmode == OpenSquareSubSubMode) + bracketSearchBackward(&m_cursor, "^\\}", count()); + else if (input.is(']') && g.subsubmode == CloseSquareSubSubMode) + bracketSearchForward(&m_cursor, "^\\{", count(), g.submode != NoSubMode); + else if (input.is('z')) + q->foldGoTo(g.subsubmode == OpenSquareSubSubMode ? -count() : count(), true); + handled = pos != position(); + if (handled) { + if (lineForPosition(pos) != lineForPosition(position())) + recordJump(pos); + finishMovement(QString("%1%2%3") + .arg(count()) + .arg(g.subsubmode == OpenSquareSubSubMode ? '[' : ']') + .arg(input.text())); + } + } else if (g.subsubmode == SurroundWithFunctionSubSubMode) { + if (input.isReturn()) { + pushUndoState(false); + beginEditBlock(); + + const QString dotCommand = "ys" + g.dotCommand + "f" + g.surroundFunction + "<CR>"; + + surroundCurrentRange(Input(')'), g.surroundFunction); + + g.dotCommand = dotCommand; + + endEditBlock(); + leaveCurrentMode(); + } else { + g.surroundFunction += input.asChar(); + } + return true; + } else if (g.subsubmode == SurroundSubSubMode) { + if (input.is('f') && g.submode == AddSurroundingSubMode) { + g.subsubmode = SurroundWithFunctionSubSubMode; + g.commandBuffer.setContents(""); + return true; + } + + pushUndoState(false); + beginEditBlock(); + + surroundCurrentRange(input); + + endEditBlock(); + leaveCurrentMode(); + } else { + handled = false; + } + return handled; +} + +bool +FakeVimHandler::Private::handleCount(const Input &input) +{ + if (!isInputCount(input)) + return false; + g.mvcount = g.mvcount * 10 + input.text().toInt(); + return true; +} + +bool +FakeVimHandler::Private::handleMovement(const Input &input) +{ + bool handled = true; + int count = this->count(); + + if (handleCount(input)) { + return true; + } else if (input.is('0')) { + g.movetype = MoveExclusive; + if (g.gflag) + moveToStartOfLineVisually(); + else + moveToStartOfLine(); + count = 1; + } else if (input.is('a') || input.is('i')) { + g.subsubmode = TextObjectSubSubMode; + g.subsubdata = input; + } else if (input.is('^') || input.is('_')) { + if (g.gflag) + moveToFirstNonBlankOnLineVisually(); + else + moveToFirstNonBlankOnLine(); + g.movetype = MoveExclusive; +#if 0 + } + else if (false && input.is(',')) { + // FIXME: fakevim uses ',' by itself, so it is incompatible + g.subsubmode = FtSubSubMode; + // HACK: toggle 'f' <-> 'F', 't' <-> 'T' + //g.subsubdata = g.semicolonType ^ 32; + handleFfTt(g.semicolonKey, true); + g.subsubmode = NoSubSubMode; +#endif + } else if (input.is(';')) { + g.subsubmode = FtSubSubMode; + g.subsubdata = g.semicolonType; + handleFfTt(g.semicolonKey, true); + g.subsubmode = NoSubSubMode; + } else if (input.is('/') || input.is('?')) { + g.lastSearchForward = input.is('/'); + if (s.useCoreSearch.value()) { + // re-use the core dialog. + g.findPending = true; + m_findStartPosition = position(); + g.movetype = MoveExclusive; + setAnchor(); // clear selection: otherwise, search is restricted to selection + q->findRequested(!g.lastSearchForward); + } else { + // FIXME: make core find dialog sufficiently flexible to + // produce the "default vi" behaviour too. For now, roll our own. + g.currentMessage.clear(); + g.movetype = MoveExclusive; + g.subsubmode = SearchSubSubMode; + g.searchBuffer.setPrompt(g.lastSearchForward ? '/' : '?'); + m_searchStartPosition = position(); + m_searchFromScreenLine = firstVisibleLine(); + m_searchCursor = QTextCursor(); + g.searchBuffer.clear(); + } + } else if (input.is('`')) { + g.subsubmode = BackTickSubSubMode; + } else if (input.is('#') || input.is('*')) { + // FIXME: That's not proper vim behaviour + QString needle; + QTextCursor tc = m_cursor; + tc.select(QTextCursor::WordUnderCursor); + needle = QRegularExpression::escape(tc.selection().toPlainText()); + if (!g.gflag) { + needle.prepend("\\<"); + needle.append("\\>"); + } + setAnchorAndPosition(tc.position(), tc.anchor()); + g.searchBuffer.historyPush(needle); + g.lastSearch = needle; + g.lastSearchForward = input.is('*'); + handled = searchNext(); + } else if (input.is('\'')) { + g.subsubmode = TickSubSubMode; + if (g.submode != NoSubMode) + g.movetype = MoveLineWise; + } else if (input.is('|')) { + moveToStartOfLine(); + const int column = count - 1; + moveRight(qMin(column, rightDist() - 1)); + m_targetColumn = column; + m_visualTargetColumn = column; + } else if (input.is('{') || input.is('}')) { + const int oldPosition = position(); + handled = input.is('}') ? moveToNextParagraph(count) : moveToPreviousParagraph(count); + if (handled) { + recordJump(oldPosition); + setTargetColumn(); + g.movetype = MoveExclusive; + } + } else if (input.isReturn()) { + moveToStartOfLine(); + moveDown(); + moveToFirstNonBlankOnLine(); + } else if (input.is('-')) { + moveToStartOfLine(); + moveUp(count); + moveToFirstNonBlankOnLine(); + } else if (input.is('+')) { + moveToStartOfLine(); + moveDown(count); + moveToFirstNonBlankOnLine(); + } else if (input.isKey(Key_Home)) { + moveToStartOfLine(); + } else if (input.is('$') || input.isKey(Key_End)) { + if (g.gflag) { + if (count > 1) + moveDownVisually(count - 1); + moveToEndOfLineVisually(); + } else { + if (count > 1) + moveDown(count - 1); + moveToEndOfLine(); + } + g.movetype = atEmptyLine() ? MoveExclusive : MoveInclusive; + if (g.submode == NoSubMode) + m_targetColumn = -1; + if (isVisualMode()) + m_visualTargetColumn = -1; + } else if (input.is('%')) { + recordJump(); + if (g.mvcount == 0) { + moveToMatchingParanthesis(); + g.movetype = MoveInclusive; + } else { + // set cursor position in percentage - formula taken from Vim help + setPosition(firstPositionInLine((count * linesInDocument() + 99) / 100)); + moveToTargetColumn(); + handleStartOfLine(); + g.movetype = MoveLineWise; + } + } else if (input.is('b') || input.isShift(Key_Left)) { + moveToNextWordStart(count, false, false); + } else if (input.is('B') || input.isControl(Key_Left)) { + moveToNextWordStart(count, true, false); + } else if (input.is('e') && g.gflag) { + moveToNextWordEnd(count, false, false); + } else if (input.is('e')) { + moveToNextWordEnd(count, false, true, false); + } else if (input.is('E') && g.gflag) { + moveToNextWordEnd(count, true, false); + } else if (input.is('E')) { + moveToNextWordEnd(count, true, true, false); + } else if (input.isControl('e')) { + // FIXME: this should use the "scroll" option, and "count" + if (cursorLineOnScreen() == 0) + moveDown(1); + scrollDown(1); + } else if (input.is('f')) { + g.subsubmode = FtSubSubMode; + g.movetype = MoveInclusive; + g.subsubdata = input; + } else if (input.is('F')) { + g.subsubmode = FtSubSubMode; + g.movetype = MoveExclusive; + g.subsubdata = input; + } else if (!g.gflag && input.is('g')) { + g.gflag = true; + return true; + } else if (input.is('g') || input.is('G')) { + QString dotCommand = QString("%1G").arg(count); + recordJump(); + if (input.is('G') && g.mvcount == 0) + dotCommand = "G"; + int n = (input.is('g')) ? 1 : linesInDocument(); + n = g.mvcount == 0 ? n : count; + if (g.submode == NoSubMode || g.submode == ZSubMode || g.submode == CapitalZSubMode || + g.submode == RegisterSubMode) { + setPosition(firstPositionInLine(n, false)); + handleStartOfLine(); + } else { + g.movetype = MoveLineWise; + g.rangemode = RangeLineMode; + setAnchor(); + setPosition(firstPositionInLine(n, false)); + } + setTargetColumn(); + updateScrollOffset(); + } else if (input.is('h') || input.isKey(Key_Left) || input.isBackspace()) { + g.movetype = MoveExclusive; + int n = qMin(count, leftDist()); + moveLeft(n); + } else if (input.is('H')) { + const CursorPosition pos(lineToBlockNumber(lineOnTop(count)), 0); + setCursorPosition(&m_cursor, pos); + handleStartOfLine(); + } else if (input.is('j') || input.isKey(Key_Down) || input.isControl('j') || + input.isControl('n')) { + moveVertically(count); + } else if (input.is('k') || input.isKey(Key_Up) || input.isControl('p')) { + moveVertically(-count); + } else if (input.is('l') || input.isKey(Key_Right) || input.is(' ')) { + g.movetype = MoveExclusive; + moveRight(qMax(0, qMin(count, rightDist() - (g.submode == NoSubMode)))); + } else if (input.is('L')) { + const CursorPosition pos(lineToBlockNumber(lineOnBottom(count)), 0); + setCursorPosition(&m_cursor, pos); + handleStartOfLine(); + } else if (g.gflag && input.is('m')) { + const QPoint pos(EDITOR(viewport()->width()) / 2, EDITOR(cursorRect(m_cursor)).y()); + QTextCursor tc = EDITOR(cursorForPosition(pos)); + if (!tc.isNull()) { + m_cursor = tc; + setTargetColumn(); + } + } else if (input.is('M')) { + m_cursor = EDITOR(cursorForPosition(QPoint(0, EDITOR(height()) / 2))); + handleStartOfLine(); + } else if (input.is('n') || input.is('N')) { + if (s.useCoreSearch.value()) { + bool forward = (input.is('n')) ? g.lastSearchForward : !g.lastSearchForward; + int pos = position(); + q->findNextRequested(!forward); + if (forward && pos == m_cursor.selectionStart()) { + // if cursor is already positioned at the start of a find result, this is returned + q->findNextRequested(false); + } + setPosition(m_cursor.selectionStart()); + } else { + handled = searchNext(input.is('n')); + } + } else if (input.is('t')) { + g.movetype = MoveInclusive; + g.subsubmode = FtSubSubMode; + g.subsubdata = input; + } else if (input.is('T')) { + g.movetype = MoveExclusive; + g.subsubmode = FtSubSubMode; + g.subsubdata = input; + } else if (input.is('w') || input.is('W') || input.isShift(Key_Right) || + input.isControl(Key_Right)) { // tested + // Special case: "cw" and "cW" work the same as "ce" and "cE" if the + // cursor is on a non-blank - except if the cursor is on the last + // character of a word: only the current word will be changed + bool simple = input.is('W') || input.isControl(Key_Right); + if (g.submode == ChangeSubMode && !characterAtCursor().isSpace()) { + moveToWordEnd(count, simple, true); + } else { + moveToNextWordStart(count, simple, true); + // Command 'dw' deletes to the next word on the same line or to end of line. + if (g.submode == DeleteSubMode && count == 1) { + const QTextBlock currentBlock = blockAt(anchor()); + setPosition(qMin(position(), currentBlock.position() + currentBlock.length())); + } + } + } else if (input.is('z')) { + g.movetype = MoveLineWise; + g.subsubmode = ZSubSubMode; + } else if (input.is('[')) { + g.subsubmode = OpenSquareSubSubMode; + } else if (input.is(']')) { + g.subsubmode = CloseSquareSubSubMode; + } else if (input.isKey(Key_PageDown) || input.isControl('f')) { + movePageDown(count); + handleStartOfLine(); + } else if (input.isKey(Key_PageUp) || input.isControl('b')) { + movePageUp(count); + handleStartOfLine(); + } else { + handled = false; + } + + if (handled && g.subsubmode == NoSubSubMode) { + if (g.submode == NoSubMode) { + leaveCurrentMode(); + } else { + // finish movement for sub modes + const QString dotMovement = (count > 1 ? QString::number(count) : QString()) + + QLatin1String(g.gflag ? "g" : "") + input.toString(); + finishMovement(dotMovement); + setTargetColumn(); + } + } + + return handled; +} + +EventResult +FakeVimHandler::Private::handleCommandMode(const Input &input) +{ + bool handled = false; + + bool clearGflag = g.gflag; + bool clearRegister = g.submode != RegisterSubMode; + bool clearCount = g.submode != RegisterSubMode && !isInputCount(input); + + // Process input for a sub-mode. + if (input.isEscape()) { + handled = handleEscape(); + } else if (m_wasReadOnly) { + return EventUnhandled; + } else if (g.subsubmode != NoSubSubMode) { + handled = handleCommandSubSubMode(input); + } else if (g.submode == NoSubMode) { + handled = handleNoSubMode(input); + } else if (g.submode == ExchangeSubMode) { + handled = handleExchangeSubMode(input); + } else if (g.submode == ChangeSubMode && input.is('x') && s.emulateExchange.value()) { + // Exchange submode is "cx", so we need to switch over from ChangeSubMode here + g.submode = ExchangeSubMode; + handled = true; + } else if (g.submode == DeleteSurroundingSubMode || g.submode == ChangeSurroundingSubMode) { + handled = handleDeleteChangeSurroundingSubMode(input); + } else if (g.submode == AddSurroundingSubMode) { + handled = handleAddSurroundingSubMode(input); + } else if (g.submode == ChangeSubMode && (input.is('s') || input.is('S')) && + s.emulateSurround.value()) { + g.submode = ChangeSurroundingSubMode; + g.surroundUpperCaseS = input.is('S'); + handled = true; + } else if (g.submode == DeleteSubMode && input.is('s') && s.emulateSurround.value()) { + g.submode = DeleteSurroundingSubMode; + handled = true; + } else if (g.submode == YankSubMode && (input.is('s') || input.is('S')) && + s.emulateSurround.value()) { + g.submode = AddSurroundingSubMode; + g.movetype = MoveInclusive; + g.surroundUpperCaseS = input.is('S'); + handled = true; + } else if (g.submode == ChangeSubMode || g.submode == DeleteSubMode || + g.submode == YankSubMode) { + handled = handleChangeDeleteYankSubModes(input); + } else if (g.submode == CommentSubMode && s.emulateVimCommentary.value()) { + handled = handleCommentSubMode(input); + } else if (g.submode == ReplaceWithRegisterSubMode && s.emulateReplaceWithRegister.value()) { + handled = handleReplaceWithRegisterSubMode(input); + } else if (g.submode == ReplaceSubMode) { + handled = handleReplaceSubMode(input); + } else if (g.submode == FilterSubMode) { + handled = handleFilterSubMode(input); + } else if (g.submode == RegisterSubMode) { + handled = handleRegisterSubMode(input); + } else if (g.submode == WindowSubMode) { + handled = handleWindowSubMode(input); + } else if (g.submode == ZSubMode) { + handled = handleZSubMode(input); + } else if (g.submode == CapitalZSubMode) { + handled = handleCapitalZSubMode(input); + } else if (g.submode == MacroRecordSubMode) { + handled = handleMacroRecordSubMode(input); + } else if (g.submode == MacroExecuteSubMode) { + handled = handleMacroExecuteSubMode(input); + } else if (g.submode == ShiftLeftSubMode || g.submode == ShiftRightSubMode || + g.submode == IndentSubMode) { + handled = handleShiftSubMode(input); + } else if (g.submode == InvertCaseSubMode || g.submode == DownCaseSubMode || + g.submode == UpCaseSubMode) { + handled = handleChangeCaseSubMode(input); + } + + if (!handled && isOperatorPending()) + handled = handleMovement(input); + + // Clear state and display incomplete command if necessary. + if (handled) { + bool noMode = + (g.mode == CommandMode && g.submode == NoSubMode && g.subsubmode == NoSubSubMode); + clearCount = clearCount && noMode && !g.gflag; + if (clearCount && clearRegister) { + leaveCurrentMode(); + } else { + // Use gflag only for next input. + if (clearGflag) + g.gflag = false; + // Clear [count] and [register] if its no longer needed. + if (clearCount) + resetCount(); + // Show or clear current command on minibuffer (showcmd). + if (input.isEscape() || g.mode != CommandMode || clearCount) + g.currentCommand.clear(); + else + g.currentCommand.append(input.toString()); + } + + saveLastVisualMode(); + } else { + leaveCurrentMode(); + //qDebug() << "IGNORED IN COMMAND MODE: " << key << text + // << " VISUAL: " << g.visualMode; + + // if a key which produces text was pressed, don't mark it as unhandled + // - otherwise the text would be inserted while being in command mode + if (input.text().isEmpty()) + handled = false; + } + + m_positionPastEnd = (m_visualTargetColumn == -1) && isVisualMode() && !atEmptyLine(); + + return handled ? EventHandled : EventCancelled; +} + +bool +FakeVimHandler::Private::handleEscape() +{ + if (isVisualMode()) + leaveVisualMode(); + leaveCurrentMode(); + return true; +} + +bool +FakeVimHandler::Private::handleNoSubMode(const Input &input) +{ + bool handled = true; + + const int oldRevision = revision(); + QString dotCommand = visualDotCommand() + QLatin1String(g.gflag ? "g" : "") + + QString::number(count()) + input.toString(); + + if (input.is('&')) { + handleExCommand(QLatin1String(g.gflag ? "%s//~/&" : "s")); + } else if (input.is(':')) { + enterExMode(); + } else if (input.is('!') && isNoVisualMode()) { + g.submode = FilterSubMode; + } else if (input.is('!') && isVisualMode()) { + enterExMode(QString("!")); + } else if (input.is('"')) { + g.submode = RegisterSubMode; + } else if (input.is(',')) { + passShortcuts(true); + } else if (input.is('.')) { + //qDebug() << "REPEATING" << quoteUnprintable(g.dotCommand) << count() + // << input; + dotCommand.clear(); + QString savedCommand = g.dotCommand; + g.dotCommand.clear(); + beginLargeEditBlock(); + replay(savedCommand); + endEditBlock(); + leaveCurrentMode(); + g.dotCommand = savedCommand; + } else if (input.is('<') || input.is('>') || input.is('=')) { + g.submode = indentModeFromInput(input); + if (isVisualMode()) { + leaveVisualMode(); + const int repeat = count(); + if (g.submode == ShiftLeftSubMode) + shiftRegionLeft(repeat); + else if (g.submode == ShiftRightSubMode) + shiftRegionRight(repeat); + else + indentSelectedText(); + g.submode = NoSubMode; + } else { + setAnchor(); + } + } else if ((!isVisualMode() && input.is('a')) || (isVisualMode() && input.is('A'))) { + if (isVisualMode()) { + if (!isVisualBlockMode()) + dotCommand = QString::number(count()) + "a"; + enterVisualInsertMode('A'); + } else { + moveRight(qMin(rightDist(), 1)); + breakEditBlock(); + enterInsertMode(); + } + } else if (input.is('A')) { + breakEditBlock(); + moveBehindEndOfLine(); + setAnchor(); + enterInsertMode(); + setTargetColumn(); + } else if (input.isControl('a')) { + changeNumberTextObject(count()); + } else if (g.gflag && input.is('c') && s.emulateVimCommentary.value()) { + if (isVisualMode()) { + pushUndoState(); + + QTextCursor start(m_cursor); + QTextCursor end(start); + end.setPosition(end.anchor()); + + const int count = qAbs(start.blockNumber() - end.blockNumber()); + if (count == 0) { + dotCommand = "gcc"; + } else { + dotCommand = QString("gc%1j").arg(count); + } + + leaveVisualMode(); + toggleComment(currentRange()); + + g.submode = NoSubMode; + } else { + g.movetype = MoveLineWise; + g.submode = CommentSubMode; + pushUndoState(); + setAnchor(); + } + } else if (g.gflag && input.is('r') && s.emulateReplaceWithRegister.value()) { + g.submode = ReplaceWithRegisterSubMode; + if (isVisualMode()) { + dotCommand = visualDotCommand() + QString::number(count()) + "gr"; + pasteText(true); + } else { + setAnchor(); + } + } else if ((input.is('c') || input.is('d') || input.is('y')) && isNoVisualMode()) { + setAnchor(); + g.opcount = g.mvcount; + g.mvcount = 0; + g.rangemode = RangeCharMode; + g.movetype = MoveExclusive; + g.submode = changeDeleteYankModeFromInput(input); + } else if ((input.is('c') || input.is('C') || input.is('s') || input.is('R')) && + (isVisualCharMode() || isVisualLineMode())) { + leaveVisualMode(); + g.submode = ChangeSubMode; + finishMovement(); + } else if ((input.is('c') || input.is('s')) && isVisualBlockMode()) { + resetCount(); + enterVisualInsertMode(input.asChar()); + } else if (input.is('C')) { + handleAs("%1c$"); + } else if (input.isControl('c')) { + if (isNoVisualMode()) { +#if defined(Q_OS_MACOS) + showMessage(MessageInfo, + Tr::tr("Type Meta-Shift-Y, Meta-Shift-Y to quit FakeVim mode.")); +#else + showMessage(MessageInfo, Tr::tr("Type Alt-Y, Alt-Y to quit FakeVim mode.")); +#endif + } else { + leaveVisualMode(); + } + } else if ((input.is('d') || input.is('x') || input.isKey(Key_Delete)) && isVisualMode()) { + cutSelectedText(); + } else if (input.is('D') && isNoVisualMode()) { + handleAs("%1d$"); + } else if ((input.is('D') || input.is('X')) && isVisualMode()) { + if (isVisualCharMode()) + toggleVisualMode(VisualLineMode); + if (isVisualBlockMode() && input.is('D')) + m_visualTargetColumn = -1; + cutSelectedText(); + } else if (input.isControl('d')) { + const int scrollOffset = windowScrollOffset(); + int sline = cursorLine() < scrollOffset ? scrollOffset : cursorLineOnScreen(); + // FIXME: this should use the "scroll" option, and "count" + moveDown(linesOnScreen() / 2); + handleStartOfLine(); + scrollToLine(cursorLine() - sline); + } else if (!g.gflag && input.is('g')) { + g.gflag = true; + } else if (!isVisualMode() && (input.is('i') || input.isKey(Key_Insert))) { + breakEditBlock(); + enterInsertMode(); + if (atEndOfLine()) + moveLeft(); + } else if (input.is('I')) { + if (isVisualMode()) { + if (!isVisualBlockMode()) + dotCommand = QString::number(count()) + "i"; + enterVisualInsertMode('I'); + } else { + if (g.gflag) + moveToStartOfLine(); + else + moveToFirstNonBlankOnLine(); + breakEditBlock(); + enterInsertMode(); + } + } else if (input.isControl('i')) { + jump(count()); + } else if (input.is('J')) { + pushUndoState(); + moveBehindEndOfLine(); + beginEditBlock(); + if (g.submode == NoSubMode) + joinLines(count(), g.gflag); + endEditBlock(); + } else if (input.isControl('l')) { + // screen redraw. should not be needed + } else if (!g.gflag && input.is('m')) { + g.subsubmode = MarkSubSubMode; + } else if (isVisualMode() && (input.is('o') || input.is('O'))) { + int pos = position(); + setAnchorAndPosition(pos, anchor()); + std::swap(m_positionPastEnd, m_anchorPastEnd); + setTargetColumn(); + if (m_positionPastEnd) + m_visualTargetColumn = -1; + } else if (input.is('o') || input.is('O')) { + bool insertAfter = input.is('o'); + pushUndoState(); + + // Prepend line only if on the first line and command is 'O'. + bool appendLine = true; + if (!insertAfter) { + if (block().blockNumber() == 0) + appendLine = false; + else + moveUp(); + } + const int line = lineNumber(block()); + + beginEditBlock(); + enterInsertMode(); + setPosition(appendLine ? lastPositionInLine(line) : firstPositionInLine(line)); + clearLastInsertion(); + setAnchor(); + insertNewLine(); + if (appendLine) { + m_buffer->insertState.newLineBefore = true; + } else { + moveUp(); + m_buffer->insertState.pos1 = position(); + m_buffer->insertState.newLineAfter = true; + } + setTargetColumn(); + endEditBlock(); + + // Close accidentally opened block. + if (block().blockNumber() > 0) { + moveUp(); + if (line != lineNumber(block())) + q->fold(1, true); + moveDown(); + } + } else if (input.isControl('o')) { + jump(-count()); + } else if (input.is('p') || input.is('P') || input.isShift(Qt::Key_Insert)) { + dotCommand = QString("\"%1%2%3").arg(QChar(m_register)).arg(count()).arg(input.asChar()); + + pasteText(!input.is('P')); + setTargetColumn(); + finishMovement(); + } else if (input.is('q')) { + if (g.isRecording) { + // Stop recording. + stopRecording(); + } else { + // Recording shouldn't work in mapping or while executing register. + handled = g.mapStates.empty(); + if (handled) + g.submode = MacroRecordSubMode; + } + } else if (input.is('r')) { + g.submode = ReplaceSubMode; + } else if (!isVisualMode() && input.is('R')) { + pushUndoState(); + breakEditBlock(); + enterReplaceMode(); + } else if (input.isControl('r')) { + dotCommand.clear(); + int repeat = count(); + while (--repeat >= 0) + redo(); + } else if (input.is('S') && isVisualMode() && s.emulateSurround.value()) { + g.submode = AddSurroundingSubMode; + g.subsubmode = SurroundSubSubMode; + } else if (input.is('s')) { + handleAs("c%1l"); + } else if (input.is('S')) { + handleAs("%1cc"); + } else if (g.gflag && input.is('t')) { + handleExCommand("tabnext"); + } else if (g.gflag && input.is('T')) { + handleExCommand("tabprevious"); + } else if (input.isControl('t')) { + handleExCommand("pop"); + } else if (!g.gflag && input.is('u') && !isVisualMode()) { + dotCommand.clear(); + int repeat = count(); + while (--repeat >= 0) + undo(); + } else if (input.isControl('u')) { + int sline = cursorLineOnScreen(); + // FIXME: this should use the "scroll" option, and "count" + moveUp(linesOnScreen() / 2); + handleStartOfLine(); + scrollToLine(cursorLine() - sline); + } else if (g.gflag && input.is('v')) { + if (isNoVisualMode()) { + CursorPosition from = markLessPosition(); + CursorPosition to = markGreaterPosition(); + if (m_buffer->lastVisualModeInverted) + std::swap(from, to); + toggleVisualMode(m_buffer->lastVisualMode); + setCursorPosition(from); + setAnchor(); + setCursorPosition(to); + setTargetColumn(); + } + } else if (input.is('v')) { + toggleVisualMode(VisualCharMode); + } else if (input.is('V')) { + toggleVisualMode(VisualLineMode); + } else if (input.isControl('v')) { + toggleVisualMode(VisualBlockMode); + } else if (input.isControl('w')) { + g.submode = WindowSubMode; + } else if (input.is('x') && isNoVisualMode()) { + handleAs("%1dl"); + } else if (input.isControl('x')) { + changeNumberTextObject(-count()); + } else if (input.is('X')) { + handleAs("%1dh"); + } else if (input.is('Y') && isNoVisualMode()) { + handleAs("%1yy"); + } else if (input.isControl('y')) { + // FIXME: this should use the "scroll" option, and "count" + if (cursorLineOnScreen() == linesOnScreen() - 1) + moveUp(1); + scrollUp(1); + } else if (input.is('y') && isVisualCharMode()) { + g.rangemode = RangeCharMode; + g.movetype = MoveInclusive; + g.submode = YankSubMode; + finishMovement(); + } else if ((input.is('y') && isVisualLineMode()) || (input.is('Y') && isVisualLineMode()) || + (input.is('Y') && isVisualCharMode())) { + g.rangemode = RangeLineMode; + g.movetype = MoveLineWise; + g.submode = YankSubMode; + finishMovement(); + } else if ((input.is('y') || input.is('Y')) && isVisualBlockMode()) { + g.rangemode = RangeBlockMode; + g.movetype = MoveInclusive; + g.submode = YankSubMode; + finishMovement(); + } else if (input.is('z')) { + g.submode = ZSubMode; + } else if (input.is('Z')) { + g.submode = CapitalZSubMode; + } else if ((input.is('~') || input.is('u') || input.is('U'))) { + g.movetype = MoveExclusive; + g.submode = letterCaseModeFromInput(input); + pushUndoState(); + if (isVisualMode()) { + leaveVisualMode(); + finishMovement(); + } else if (g.gflag || (g.submode == InvertCaseSubMode && s.tildeOp.value())) { + if (atEndOfLine()) + moveLeft(); + setAnchor(); + } else { + const QString movementCommand = QString("%1l%1l").arg(count()); + handleAs("g" + input.toString() + movementCommand); + } + } else if (input.is('@')) { + g.submode = MacroExecuteSubMode; + } else if (input.isKey(Key_Delete)) { + setAnchor(); + moveRight(qMin(1, rightDist())); + removeText(currentRange()); + if (atEndOfLine()) + moveLeft(); + } else if (input.isControl(Key_BracketRight)) { + handleExCommand("tag"); + } else if (handleMovement(input)) { + // movement handled + dotCommand.clear(); + } else { + handled = false; + } + + // Set dot command if the current input changed document or entered insert mode. + if (handled && !dotCommand.isEmpty() && (oldRevision != revision() || isInsertMode())) + setDotCommand(dotCommand); + + return handled; +} + +bool +FakeVimHandler::Private::handleChangeDeleteYankSubModes(const Input &input) +{ + if (g.submode != changeDeleteYankModeFromInput(input)) + return false; + + handleChangeDeleteYankSubModes(); + + return true; +} + +void +FakeVimHandler::Private::handleChangeDeleteYankSubModes() +{ + g.movetype = MoveLineWise; + + const QString dotCommand = dotCommandFromSubMode(g.submode); + + if (!dotCommand.isEmpty()) + pushUndoState(); + + const int anc = firstPositionInLine(cursorLine() + 1); + moveDown(count() - 1); + const int pos = lastPositionInLine(cursorLine() + 1); + setAnchorAndPosition(anc, pos); + + if (!dotCommand.isEmpty()) + setDotCommand(QString("%2%1%1").arg(dotCommand), count()); + + finishMovement(); + + g.submode = NoSubMode; +} + +bool +FakeVimHandler::Private::handleReplaceSubMode(const Input &input) +{ + bool handled = true; + + const QChar c = input.asChar(); + setDotCommand(visualDotCommand() + 'r' + c); + if (isVisualMode()) { + pushUndoState(); + leaveVisualMode(); + Range range = currentRange(); + if (g.rangemode == RangeCharMode) + ++range.endPos; + // Replace each character but preserve lines. + transformText(range, [&c](const QString &text) { + return QString(text).replace(QRegularExpression("[^\\n]"), c); + }); + } else if (count() <= rightDist()) { + pushUndoState(); + setAnchor(); + moveRight(count()); + Range range = currentRange(); + if (input.isReturn()) { + beginEditBlock(); + replaceText(range, QString()); + insertText(QString("\n")); + endEditBlock(); + } else { + replaceText(range, QString(count(), c)); + moveRight(count() - 1); + } + setTargetColumn(); + setDotCommand("%1r" + input.text(), count()); + } else { + handled = false; + } + g.submode = NoSubMode; + finishMovement(); + + return handled; +} + +bool +FakeVimHandler::Private::handleCommentSubMode(const Input &input) +{ + if (!input.is('c')) + return false; + + g.movetype = MoveLineWise; + + const int anc = firstPositionInLine(cursorLine() + 1); + moveDown(count() - 1); + const int pos = lastPositionInLine(cursorLine() + 1); + setAnchorAndPosition(anc, pos); + + setDotCommand(QString("%1gcc").arg(count())); + + finishMovement(); + + g.submode = NoSubMode; + + return true; +} + +bool +FakeVimHandler::Private::handleReplaceWithRegisterSubMode(const Input &input) +{ + if (!input.is('r')) + return false; + + pushUndoState(false); + beginEditBlock(); + + const QString movement = (count() == 1) ? QString() : (QString::number(count() - 1) + "j"); + + g.dotCommand = "V" + movement + "gr"; + replay(g.dotCommand); + + endEditBlock(); + + return true; +} + +bool +FakeVimHandler::Private::handleExchangeSubMode(const Input &input) +{ + if (input.is('c')) { // cxc + g.exchangeRange.reset(); + g.submode = NoSubMode; + return true; + } + + if (input.is('x')) { // cxx + setAnchorAndPosition(firstPositionInLine(cursorLine() + 1), + lastPositionInLine(cursorLine() + 1) + 1); + + setDotCommand("cxx"); + + finishMovement(); + + g.submode = NoSubMode; + + return true; + } + + return false; +} + +bool +FakeVimHandler::Private::handleDeleteChangeSurroundingSubMode(const Input &input) +{ + if (g.submode != ChangeSurroundingSubMode && g.submode != DeleteSurroundingSubMode) + return false; + + bool handled = false; + + if (input.is('(') || input.is(')') || input.is('b')) { + handled = selectBlockTextObject(false, '(', ')'); + } else if (input.is('{') || input.is('}') || input.is('B')) { + handled = selectBlockTextObject(false, '{', '}'); + } else if (input.is('[') || input.is(']')) { + handled = selectBlockTextObject(false, '[', ']'); + } else if (input.is('<') || input.is('>') || input.is('t')) { + handled = selectBlockTextObject(false, '<', '>'); + } else if (input.is('"') || input.is('\'') || input.is('`')) { + handled = selectQuotedStringTextObject(false, input.asChar()); + } + + if (handled) { + if (g.submode == DeleteSurroundingSubMode) { + pushUndoState(false); + beginEditBlock(); + + // Surround is always one character, so just delete the first and last one + transformText(currentRange(), + [](const QString &text) { return text.mid(1, text.size() - 2); }); + + endEditBlock(); + clearCurrentMode(); + + g.dotCommand = QStringLiteral("ds") + input.asChar(); + } else if (g.submode == ChangeSurroundingSubMode) { + g.subsubmode = SurroundSubSubMode; + } + } + + return handled; +} + +bool +FakeVimHandler::Private::handleAddSurroundingSubMode(const Input &input) +{ + if (!input.is('s')) + return false; + + g.subsubmode = SurroundSubSubMode; + + int anc = firstPositionInLine(cursorLine() + 1); + const int pos = lastPositionInLine(cursorLine() + 1); + + // Ignore leading spaces + while ((characterAt(anc) == ' ' || characterAt(anc) == '\t') && anc != pos) { + anc++; + } + + setAnchorAndPosition(anc, pos); + + finishMovement("s"); + + return true; +} + +bool +FakeVimHandler::Private::handleFilterSubMode(const Input &) +{ + return false; +} + +bool +FakeVimHandler::Private::handleRegisterSubMode(const Input &input) +{ + bool handled = false; + + QChar reg = input.asChar(); + if (QString("*+.%#:-\"_").contains(reg) || reg.isLetterOrNumber()) { + m_register = reg.unicode(); + handled = true; + } + g.submode = NoSubMode; + + return handled; +} + +bool +FakeVimHandler::Private::handleShiftSubMode(const Input &input) +{ + if (g.submode != indentModeFromInput(input)) + return false; + + g.movetype = MoveLineWise; + pushUndoState(); + moveDown(count() - 1); + setDotCommand(QString("%2%1%1").arg(input.asChar()), count()); + finishMovement(); + g.submode = NoSubMode; + + return true; +} + +bool +FakeVimHandler::Private::handleChangeCaseSubMode(const Input &input) +{ + if (g.submode != letterCaseModeFromInput(input)) + return false; + + if (!isFirstNonBlankOnLine(position())) { + moveToStartOfLine(); + moveToFirstNonBlankOnLine(); + } + setTargetColumn(); + pushUndoState(); + setAnchor(); + setPosition(lastPositionInLine(cursorLine() + count()) + 1); + finishMovement(QString("%1%2").arg(count()).arg(input.raw())); + g.submode = NoSubMode; + + return true; +} + +bool +FakeVimHandler::Private::handleWindowSubMode(const Input &input) +{ + if (handleCount(input)) + return true; + + leaveVisualMode(); + leaveCurrentMode(); + q->windowCommandRequested(input.toString(), count()); + + return true; +} + +bool +FakeVimHandler::Private::handleZSubMode(const Input &input) +{ + bool handled = true; + bool foldMaybeClosed = false; + if (input.isReturn() || input.is('t') || input.is('-') || input.is('b') || input.is('.') || + input.is('z')) { + // Cursor line to top/center/bottom of window. + Qt::AlignmentFlag align; + if (input.isReturn() || input.is('t')) + align = Qt::AlignTop; + else if (input.is('.') || input.is('z')) + align = Qt::AlignVCenter; + else + align = Qt::AlignBottom; + const bool moveToNonBlank = (input.is('.') || input.isReturn() || input.is('-')); + const int line = g.mvcount == 0 ? -1 : firstPositionInLine(count()); + alignViewportToCursor(align, line, moveToNonBlank); + } else if (input.is('o') || input.is('c')) { + // Open/close current fold. + foldMaybeClosed = input.is('c'); + q->fold(count(), foldMaybeClosed); + } else if (input.is('O') || input.is('C')) { + // Recursively open/close current fold. + foldMaybeClosed = input.is('C'); + q->fold(-1, foldMaybeClosed); + } else if (input.is('a') || input.is('A')) { + // Toggle current fold. + foldMaybeClosed = true; + q->foldToggle(input.is('a') ? count() : -1); + } else if (input.is('R') || input.is('M')) { + // Open/close all folds in document. + foldMaybeClosed = input.is('M'); + q->foldAll(foldMaybeClosed); + } else if (input.is('j') || input.is('k')) { + q->foldGoTo(input.is('j') ? count() : -count(), false); + } else { + handled = false; + } + if (foldMaybeClosed) + ensureCursorVisible(); + g.submode = NoSubMode; + return handled; +} + +bool +FakeVimHandler::Private::handleCapitalZSubMode(const Input &input) +{ + // Recognize ZZ and ZQ as aliases for ":x" and ":q!". + bool handled = true; + if (input.is('Z')) + handleExCommand("x"); + else if (input.is('Q')) + handleExCommand("q!"); + else + handled = false; + g.submode = NoSubMode; + return handled; +} + +bool +FakeVimHandler::Private::handleMacroRecordSubMode(const Input &input) +{ + g.submode = NoSubMode; + return startRecording(input); +} + +bool +FakeVimHandler::Private::handleMacroExecuteSubMode(const Input &input) +{ + g.submode = NoSubMode; + + bool result = true; + int repeat = count(); + while (result && --repeat >= 0) + result = executeRegister(input.asChar().unicode()); + + return result; +} + +EventResult +FakeVimHandler::Private::handleInsertOrReplaceMode(const Input &input) +{ + if (position() < m_buffer->insertState.pos1 || position() > m_buffer->insertState.pos2) { + commitInsertState(); + invalidateInsertState(); + } + + if (g.mode == InsertMode) + handleInsertMode(input); + else + handleReplaceMode(input); + + if (!m_textedit && !m_plaintextedit) + return EventHandled; + + if (!isInsertMode() || m_buffer->breakEditBlock || position() < m_buffer->insertState.pos1 || + position() > m_buffer->insertState.pos2) { + commitInsertState(); + invalidateInsertState(); + breakEditBlock(); + m_visualBlockInsert = NoneBlockInsertMode; + } + + // We don't want fancy stuff in insert mode. + return EventHandled; +} + +void +FakeVimHandler::Private::handleReplaceMode(const Input &input) +{ + if (input.isEscape()) { + commitInsertState(); + moveLeft(qMin(1, leftDist())); + enterCommandMode(); + g.dotCommand.append(m_buffer->lastInsertion + "<ESC>"); + } else if (input.isKey(Key_Left)) { + moveLeft(); + } else if (input.isKey(Key_Right)) { + moveRight(); + } else if (input.isKey(Key_Up)) { + moveUp(); + } else if (input.isKey(Key_Down)) { + moveDown(); + } else if (input.isKey(Key_Insert)) { + g.mode = InsertMode; + } else if (input.isControl('o')) { + enterCommandMode(ReplaceMode); + } else { + joinPreviousEditBlock(); + if (!atEndOfLine()) { + setAnchor(); + moveRight(); + removeText(currentRange()); + } + const QString text = input.text(); + setAnchor(); + insertText(text); + setTargetColumn(); + endEditBlock(); + } +} + +void +FakeVimHandler::Private::finishInsertMode() +{ + bool newLineAfter = m_buffer->insertState.newLineAfter; + bool newLineBefore = m_buffer->insertState.newLineBefore; + + // Repeat insertion [count] times. + // One instance was already physically inserted while typing. + if (!m_buffer->breakEditBlock && isInsertStateValid()) { + commitInsertState(); + + QString text = m_buffer->lastInsertion; + const QString dotCommand = g.dotCommand; + const int repeat = count() - 1; + m_buffer->lastInsertion.clear(); + joinPreviousEditBlock(); + + if (newLineAfter) { + text.chop(1); + text.prepend("<END>\n"); + } else if (newLineBefore) { + text.prepend("<END>"); + } + + replay(text, repeat); + + if (m_visualBlockInsert != NoneBlockInsertMode && !text.contains('\n')) { + const CursorPosition lastAnchor = markLessPosition(); + const CursorPosition lastPosition = markGreaterPosition(); + const bool change = m_visualBlockInsert == ChangeBlockInsertMode; + const int insertColumn = (m_visualBlockInsert == InsertBlockInsertMode || change) + ? qMin(lastPosition.column, lastAnchor.column) + : qMax(lastPosition.column, lastAnchor.column) + 1; + + CursorPosition pos(lastAnchor.line, insertColumn); + + if (change) + pos.column = columnAt(m_buffer->insertState.pos1); + + // Cursor position after block insert is on the first selected line, + // last selected column for 's' command, otherwise first selected column. + const int endColumn = change ? qMax(0, m_cursor.positionInBlock() - 1) + : qMin(lastPosition.column, lastAnchor.column); + + while (pos.line < lastPosition.line) { + ++pos.line; + setCursorPosition(&m_cursor, pos); + if (m_visualBlockInsert == AppendToEndOfLineBlockInsertMode) { + moveToEndOfLine(); + } else if (m_visualBlockInsert == AppendBlockInsertMode) { + // Prepend spaces if necessary. + int spaces = pos.column - m_cursor.positionInBlock(); + if (spaces > 0) { + setAnchor(); + m_cursor.insertText(QString(" ").repeated(spaces)); + } + } else if (m_cursor.positionInBlock() != pos.column) { + continue; + } + replay(text, repeat + 1); + } + + setCursorPosition(CursorPosition(lastAnchor.line, endColumn)); + } else { + moveLeft(qMin(1, leftDist())); + } + + endEditBlock(); + breakEditBlock(); + + m_buffer->lastInsertion = text; + g.dotCommand = dotCommand; + } else { + moveLeft(qMin(1, leftDist())); + } + + if (newLineBefore || newLineAfter) + m_buffer->lastInsertion.remove(0, m_buffer->lastInsertion.indexOf('\n') + 1); + g.dotCommand.append(m_buffer->lastInsertion + "<ESC>"); + + setTargetColumn(); + enterCommandMode(); +} + +void +FakeVimHandler::Private::handleInsertMode(const Input &input) +{ + if (input.isEscape()) { + if (g.submode == CtrlRSubMode || g.submode == CtrlVSubMode) { + g.submode = NoSubMode; + g.subsubmode = NoSubSubMode; + updateMiniBuffer(); + } else { + finishInsertMode(); + } + } else if (g.submode == CtrlRSubMode) { + m_cursor.insertText(registerContents(input.asChar().unicode())); + g.submode = NoSubMode; + } else if (g.submode == CtrlVSubMode) { + if (g.subsubmode == NoSubSubMode) { + g.subsubmode = CtrlVUnicodeSubSubMode; + m_ctrlVAccumulator = 0; + if (input.is('x') || input.is('X')) { + // ^VXnn or ^Vxnn with 00 <= nn <= FF + // BMP Unicode codepoints ^Vunnnn with 0000 <= nnnn <= FFFF + // any Unicode codepoint ^VUnnnnnnnn with 00000000 <= nnnnnnnn <= 7FFFFFFF + // ^Vnnn with 000 <= nnn <= 255 + // ^VOnnn or ^Vonnn with 000 <= nnn <= 377 + m_ctrlVLength = 2; + m_ctrlVBase = 16; + } else if (input.is('O') || input.is('o')) { + m_ctrlVLength = 3; + m_ctrlVBase = 8; + } else if (input.is('u')) { + m_ctrlVLength = 4; + m_ctrlVBase = 16; + } else if (input.is('U')) { + m_ctrlVLength = 8; + m_ctrlVBase = 16; + } else if (input.isDigit()) { + bool ok; + m_ctrlVAccumulator = input.toInt(&ok, 10); + m_ctrlVLength = 2; + m_ctrlVBase = 10; + } else { + insertInInsertMode(input.raw()); + g.submode = NoSubMode; + g.subsubmode = NoSubSubMode; + } + } else { + bool ok; + int current = input.toInt(&ok, m_ctrlVBase); + if (ok) + m_ctrlVAccumulator = m_ctrlVAccumulator * m_ctrlVBase + current; + --m_ctrlVLength; + if (m_ctrlVLength == 0 || !ok) { + QString str; + if (QChar::requiresSurrogates(static_cast<uint>(m_ctrlVAccumulator))) { + str.append(QChar(QChar::highSurrogate(static_cast<uint>(m_ctrlVAccumulator)))); + str.append(QChar(QChar::lowSurrogate(static_cast<uint>(m_ctrlVAccumulator)))); + } else { + str.append(QChar(m_ctrlVAccumulator)); + } + insertInInsertMode(str); + g.submode = NoSubMode; + g.subsubmode = NoSubSubMode; + + // Try again without Ctrl-V interpretation. + if (!ok) + handleInsertMode(input); + } + } + } else if (input.isControl('o')) { + enterCommandMode(InsertMode); + } else if (input.isControl('v')) { + g.submode = CtrlVSubMode; + g.subsubmode = NoSubSubMode; + updateMiniBuffer(); + } else if (input.isControl('r')) { + g.submode = CtrlRSubMode; + g.subsubmode = NoSubSubMode; + updateMiniBuffer(); + } else if (input.isControl('w')) { + const int blockNumber = m_cursor.blockNumber(); + const int endPos = position(); + moveToNextWordStart(1, false, false); + if (blockNumber != m_cursor.blockNumber()) + moveToEndOfLine(); + const int beginPos = position(); + Range range(beginPos, endPos, RangeCharMode); + removeText(range); + } else if (input.isControl('u')) { + const int blockNumber = m_cursor.blockNumber(); + const int endPos = position(); + moveToStartOfLine(); + if (blockNumber != m_cursor.blockNumber()) + moveToEndOfLine(); + const int beginPos = position(); + Range range(beginPos, endPos, RangeCharMode); + removeText(range); + } else if (input.isKey(Key_Insert)) { + g.mode = ReplaceMode; + } else if (input.isKey(Key_Left)) { + moveLeft(); + } else if (input.isShift(Key_Left) || input.isControl(Key_Left)) { + moveToNextWordStart(1, false, false); + } else if (input.isKey(Key_Down)) { + g.submode = NoSubMode; + moveDown(); + } else if (input.isKey(Key_Up)) { + g.submode = NoSubMode; + moveUp(); + } else if (input.isKey(Key_Right)) { + moveRight(); + } else if (input.isShift(Key_Right) || input.isControl(Key_Right)) { + moveToNextWordStart(1, false, true); + } else if (input.isKey(Key_Home)) { + moveToStartOfLine(); + } else if (input.isKey(Key_End)) { + moveBehindEndOfLine(); + m_targetColumn = -1; + } else if (input.isReturn() || input.isControl('j') || input.isControl('m')) { + if (!input.isReturn() || !handleInsertInEditor(input)) { + joinPreviousEditBlock(); + g.submode = NoSubMode; + insertNewLine(); + endEditBlock(); + } + } else if (input.isBackspace()) { + // pass C-h as backspace, too + if (!handleInsertInEditor(Input(Qt::Key_Backspace, Qt::NoModifier))) { + joinPreviousEditBlock(); + if (!m_buffer->lastInsertion.isEmpty() || s.backspace.value().contains("start") || + s.backspace.value().contains("2")) { + const int line = cursorLine() + 1; + const Column col = cursorColumn(); + QString data = lineContents(line); + const Column ind = indentation(data); + if (col.logical <= ind.logical && col.logical && + startsWithWhitespace(data, col.physical)) { + const int ts = static_cast<int>(s.tabStop.value()); + const int newl = col.logical - 1 - (col.logical - 1) % ts; + const QString prefix = tabExpand(newl); + setLineContents(line, prefix + data.mid(col.physical)); + moveToStartOfLine(); + moveRight(prefix.size()); + } else { + setAnchor(); + m_cursor.deletePreviousChar(); + } + } + endEditBlock(); + } + } else if (input.isKey(Key_Delete)) { + if (!handleInsertInEditor(input)) { + joinPreviousEditBlock(); + m_cursor.deleteChar(); + endEditBlock(); + } + } else if (input.isKey(Key_PageDown) || input.isControl('f')) { + movePageDown(); + } else if (input.isKey(Key_PageUp) || input.isControl('b')) { + movePageUp(); + } else if (input.isKey(Key_Tab)) { + m_buffer->insertState.insertingSpaces = true; + if (s.expandTab.value()) { + const int ts = static_cast<int>(s.tabStop.value()); + const int col = logicalCursorColumn(); + QString str = QString(ts - col % ts, ' '); + insertText(str); + } else { + insertInInsertMode(input.raw()); + } + m_buffer->insertState.insertingSpaces = false; + } else if (input.isControl('d')) { + // remove one level of indentation from the current line + const int shift = static_cast<int>(s.shiftWidth.value()); + const int tab = static_cast<int>(s.tabStop.value()); + int line = cursorLine() + 1; + int pos = firstPositionInLine(line); + QString text = lineContents(line); + int amount = 0; + int i = 0; + for (; i < text.size() && amount < shift; ++i) { + if (text.at(i) == ' ') + ++amount; + else if (text.at(i) == '\t') + amount += tab; // FIXME: take position into consideration + else + break; + } + removeText(Range(pos, pos + i)); + } else if (input.isControl('p') || input.isControl('n')) { + QTextCursor tc = m_cursor; + moveToNextWordStart(1, false, false); + QString str = selectText(Range(position(), tc.position())); + m_cursor = tc; + q->simpleCompletionRequested(str, input.isControl('n')); + } else if (input.isShift(Qt::Key_Insert)) { + // Insert text from clipboard. + QClipboard *clipboard = QApplication::clipboard(); + const QMimeData *data = clipboard->mimeData(); + if (data && data->hasText()) + insertInInsertMode(data->text()); + } else { + m_buffer->insertState.insertingSpaces = input.isKey(Key_Space); + if (!handleInsertInEditor(input)) { + const QString toInsert = input.text(); + if (toInsert.isEmpty()) + return; + insertInInsertMode(toInsert); + } + m_buffer->insertState.insertingSpaces = false; + } +} + +void +FakeVimHandler::Private::insertInInsertMode(const QString &text) +{ + joinPreviousEditBlock(); + insertText(text); + if (s.smartIndent.value() && isElectricCharacter(text.at(0))) { + const QString leftText = block().text().left(position() - 1 - block().position()); + if (leftText.simplified().isEmpty()) { + Range range(position(), position(), g.rangemode); + indentText(range, text.at(0)); + } + } + setTargetColumn(); + endEditBlock(); + g.submode = NoSubMode; +} + +bool +FakeVimHandler::Private::startRecording(const Input &input) +{ + QChar reg = input.asChar(); + if (reg == '"' || reg.isLetterOrNumber()) { + g.currentRegister = reg.unicode(); + g.isRecording = true; + g.recorded.clear(); + return true; + } + + return false; +} + +void +FakeVimHandler::Private::record(const Input &input) +{ + if (g.isRecording) + g.recorded.append(input.toString()); +} + +void +FakeVimHandler::Private::stopRecording() +{ + // Remove q from end (stop recording command). + g.isRecording = false; + g.recorded.chop(1); + setRegister(g.currentRegister, g.recorded, g.rangemode); + g.currentRegister = 0; + g.recorded.clear(); +} + +void +FakeVimHandler::Private::handleAs(const QString &command) +{ + QString cmd = QString("\"%1").arg(QChar(m_register)); + + if (command.contains("%1")) + cmd.append(command.arg(count())); + else + cmd.append(command); + + leaveVisualMode(); + beginLargeEditBlock(); + replay(cmd); + endEditBlock(); +} + +bool +FakeVimHandler::Private::executeRegister(int reg) +{ + QChar regChar(reg); + + // TODO: Prompt for an expression to execute if register is '='. + if (reg == '@' && g.lastExecutedRegister != 0) + reg = g.lastExecutedRegister; + else if (QString("\".*+").contains(regChar) || regChar.isLetterOrNumber()) + g.lastExecutedRegister = reg; + else + return false; + + // FIXME: In Vim it's possible to interrupt recursive macro with <C-c>. + // One solution may be to call QApplication::processEvents() and check if <C-c> was + // used when a mapping is active. + // According to Vim, register is executed like mapping. + prependMapping(Inputs(registerContents(reg), false, false)); + + return true; +} + +EventResult +FakeVimHandler::Private::handleExMode(const Input &input) +{ + // handle C-R, C-R C-W, C-R {register} + if (handleCommandBufferPaste(input)) + return EventHandled; + + if (input.isEscape()) { + g.commandBuffer.clear(); + leaveCurrentMode(); + g.submode = NoSubMode; + } else if (g.submode == CtrlVSubMode) { + g.commandBuffer.insertChar(input.raw()); + g.submode = NoSubMode; + } else if (input.isControl('v')) { + g.submode = CtrlVSubMode; + g.subsubmode = NoSubSubMode; + return EventHandled; + } else if (input.isBackspace()) { + if (g.commandBuffer.isEmpty()) { + leaveVisualMode(); + leaveCurrentMode(); + } else if (g.commandBuffer.hasSelection()) { + g.commandBuffer.deleteSelected(); + } else { + g.commandBuffer.deleteChar(); + } + } else if (input.isKey(Key_Tab)) { + // FIXME: Complete actual commands. + g.commandBuffer.historyUp(); + } else if (input.isReturn()) { + showMessage(MessageCommand, g.commandBuffer.display()); + handleExCommand(g.commandBuffer.contents()); + g.commandBuffer.clear(); + } else if (!g.commandBuffer.handleInput(input)) { + qDebug() << "IGNORED IN EX-MODE: " << input.key() << input.text(); + return EventUnhandled; + } + + return EventHandled; +} + +EventResult +FakeVimHandler::Private::handleSearchSubSubMode(const Input &input) +{ + EventResult handled = EventHandled; + + // handle C-R, C-R C-W, C-R {register} + if (handleCommandBufferPaste(input)) + return handled; + + if (input.isEscape()) { + g.currentMessage.clear(); + setPosition(m_searchStartPosition); + scrollToLine(m_searchFromScreenLine); + } else if (input.isBackspace()) { + if (g.searchBuffer.isEmpty()) + leaveCurrentMode(); + else if (g.searchBuffer.hasSelection()) + g.searchBuffer.deleteSelected(); + else + g.searchBuffer.deleteChar(); + } else if (input.isReturn()) { + const QString &needle = g.searchBuffer.contents(); + if (!needle.isEmpty()) + g.lastSearch = needle; + else + g.searchBuffer.setContents(g.lastSearch); + + updateFind(true); + + if (finishSearch()) { + if (g.submode != NoSubMode) + finishMovement(g.searchBuffer.prompt() + g.lastSearch + '\n'); + if (g.currentMessage.isEmpty()) + showMessage(MessageCommand, g.searchBuffer.display()); + } else { + handled = EventCancelled; // Not found so cancel mapping if any. + } + } else if (input.isKey(Key_Tab)) { + g.searchBuffer.insertChar(QChar(9)); + } else if (!g.searchBuffer.handleInput(input)) { + //qDebug() << "IGNORED IN SEARCH MODE: " << input.key() << input.text(); + return EventUnhandled; + } + + if (input.isReturn() || input.isEscape()) { + g.searchBuffer.clear(); + leaveCurrentMode(); + } else { + updateFind(false); + } + + return handled; +} + +// This uses 0 based line counting (hidden lines included). +int +FakeVimHandler::Private::parseLineAddress(QString *cmd) +{ + //qDebug() << "CMD: " << cmd; + if (cmd->isEmpty()) + return -1; + + int result = -1; + QChar c = cmd->at(0); + if (c == '.') { // current line + result = cursorBlockNumber(); + cmd->remove(0, 1); + } else if (c == '$') { // last line + result = document()->blockCount() - 1; + cmd->remove(0, 1); + } else if (c == '\'') { // mark + cmd->remove(0, 1); + if (cmd->isEmpty()) { + showMessage(MessageError, msgMarkNotSet(QString())); + return -1; + } + c = cmd->at(0); + Mark m = mark(c); + if (!m.isValid() || !m.isLocal(m_currentFileName)) { + showMessage(MessageError, msgMarkNotSet(c)); + return -1; + } + cmd->remove(0, 1); + result = m.position(document()).line; + } else if (c.isDigit()) { // line with given number + result = 0; + } else if (c == '-' || c == '+') { // add or subtract from current line number + result = cursorBlockNumber(); + } else if (c == '/' || c == '?' || + (c == '\\' && cmd->size() > 1 && QString("/?&").contains(cmd->at(1)))) { + // search for expression + SearchData sd; + if (c == '/' || c == '?') { + const int end = findUnescaped(c, *cmd, 1); + if (end == -1) + return -1; + sd.needle = cmd->mid(1, end - 1); + cmd->remove(0, end + 1); + } else { + c = cmd->at(1); + cmd->remove(0, 2); + sd.needle = (c == '&') ? g.lastSubstitutePattern : g.lastSearch; + } + sd.forward = (c != '?'); + const QTextBlock b = block(); + const int pos = b.position() + (sd.forward ? b.length() - 1 : 0); + QTextCursor tc = search(sd, pos, 1, true); + g.lastSearch = sd.needle; + if (tc.isNull()) + return -1; + result = tc.block().blockNumber(); + } else { + return cursorBlockNumber(); + } + + // basic arithmetic ("-3+5" or "++" means "+2" etc.) + int n = 0; + bool add = true; + int i = 0; + for (; i < cmd->size(); ++i) { + c = cmd->at(i); + if (c == '-' || c == '+') { + if (n != 0) + result = result + (add ? n - 1 : -(n - 1)); + add = c == '+'; + result = result + (add ? 1 : -1); + n = 0; + } else if (c.isDigit()) { + n = n * 10 + c.digitValue(); + } else if (!c.isSpace()) { + break; + } + } + if (n != 0) + result = result + (add ? n - 1 : -(n - 1)); + *cmd = cmd->mid(i).trimmed(); + + return result; +} + +void +FakeVimHandler::Private::setCurrentRange(const Range &range) +{ + setAnchorAndPosition(range.beginPos, range.endPos); + g.rangemode = range.rangemode; +} + +bool +FakeVimHandler::Private::parseExCommand(QString *line, ExCommand *cmd) +{ + *cmd = ExCommand(); + if (line->isEmpty()) + return false; + + // parse range first + if (!parseLineRange(line, cmd)) + return false; + + // get first command from command line + QChar close; + bool subst = false; + int i = 0; + for (; i < line->size(); ++i) { + const QChar &c = line->at(i); + if (c == '\\') { + ++i; // skip escaped character + } else if (close.isNull()) { + if (c == '|') { + // split on | + break; + } else if (c == '/') { + subst = i > 0 && (line->at(i - 1) == 's'); + close = c; + } else if (c == '"' || c == '\'') { + close = c; + } + } else if (c == close) { + if (subst) + subst = false; + else + close = QChar(); + } + } + + cmd->cmd = line->mid(0, i).trimmed(); + + // command arguments starts with first non-letter character + cmd->args = cmd->cmd.section(QRegularExpression("(?=[^a-zA-Z])"), 1); + if (!cmd->args.isEmpty()) { + cmd->cmd.chop(cmd->args.size()); + cmd->args = cmd->args.trimmed(); + + // '!' at the end of command + cmd->hasBang = cmd->args.startsWith('!'); + if (cmd->hasBang) + cmd->args = cmd->args.mid(1).trimmed(); + } + + // remove the first command from command line + line->remove(0, i + 1); + + return true; +} + +bool +FakeVimHandler::Private::parseLineRange(QString *line, ExCommand *cmd) +{ + // remove leading colons and spaces + line->remove(QRegularExpression("^\\s*(:+\\s*)*")); + + // special case ':!...' (use invalid range) + if (line->startsWith('!')) { + cmd->range = Range(); + return true; + } + + // FIXME: that seems to be different for %w and %s + if (line->startsWith('%')) + line->replace(0, 1, "1,$"); + + int beginLine = parseLineAddress(line); + int endLine; + if (line->startsWith(',')) { + *line = line->mid(1).trimmed(); + endLine = parseLineAddress(line); + } else { + endLine = beginLine; + } + if (beginLine == -1 || endLine == -1) + return false; + + const int beginPos = firstPositionInLine(qMin(beginLine, endLine) + 1, false); + const int endPos = lastPositionInLine(qMax(beginLine, endLine) + 1, false); + cmd->range = Range(beginPos, endPos, RangeLineMode); + cmd->count = beginLine; + + return true; +} + +void +FakeVimHandler::Private::parseRangeCount(const QString &line, Range *range) const +{ + bool ok; + const int count = qAbs(line.trimmed().toInt(&ok)); + if (ok) { + const int beginLine = blockAt(range->endPos).blockNumber() + 1; + const int endLine = qMin(beginLine + count - 1, document()->blockCount()); + range->beginPos = firstPositionInLine(beginLine, false); + range->endPos = lastPositionInLine(endLine, false); + } +} + +// use handleExCommand for invoking commands that might move the cursor +void +FakeVimHandler::Private::handleCommand(const QString &cmd) +{ + handleExCommand(cmd); +} + +bool +FakeVimHandler::Private::handleExSubstituteCommand(const ExCommand &cmd) +{ + // :substitute + if (!cmd.matches("s", "substitute") && + !(cmd.cmd.isEmpty() && !cmd.args.isEmpty() && QString("&~").contains(cmd.args[0]))) { + return false; + } + + int count = 1; + QString line = cmd.args; + const QRegularExpressionMatch match = QRegularExpression("\\d+$").match(line); + if (match.hasMatch()) { + count = match.captured().toInt(); + line = line.left(match.capturedStart()).trimmed(); + } + + if (cmd.cmd.isEmpty()) { + // keep previous substitution flags on '&&' and '~&' + if (line.size() > 1 && line[1] == '&') + g.lastSubstituteFlags += line.mid(2); + else + g.lastSubstituteFlags = line.mid(1); + if (line[0] == '~') + g.lastSubstitutePattern = g.lastSearch; + } else { + if (line.isEmpty()) { + g.lastSubstituteFlags.clear(); + } else { + // we have /{pattern}/{string}/[flags] now + const QChar separator = line.at(0); + int pos1 = findUnescaped(separator, line, 1); + if (pos1 == -1) + return false; + int pos2 = findUnescaped(separator, line, pos1 + 1); + if (pos2 == -1) + pos2 = line.size(); + + g.lastSubstitutePattern = line.mid(1, pos1 - 1); + g.lastSubstituteReplacement = line.mid(pos1 + 1, pos2 - pos1 - 1); + g.lastSubstituteFlags = line.mid(pos2 + 1); + } + } + + count = qMax(1, count); + QString needle = g.lastSubstitutePattern; + + if (g.lastSubstituteFlags.contains('i')) + needle.prepend("\\c"); + + const QRegularExpression pattern = vimPatternToQtPattern(needle); + + QTextBlock lastBlock; + QTextBlock firstBlock; + const bool global = g.lastSubstituteFlags.contains('g'); + for (int a = 0; a != count; ++a) { + for (QTextBlock block = blockAt(cmd.range.endPos); + block.isValid() && block.position() + block.length() > cmd.range.beginPos; + block = block.previous()) { + QString text = block.text(); + if (substituteText(&text, pattern, g.lastSubstituteReplacement, global)) { + firstBlock = block; + if (!lastBlock.isValid()) { + lastBlock = block; + beginEditBlock(); + } + QTextCursor tc = m_cursor; + const int pos = block.position(); + const int anchor = pos + block.length() - 1; + tc.setPosition(anchor); + tc.setPosition(pos, QTextCursor::KeepAnchor); + tc.insertText(text); + } + } + } + + if (lastBlock.isValid()) { + m_buffer->undoState.position = CursorPosition(firstBlock.blockNumber(), 0); + + leaveVisualMode(); + setPosition(lastBlock.position()); + setAnchor(); + moveToFirstNonBlankOnLine(); + + endEditBlock(); + } + + return true; +} + +bool +FakeVimHandler::Private::handleExTabNextCommand(const ExCommand &cmd) +{ + if (!cmd.matches("tabn", "tabnext")) + return false; + + q->tabNextRequested(); + return true; +} + +bool +FakeVimHandler::Private::handleExTabPreviousCommand(const ExCommand &cmd) +{ + if (!cmd.matches("tabp", "tabprevious")) + return false; + + q->tabPreviousRequested(); + return true; +} + +bool +FakeVimHandler::Private::handleExMapCommand(const ExCommand &cmd0) // :map +{ + QByteArray modes; + enum Type { Map, Noremap, Unmap } type; + + QByteArray cmd = cmd0.cmd.toLatin1(); + + // Strange formatting. But everything else is even uglier. + if (cmd == "map") { + modes = "nvo"; + type = Map; + } else if (cmd == "nm" || cmd == "nmap") { + modes = "n"; + type = Map; + } else if (cmd == "vm" || cmd == "vmap") { + modes = "v"; + type = Map; + } else if (cmd == "xm" || cmd == "xmap") { + modes = "x"; + type = Map; + } else if (cmd == "smap") { + modes = "s"; + type = Map; + } else if (cmd == "omap") { + modes = "o"; + type = Map; + } else if (cmd == "map!") { + modes = "ic"; + type = Map; + } else if (cmd == "im" || cmd == "imap") { + modes = "i"; + type = Map; + } else if (cmd == "lm" || cmd == "lmap") { + modes = "l"; + type = Map; + } else if (cmd == "cm" || cmd == "cmap") { + modes = "c"; + type = Map; + } else if (cmd == "no" || cmd == "noremap") { + modes = "nvo"; + type = Noremap; + } else if (cmd == "nn" || cmd == "nnoremap") { + modes = "n"; + type = Noremap; + } else if (cmd == "vn" || cmd == "vnoremap") { + modes = "v"; + type = Noremap; + } else if (cmd == "xn" || cmd == "xnoremap") { + modes = "x"; + type = Noremap; + } else if (cmd == "snor" || cmd == "snoremap") { + modes = "s"; + type = Noremap; + } else if (cmd == "ono" || cmd == "onoremap") { + modes = "o"; + type = Noremap; + } else if (cmd == "no!" || cmd == "noremap!") { + modes = "ic"; + type = Noremap; + } else if (cmd == "ino" || cmd == "inoremap") { + modes = "i"; + type = Noremap; + } else if (cmd == "ln" || cmd == "lnoremap") { + modes = "l"; + type = Noremap; + } else if (cmd == "cno" || cmd == "cnoremap") { + modes = "c"; + type = Noremap; + } else if (cmd == "unm" || cmd == "unmap") { + modes = "nvo"; + type = Unmap; + } else if (cmd == "nun" || cmd == "nunmap") { + modes = "n"; + type = Unmap; + } else if (cmd == "vu" || cmd == "vunmap") { + modes = "v"; + type = Unmap; + } else if (cmd == "xu" || cmd == "xunmap") { + modes = "x"; + type = Unmap; + } else if (cmd == "sunm" || cmd == "sunmap") { + modes = "s"; + type = Unmap; + } else if (cmd == "ou" || cmd == "ounmap") { + modes = "o"; + type = Unmap; + } else if (cmd == "unm!" || cmd == "unmap!") { + modes = "ic"; + type = Unmap; + } else if (cmd == "iu" || cmd == "iunmap") { + modes = "i"; + type = Unmap; + } else if (cmd == "lu" || cmd == "lunmap") { + modes = "l"; + type = Unmap; + } else if (cmd == "cu" || cmd == "cunmap") { + modes = "c"; + type = Unmap; + } + + else + return false; + + QString args = cmd0.args; + bool silent = false; + bool unique = false; + forever + { + if (eatString("<silent>", &args)) { + silent = true; + } else if (eatString("<unique>", &args)) { + continue; + } else if (eatString("<special>", &args)) { + continue; + } else if (eatString("<buffer>", &args)) { + notImplementedYet(); + continue; + } else if (eatString("<script>", &args)) { + notImplementedYet(); + continue; + } else if (eatString("<expr>", &args)) { + notImplementedYet(); + return true; + } + break; + } + + const QString lhs = args.section(QRegularExpression("\\s+"), 0, 0); + const QString rhs = args.section(QRegularExpression("\\s+"), 1); + if ((rhs.isNull() && type != Unmap) || (!rhs.isNull() && type == Unmap)) { + // FIXME: Dump mappings here. + //qDebug() << g.mappings; + return true; + } + + Inputs key(lhs); + //qDebug() << "MAPPING: " << modes << lhs << rhs; + switch (type) { + case Unmap: foreach(char c, modes) MappingsIterator(&g.mappings, c, key).remove(); break; + case Map: Q_FALLTHROUGH(); + case Noremap: { + Inputs inputs(rhs, type == Noremap, silent); + foreach(char c, modes) MappingsIterator(&g.mappings, c).setInputs(key, inputs, unique); + break; + } + } + return true; +} + +bool +FakeVimHandler::Private::handleExHistoryCommand(const ExCommand &cmd) +{ + // :his[tory] + if (!cmd.matches("his", "history")) + return false; + + if (cmd.args.isEmpty()) { + QString info; + info += "# command history\n"; + int i = 0; + foreach(const QString &item, g.commandBuffer.historyItems()) + { + ++i; + info += QString("%1 %2\n").arg(i, -8).arg(item); + } + q->extraInformationChanged(info); + } else { + notImplementedYet(); + } + + return true; +} + +bool +FakeVimHandler::Private::handleExRegisterCommand(const ExCommand &cmd) +{ + // :reg[isters] and :di[splay] + if (!cmd.matches("reg", "registers") && !cmd.matches("di", "display")) + return false; + + QByteArray regs = cmd.args.toLatin1(); + if (regs.isEmpty()) { + regs = "\"0123456789"; + for (auto it = g.registers.cbegin(), end = g.registers.cend(); it != end; ++it) { + if (it.key() > '9') + regs += char(it.key()); + } + } + QString info; + info += "--- Registers ---\n"; + for (char reg : qAsConst(regs)) { + QString value = quoteUnprintable(registerContents(reg)); + info += QString("\"%1 %2\n").arg(reg).arg(value); + } + q->extraInformationChanged(info); + + return true; +} + +bool +FakeVimHandler::Private::handleExSetCommand(const ExCommand &cmd) +{ + // :se[t] + if (!cmd.matches("se", "set")) + return false; + + clearMessage(); + + if (cmd.args.contains('=')) { + // Non-boolean config to set. + int p = cmd.args.indexOf('='); + QString error = s.trySetValue(cmd.args.left(p), cmd.args.mid(p + 1)); + if (!error.isEmpty()) + showMessage(MessageError, error); + } else { + QString optionName = cmd.args; + + bool toggleOption = optionName.endsWith('!'); + bool printOption = !toggleOption && optionName.endsWith('?'); + if (printOption || toggleOption) + optionName.chop(1); + + bool negateOption = optionName.startsWith("no"); + if (negateOption) + optionName.remove(0, 2); + + FvBaseAspect *act = s.item(optionName); + if (!act) { + showMessage(MessageError, Tr::tr("Unknown option:") + ' ' + cmd.args); + } else if (act->defaultValue().type() == QVariant::Bool) { + bool oldValue = act->value().toBool(); + if (printOption) { + showMessage(MessageInfo, + QLatin1String(oldValue ? "" : "no") + act->settingsKey().toLower()); + } else if (toggleOption || negateOption == oldValue) { + act->setValue(!oldValue); + } + } else if (negateOption && !printOption) { + showMessage(MessageError, Tr::tr("Invalid argument:") + ' ' + cmd.args); + } else if (toggleOption) { + showMessage(MessageError, Tr::tr("Trailing characters:") + ' ' + cmd.args); + } else { + showMessage(MessageInfo, act->settingsKey().toLower() + "=" + act->value().toString()); + } + } + updateEditor(); + updateHighlights(); + return true; +} + +bool +FakeVimHandler::Private::handleExNormalCommand(const ExCommand &cmd) +{ + // :norm[al] + if (!cmd.matches("norm", "normal")) + return false; + //qDebug() << "REPLAY NORMAL: " << quoteUnprintable(reNormal.cap(3)); + replay(cmd.args); + return true; +} + +bool +FakeVimHandler::Private::handleExYankDeleteCommand(const ExCommand &cmd) +{ + // :[range]d[elete] [x] [count] + // :[range]y[ank] [x] [count] + const bool remove = cmd.matches("d", "delete"); + if (!remove && !cmd.matches("y", "yank")) + return false; + + // get register from arguments + const bool hasRegisterArg = !cmd.args.isEmpty() && !cmd.args.at(0).isDigit(); + const int r = hasRegisterArg ? cmd.args.at(0).unicode() : m_register; + + // get [count] from arguments + Range range = cmd.range; + parseRangeCount(cmd.args.mid(hasRegisterArg ? 1 : 0).trimmed(), &range); + + yankText(range, r); + + if (remove) { + leaveVisualMode(); + setPosition(range.beginPos); + pushUndoState(); + setCurrentRange(range); + removeText(currentRange()); + } + + return true; +} + +bool +FakeVimHandler::Private::handleExChangeCommand(const ExCommand &cmd) +{ + // :[range]c[hange] + if (!cmd.matches("c", "change")) + return false; + + Range range = cmd.range; + range.rangemode = RangeLineModeExclusive; + removeText(range); + insertAutomaticIndentation(true, cmd.hasBang); + + // FIXME: In Vim same or less number of lines can be inserted and position after insertion is + // beginning of last inserted line. + enterInsertMode(); + + return true; +} + +bool +FakeVimHandler::Private::handleExMoveCommand(const ExCommand &cmd) +{ + // :[range]m[ove] {address} + if (!cmd.matches("m", "move")) + return false; + + QString lineCode = cmd.args; + + const int startLine = blockAt(cmd.range.beginPos).blockNumber(); + const int endLine = blockAt(cmd.range.endPos).blockNumber(); + const int lines = endLine - startLine + 1; + + int targetLine = lineCode == "0" ? -1 : parseLineAddress(&lineCode); + if (targetLine >= startLine && targetLine < endLine) { + showMessage(MessageError, Tr::tr("Move lines into themselves.")); + return true; + } + + CursorPosition lastAnchor = markLessPosition(); + CursorPosition lastPosition = markGreaterPosition(); + + recordJump(); + setPosition(cmd.range.beginPos); + pushUndoState(); + + setCurrentRange(cmd.range); + QString text = selectText(cmd.range); + removeText(currentRange()); + + const bool insertAtEnd = targetLine == document()->blockCount(); + if (targetLine >= startLine) + targetLine -= lines; + QTextBlock block = document()->findBlockByNumber(insertAtEnd ? targetLine : targetLine + 1); + setPosition(block.position()); + setAnchor(); + + if (insertAtEnd) { + moveBehindEndOfLine(); + text.chop(1); + insertText(QString("\n")); + } + insertText(text); + + if (!insertAtEnd) + moveUp(1); + if (s.startOfLine.value()) + moveToFirstNonBlankOnLine(); + + if (lastAnchor.line >= startLine && lastAnchor.line <= endLine) + lastAnchor.line += targetLine - startLine + 1; + if (lastPosition.line >= startLine && lastPosition.line <= endLine) + lastPosition.line += targetLine - startLine + 1; + setMark('<', lastAnchor); + setMark('>', lastPosition); + + if (lines > 2) + showMessage(MessageInfo, Tr::tr("%n lines moved.", nullptr, lines)); + + return true; +} + +bool +FakeVimHandler::Private::handleExJoinCommand(const ExCommand &cmd) +{ + // :[range]j[oin][!] [count] + // FIXME: Argument [count] can follow immediately. + if (!cmd.matches("j", "join")) + return false; + + // get [count] from arguments + bool ok; + int count = cmd.args.toInt(&ok); + + if (ok) { + setPosition(cmd.range.endPos); + } else { + setPosition(cmd.range.beginPos); + const int startLine = blockAt(cmd.range.beginPos).blockNumber(); + const int endLine = blockAt(cmd.range.endPos).blockNumber(); + count = endLine - startLine + 1; + } + + moveToStartOfLine(); + pushUndoState(); + joinLines(count, cmd.hasBang); + + moveToFirstNonBlankOnLine(); + + return true; +} + +bool +FakeVimHandler::Private::handleExWriteCommand(const ExCommand &cmd) +{ + // Note: The cmd.args.isEmpty() case is handled by handleExPluginCommand. + // :w, :x, :wq, ... + //static QRegularExpression reWrite("^[wx]q?a?!?( (.*))?$"); + if (cmd.cmd != "w" && cmd.cmd != "x" && cmd.cmd != "wq") + return false; + + int beginLine = lineForPosition(cmd.range.beginPos); + int endLine = lineForPosition(cmd.range.endPos); + const bool noArgs = (beginLine == -1); + if (beginLine == -1) + beginLine = 0; + if (endLine == -1) + endLine = linesInDocument(); + //qDebug() << "LINES: " << beginLine << endLine; + //QString prefix = cmd.args; + const bool forced = cmd.hasBang; + //const bool quit = prefix.contains('q') || prefix.contains('x'); + //const bool quitAll = quit && prefix.contains('a'); + QString fileName = replaceTildeWithHome(cmd.args); + if (fileName.isEmpty()) + fileName = m_currentFileName; + QFile file1(fileName); + const bool exists = file1.exists(); + if (exists && !forced && !noArgs) { + showMessage(MessageError, Tr::tr("File \"%1\" exists (add ! to override)").arg(fileName)); + } else if (file1.open(QIODevice::ReadWrite)) { + // Nobody cared, so act ourselves. + file1.close(); + Range range(firstPositionInLine(beginLine), firstPositionInLine(endLine), RangeLineMode); + QString contents = selectText(range); + QFile::remove(fileName); + QFile file2(fileName); + if (file2.open(QIODevice::ReadWrite)) { + QTextStream ts(&file2); + ts << contents; + } else { + showMessage(MessageError, Tr::tr("Cannot open file \"%1\" for writing").arg(fileName)); + } + // Check result by reading back. + QFile file3(fileName); + file3.open(QIODevice::ReadOnly); + QByteArray ba = file3.readAll(); + showMessage(MessageInfo, Tr::tr("\"%1\" %2 %3L, %4C written.") + .arg(fileName) + .arg(exists ? QString(" ") : Tr::tr(" [New] ")) + .arg(ba.count('\n')) + .arg(ba.size())); + //if (quitAll) + // passUnknownExCommand(forced ? "qa!" : "qa"); + //else if (quit) + // passUnknownExCommand(forced ? "q!" : "q"); + } else { + showMessage(MessageError, Tr::tr("Cannot open file \"%1\" for reading").arg(fileName)); + } + return true; +} + +bool +FakeVimHandler::Private::handleExReadCommand(const ExCommand &cmd) +{ + // :r[ead] + if (!cmd.matches("r", "read")) + return false; + + beginEditBlock(); + + moveToStartOfLine(); + moveDown(); + int pos = position(); + + m_currentFileName = replaceTildeWithHome(cmd.args); + QFile file(m_currentFileName); + file.open(QIODevice::ReadOnly); + QTextStream ts(&file); + QString data = ts.readAll(); + insertText(data); + + setAnchorAndPosition(pos, pos); + + endEditBlock(); + + showMessage( + MessageInfo, + Tr::tr("\"%1\" %2L, %3C").arg(m_currentFileName).arg(data.count('\n')).arg(data.size())); + + return true; +} + +bool +FakeVimHandler::Private::handleExBangCommand(const ExCommand &cmd) // :! +{ + if (!cmd.cmd.isEmpty() || !cmd.hasBang) + return false; + + bool replaceText = cmd.range.isValid(); + const QString command = QString(cmd.cmd.mid(1) + ' ' + cmd.args).trimmed(); + const QString input = replaceText ? selectText(cmd.range) : QString(); + + const QString result = getProcessOutput(command, input); + + if (replaceText) { + setCurrentRange(cmd.range); + int targetPosition = firstPositionInLine(lineForPosition(cmd.range.beginPos)); + beginEditBlock(); + removeText(currentRange()); + insertText(result); + setPosition(targetPosition); + endEditBlock(); + leaveVisualMode(); + //qDebug() << "FILTER: " << command; + showMessage(MessageInfo, Tr::tr("%n lines filtered.", nullptr, input.count('\n'))); + } else if (!result.isEmpty()) { + q->extraInformationChanged(result); + } + + return true; +} + +bool +FakeVimHandler::Private::handleExShiftCommand(const ExCommand &cmd) +{ + // :[range]{<|>}* [count] + if (!cmd.cmd.isEmpty() || (!cmd.args.startsWith('<') && !cmd.args.startsWith('>'))) + return false; + + const QChar c = cmd.args.at(0); + + // get number of repetition + int repeat = 1; + int i = 1; + for (; i < cmd.args.size(); ++i) { + const QChar c2 = cmd.args.at(i); + if (c2 == c) + ++repeat; + else if (!c2.isSpace()) + break; + } + + // get [count] from arguments + Range range = cmd.range; + parseRangeCount(cmd.args.mid(i), &range); + + setCurrentRange(range); + if (c == '<') + shiftRegionLeft(repeat); + else + shiftRegionRight(repeat); + + leaveVisualMode(); + + return true; +} + +bool +FakeVimHandler::Private::handleExSortCommand(const ExCommand &cmd) +{ + // :[range]sor[t][!] [b][f][i][n][o][r][u][x] [/{pattern}/] + // FIXME: Only the ! for reverse is implemented. + if (!cmd.matches("sor", "sort")) + return false; + + // Force operation on full lines, and full document if only + // one line (the current one...) is specified + int beginLine = lineForPosition(cmd.range.beginPos); + int endLine = lineForPosition(cmd.range.endPos); + if (beginLine == endLine) { + beginLine = 0; + endLine = lineForPosition(lastPositionInDocument()); + } + Range range(firstPositionInLine(beginLine), firstPositionInLine(endLine), RangeLineMode); + + QString input = selectText(range); + if (input.endsWith('\n')) // It should always... + input.chop(1); + + QStringList lines = input.split('\n'); + lines.sort(); + if (cmd.hasBang) + std::reverse(lines.begin(), lines.end()); + QString res = lines.join('\n') + '\n'; + + replaceText(range, res); + + return true; +} + +bool +FakeVimHandler::Private::handleExNohlsearchCommand(const ExCommand &cmd) +{ + // :noh, :nohl, ..., :nohlsearch + if (cmd.cmd.size() < 3 || !QString("nohlsearch").startsWith(cmd.cmd)) + return false; + + g.highlightsCleared = true; + updateHighlights(); + return true; +} + +bool +FakeVimHandler::Private::handleExUndoRedoCommand(const ExCommand &cmd) +{ + // :undo + // :redo + bool undo = (cmd.cmd == "u" || cmd.cmd == "un" || cmd.cmd == "undo"); + if (!undo && cmd.cmd != "red" && cmd.cmd != "redo") + return false; + + undoRedo(undo); + + return true; +} + +bool +FakeVimHandler::Private::handleExGotoCommand(const ExCommand &cmd) +{ + // :{address} + if (!cmd.cmd.isEmpty() || !cmd.args.isEmpty()) + return false; + + const int beginLine = lineForPosition(cmd.range.endPos); + setPosition(firstPositionInLine(beginLine)); + clearMessage(); + return true; +} + +bool +FakeVimHandler::Private::handleExSourceCommand(const ExCommand &cmd) +{ + // :source + if (cmd.cmd != "so" && cmd.cmd != "source") + return false; + + QString fileName = replaceTildeWithHome(cmd.args); + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + showMessage(MessageError, Tr::tr("Cannot open file %1").arg(fileName)); + return true; + } + + bool inFunction = false; + QByteArray line; + while (!file.atEnd() || !line.isEmpty()) { + QByteArray nextline = !file.atEnd() ? file.readLine() : QByteArray(); + + // remove comment + int i = nextline.lastIndexOf('"'); + if (i != -1) + nextline = nextline.remove(i, nextline.size() - i); + + nextline = nextline.trimmed(); + + // multi-line command? + if (nextline.startsWith('\\')) { + line += nextline.mid(1); + continue; + } + + if (line.startsWith("function")) { + //qDebug() << "IGNORING FUNCTION" << line; + inFunction = true; + } else if (inFunction && line.startsWith("endfunction")) { + inFunction = false; + } else if (!line.isEmpty() && !inFunction) { + //qDebug() << "EXECUTING: " << line; + ExCommand cmdLocal; + QString commandLine = QString::fromLocal8Bit(line); + while (parseExCommand(&commandLine, &cmdLocal)) { + if (!handleExCommandHelper(cmdLocal)) + break; + } + } + + line = nextline; + } + file.close(); + return true; +} + +bool +FakeVimHandler::Private::handleExEchoCommand(const ExCommand &cmd) +{ + // :echo + if (cmd.cmd != "echo") + return false; + showMessage(MessageInfo, cmd.args); + return true; +} + +void +FakeVimHandler::Private::handleExCommand(const QString &line0) +{ + QString line = line0; // Make sure we have a copy to prevent aliasing. + + if (line.endsWith('%')) { + line.chop(1); + int percent = line.toInt(); + setPosition(firstPositionInLine(percent * linesInDocument() / 100)); + clearMessage(); + return; + } + + //qDebug() << "CMD: " << cmd; + + enterCommandMode(g.returnToMode); + + beginLargeEditBlock(); + ExCommand cmd; + QString lastCommand = line; + while (parseExCommand(&line, &cmd)) { + if (!handleExCommandHelper(cmd)) { + showMessage(MessageError, Tr::tr("Not an editor command: %1").arg(lastCommand)); + break; + } + lastCommand = line; + } + + // if the last command closed the editor, we would crash here (:vs and then :on) + if (!(m_textedit || m_plaintextedit)) + return; + + endEditBlock(); + + if (isVisualMode()) + leaveVisualMode(); + leaveCurrentMode(); +} + +bool +FakeVimHandler::Private::handleExCommandHelper(ExCommand &cmd) +{ + return handleExPluginCommand(cmd) || handleExGotoCommand(cmd) || handleExBangCommand(cmd) || + handleExHistoryCommand(cmd) || handleExRegisterCommand(cmd) || + handleExYankDeleteCommand(cmd) || handleExChangeCommand(cmd) || + handleExMoveCommand(cmd) || handleExJoinCommand(cmd) || handleExMapCommand(cmd) || + handleExNohlsearchCommand(cmd) || handleExNormalCommand(cmd) || + handleExReadCommand(cmd) || handleExUndoRedoCommand(cmd) || handleExSetCommand(cmd) || + handleExShiftCommand(cmd) || handleExSortCommand(cmd) || handleExSourceCommand(cmd) || + handleExSubstituteCommand(cmd) || handleExTabNextCommand(cmd) || + handleExTabPreviousCommand(cmd) || handleExWriteCommand(cmd) || handleExEchoCommand(cmd); +} + +bool +FakeVimHandler::Private::handleExPluginCommand(const ExCommand &cmd) +{ + bool handled = false; + int pos = m_cursor.position(); + commitCursor(); + q->handleExCommandRequested(&handled, cmd); + //qDebug() << "HANDLER REQUEST: " << cmd.cmd << handled; + if (handled && (m_textedit || m_plaintextedit)) { + pullCursor(); + if (m_cursor.position() != pos) + recordJump(pos); + } + return handled; +} + +void +FakeVimHandler::Private::searchBalanced(bool forward, QChar needle, QChar other) +{ + int level = 1; + int pos = position(); + const int npos = forward ? lastPositionInDocument() : 0; + while (true) { + if (forward) + ++pos; + else + --pos; + if (pos == npos) + return; + QChar c = characterAt(pos); + if (c == other) + ++level; + else if (c == needle) + --level; + if (level == 0) { + const int oldLine = cursorLine() - cursorLineOnScreen(); + // Making this unconditional feels better, but is not "vim like". + if (oldLine != cursorLine() - cursorLineOnScreen()) + scrollToLine(cursorLine() - linesOnScreen() / 2); + recordJump(); + setPosition(pos); + setTargetColumn(); + return; + } + } +} + +QTextCursor +FakeVimHandler::Private::search(const SearchData &sd, int startPos, int count, bool showMessages) +{ + const QRegularExpression needleExp = vimPatternToQtPattern(sd.needle); + + if (!needleExp.isValid()) { + if (showMessages) { + QString error = needleExp.errorString(); + showMessage(MessageError, Tr::tr("Invalid regular expression: %1").arg(error)); + } + if (sd.highlightMatches) + highlightMatches(QString()); + return QTextCursor(); + } + + int repeat = count; + const int pos = startPos + (sd.forward ? 1 : -1); + + QTextCursor tc; + if (pos >= 0 && pos < document()->characterCount()) { + tc = QTextCursor(document()); + tc.setPosition(pos); + if (sd.forward && afterEndOfLine(document(), pos)) + tc.movePosition(QTextCursor::Right); + + if (!tc.isNull()) { + if (sd.forward) + searchForward(&tc, needleExp, &repeat); + else + searchBackward(&tc, needleExp, &repeat); + } + } + + if (tc.isNull()) { + if (s.wrapScan.value()) { + tc = QTextCursor(document()); + tc.movePosition(sd.forward ? QTextCursor::Start : QTextCursor::End); + if (sd.forward) + searchForward(&tc, needleExp, &repeat); + else + searchBackward(&tc, needleExp, &repeat); + if (tc.isNull()) { + if (showMessages) { + showMessage(MessageError, Tr::tr("Pattern not found: %1").arg(sd.needle)); + } + } else if (showMessages) { + QString msg = sd.forward ? Tr::tr("Search hit BOTTOM, continuing at TOP.") + : Tr::tr("Search hit TOP, continuing at BOTTOM."); + showMessage(MessageWarning, msg); + } + } else if (showMessages) { + QString msg = sd.forward ? Tr::tr("Search hit BOTTOM without match for: %1") + : Tr::tr("Search hit TOP without match for: %1"); + showMessage(MessageError, msg.arg(sd.needle)); + } + } + + if (sd.highlightMatches) + highlightMatches(needleExp.pattern()); + + return tc; +} + +void +FakeVimHandler::Private::search(const SearchData &sd, bool showMessages) +{ + const int oldLine = cursorLine() - cursorLineOnScreen(); + + QTextCursor tc = search(sd, m_searchStartPosition, count(), showMessages); + if (tc.isNull()) { + tc = m_cursor; + tc.setPosition(m_searchStartPosition); + } + + if (isVisualMode()) { + int dLocal = tc.anchor() - tc.position(); + setPosition(tc.position() + dLocal); + } else { + // Set Cursor. In contrast to the main editor we have the cursor + // position before the anchor position. + setAnchorAndPosition(tc.position(), tc.anchor()); + } + + // Making this unconditional feels better, but is not "vim like". + if (oldLine != cursorLine() - cursorLineOnScreen()) + scrollToLine(cursorLine() - linesOnScreen() / 2); + + m_searchCursor = m_cursor; + + setTargetColumn(); +} + +bool +FakeVimHandler::Private::searchNext(bool forward) +{ + SearchData sd; + sd.needle = g.lastSearch; + sd.forward = forward ? g.lastSearchForward : !g.lastSearchForward; + sd.highlightMatches = true; + m_searchStartPosition = position(); + showMessage(MessageCommand, QLatin1Char(g.lastSearchForward ? '/' : '?') + sd.needle); + recordJump(); + search(sd); + return finishSearch(); +} + +void +FakeVimHandler::Private::highlightMatches(const QString &needle) +{ + g.lastNeedle = needle; + g.highlightsCleared = false; + updateHighlights(); +} + +void +FakeVimHandler::Private::moveToFirstNonBlankOnLine() +{ + g.movetype = MoveLineWise; + moveToFirstNonBlankOnLine(&m_cursor); + setTargetColumn(); +} + +void +FakeVimHandler::Private::moveToFirstNonBlankOnLine(QTextCursor *tc) +{ + tc->setPosition(tc->block().position(), QTextCursor::KeepAnchor); + moveToNonBlankOnLine(tc); +} + +void +FakeVimHandler::Private::moveToFirstNonBlankOnLineVisually() +{ + moveToStartOfLineVisually(); + moveToNonBlankOnLine(&m_cursor); + setTargetColumn(); +} + +void +FakeVimHandler::Private::moveToNonBlankOnLine(QTextCursor *tc) +{ + const QTextBlock block = tc->block(); + const int maxPos = block.position() + block.length() - 1; + int i = tc->position(); + while (characterAt(i).isSpace() && i < maxPos) + ++i; + tc->setPosition(i, QTextCursor::KeepAnchor); +} + +void +FakeVimHandler::Private::indentSelectedText(QChar typedChar) +{ + beginEditBlock(); + setTargetColumn(); + int beginLine = qMin(lineForPosition(position()), lineForPosition(anchor())); + int endLine = qMax(lineForPosition(position()), lineForPosition(anchor())); + + Range range(anchor(), position(), g.rangemode); + indentText(range, typedChar); + + setPosition(firstPositionInLine(beginLine)); + handleStartOfLine(); + setTargetColumn(); + setDotCommand("%1==", endLine - beginLine + 1); + endEditBlock(); + + const int lines = endLine - beginLine + 1; + if (lines > 2) + showMessage(MessageInfo, Tr::tr("%n lines indented.", nullptr, lines)); +} + +void +FakeVimHandler::Private::indentText(const Range &range, QChar typedChar) +{ + int beginBlock = blockAt(range.beginPos).blockNumber(); + int endBlock = blockAt(range.endPos).blockNumber(); + if (beginBlock > endBlock) + std::swap(beginBlock, endBlock); + + // Don't remember current indentation in last text insertion. + const QString lastInsertion = m_buffer->lastInsertion; + q->indentRegion(beginBlock, endBlock, typedChar); + m_buffer->lastInsertion = lastInsertion; +} + +bool +FakeVimHandler::Private::isElectricCharacter(QChar c) const +{ + bool result = false; + q->checkForElectricCharacter(&result, c); + return result; +} + +void +FakeVimHandler::Private::shiftRegionRight(int repeat) +{ + int beginLine = lineForPosition(anchor()); + int endLine = lineForPosition(position()); + int targetPos = anchor(); + if (beginLine > endLine) { + std::swap(beginLine, endLine); + targetPos = position(); + } + if (s.startOfLine.value()) + targetPos = firstPositionInLine(beginLine); + + const int sw = static_cast<int>(s.shiftWidth.value()); + g.movetype = MoveLineWise; + beginEditBlock(); + QTextBlock block = document()->findBlockByLineNumber(beginLine - 1); + while (block.isValid() && lineNumber(block) <= endLine) { + const Column col = indentation(block.text()); + QTextCursor tc = m_cursor; + tc.setPosition(block.position()); + if (col.physical > 0) + tc.setPosition(tc.position() + col.physical, QTextCursor::KeepAnchor); + tc.insertText(tabExpand(col.logical + sw * repeat)); + block = block.next(); + } + endEditBlock(); + + setPosition(targetPos); + handleStartOfLine(); + + const int lines = endLine - beginLine + 1; + if (lines > 2) { + showMessage(MessageInfo, Tr::tr("%n lines %1ed %2 time.", nullptr, lines) + .arg(repeat > 0 ? '>' : '<') + .arg(qAbs(repeat))); + } +} + +void +FakeVimHandler::Private::shiftRegionLeft(int repeat) +{ + shiftRegionRight(-repeat); +} + +void +FakeVimHandler::Private::moveToTargetColumn() +{ + const QTextBlock &bl = block(); + //Column column = cursorColumn(); + //int logical = logical + const int pos = lastPositionInLine(bl.blockNumber() + 1, false); + if (m_targetColumn == -1) { + setPosition(pos); + return; + } + const int physical = bl.position() + logicalToPhysicalColumn(m_targetColumn, bl.text()); + //qDebug() << "CORRECTING COLUMN FROM: " << logical << "TO" << m_targetColumn; + setPosition(qMin(pos, physical)); +} + +void +FakeVimHandler::Private::setTargetColumn() +{ + m_targetColumn = logicalCursorColumn(); + m_visualTargetColumn = m_targetColumn; + + QTextCursor tc = m_cursor; + tc.movePosition(QTextCursor::StartOfLine); + m_targetColumnWrapped = m_cursor.position() - tc.position(); +} + +/* if simple is given: + * class 0: spaces + * class 1: non-spaces + * else + * class 0: spaces + * class 1: non-space-or-letter-or-number + * class 2: letter-or-number + */ + +int +FakeVimHandler::Private::charClass(QChar c, bool simple) const +{ + if (simple) + return c.isSpace() ? 0 : 1; + // FIXME: This means that only characters < 256 in the + // ConfigIsKeyword setting are handled properly. + if (c.unicode() < 256) { + //int old = (c.isLetterOrNumber() || c.unicode() == '_') ? 2 + // : c.isSpace() ? 0 : 1; + //qDebug() << c.unicode() << old << m_charClass[c.unicode()]; + return m_charClass[c.unicode()]; + } + if (c.isLetterOrNumber() || c == '_') + return 2; + return c.isSpace() ? 0 : 1; +} + +void +FakeVimHandler::Private::miniBufferTextEdited(const QString &text, int cursorPos, int anchorPos) +{ + if (!isCommandLineMode()) { + editor()->setFocus(); + } else if (text.isEmpty()) { + // editing cancelled + enterFakeVim(); + handleDefaultKey(Input(Qt::Key_Escape, Qt::NoModifier, QString())); + leaveFakeVim(); + editor()->setFocus(); + } else { + CommandBuffer &cmdBuf = (g.mode == ExMode) ? g.commandBuffer : g.searchBuffer; + int pos = qMax(1, cursorPos); + int anchor = anchorPos == -1 ? pos : qMax(1, anchorPos); + QString buffer = text; + // prepend prompt character if missing + if (!buffer.startsWith(cmdBuf.prompt())) { + buffer.prepend(cmdBuf.prompt()); + ++pos; + ++anchor; + } + // update command/search buffer + cmdBuf.setContents(buffer.mid(1), pos - 1, anchor - 1); + if (pos != cursorPos || anchor != anchorPos || buffer != text) + q->commandBufferChanged(buffer, pos, anchor, 0); + // update search expression + if (g.subsubmode == SearchSubSubMode) { + updateFind(false); + commitCursor(); + } + } +} + +void +FakeVimHandler::Private::pullOrCreateBufferData() +{ + const QVariant data = document()->property("FakeVimSharedData"); + if (data.isValid()) { + // FakeVimHandler has been already created for this document (e.g. in other split). + m_buffer = data.value<BufferDataPtr>(); + } else { + // FakeVimHandler has not been created for this document yet. + m_buffer = BufferDataPtr(new BufferData); + document()->setProperty("FakeVimSharedData", QVariant::fromValue(m_buffer)); + } + + if (editor()->hasFocus()) + m_buffer->currentHandler = this; +} + +// Helper to parse a-z,A-Z,48-57,_ +static int +someInt(const QString &str) +{ + if (str.toInt()) + return str.toInt(); + if (!str.isEmpty()) + return str.at(0).unicode(); + return 0; +} + +void +FakeVimHandler::Private::setupCharClass() +{ + for (int i = 0; i < 256; ++i) { + const QChar c = QLatin1Char(static_cast<char>(i)); + m_charClass[i] = c.isSpace() ? 0 : 1; + } + const QString conf = s.isKeyword.value(); + for (const QString &part : conf.split(',')) { + if (part.contains('-')) { + const int from = someInt(part.section('-', 0, 0)); + const int to = someInt(part.section('-', 1, 1)); + for (int i = qMax(0, from); i <= qMin(255, to); ++i) + m_charClass[i] = 2; + } else { + m_charClass[qMin(255, someInt(part))] = 2; + } + } +} + +void +FakeVimHandler::Private::moveToBoundary(bool simple, bool forward) +{ + QTextCursor tc(document()); + tc.setPosition(position()); + if (forward ? tc.atBlockEnd() : tc.atBlockStart()) + return; + + QChar c = characterAt(tc.position() + (forward ? -1 : 1)); + int lastClass = tc.atStart() ? -1 : charClass(c, simple); + QTextCursor::MoveOperation op = forward ? QTextCursor::Right : QTextCursor::Left; + while (true) { + c = characterAt(tc.position()); + int thisClass = charClass(c, simple); + if (thisClass != lastClass || (forward ? tc.atBlockEnd() : tc.atBlockStart())) { + if (tc != m_cursor) + tc.movePosition(forward ? QTextCursor::Left : QTextCursor::Right); + break; + } + lastClass = thisClass; + tc.movePosition(op); + } + setPosition(tc.position()); +} + +void +FakeVimHandler::Private::moveToNextBoundary(bool end, int count, bool simple, bool forward) +{ + int repeat = count; + while (repeat > 0 && !(forward ? atDocumentEnd() : atDocumentStart())) { + setPosition(position() + (forward ? 1 : -1)); + moveToBoundary(simple, forward); + if (atBoundary(end, simple)) + --repeat; + } +} + +void +FakeVimHandler::Private::moveToNextBoundaryStart(int count, bool simple, bool forward) +{ + moveToNextBoundary(false, count, simple, forward); +} + +void +FakeVimHandler::Private::moveToNextBoundaryEnd(int count, bool simple, bool forward) +{ + moveToNextBoundary(true, count, simple, forward); +} + +void +FakeVimHandler::Private::moveToBoundaryStart(int count, bool simple, bool forward) +{ + moveToNextBoundaryStart(atBoundary(false, simple) ? count - 1 : count, simple, forward); +} + +void +FakeVimHandler::Private::moveToBoundaryEnd(int count, bool simple, bool forward) +{ + moveToNextBoundaryEnd(atBoundary(true, simple) ? count - 1 : count, simple, forward); +} + +void +FakeVimHandler::Private::moveToNextWord(bool end, int count, bool simple, bool forward, + bool emptyLines) +{ + int repeat = count; + while (repeat > 0 && !(forward ? atDocumentEnd() : atDocumentStart())) { + setPosition(position() + (forward ? 1 : -1)); + moveToBoundary(simple, forward); + if (atWordBoundary(end, simple) && (emptyLines || !atEmptyLine())) + --repeat; + } +} + +void +FakeVimHandler::Private::moveToNextWordStart(int count, bool simple, bool forward, bool emptyLines) +{ + g.movetype = MoveExclusive; + moveToNextWord(false, count, simple, forward, emptyLines); + setTargetColumn(); +} + +void +FakeVimHandler::Private::moveToNextWordEnd(int count, bool simple, bool forward, bool emptyLines) +{ + g.movetype = MoveInclusive; + moveToNextWord(true, count, simple, forward, emptyLines); + setTargetColumn(); +} + +void +FakeVimHandler::Private::moveToWordStart(int count, bool simple, bool forward, bool emptyLines) +{ + moveToNextWordStart(atWordStart(simple) ? count - 1 : count, simple, forward, emptyLines); +} + +void +FakeVimHandler::Private::moveToWordEnd(int count, bool simple, bool forward, bool emptyLines) +{ + moveToNextWordEnd(atWordEnd(simple) ? count - 1 : count, simple, forward, emptyLines); +} + +bool +FakeVimHandler::Private::handleFfTt(const QString &key, bool repeats) +{ + int key0 = key.size() == 1 ? key.at(0).unicode() : 0; + // g.subsubmode \\in { 'f', 'F', 't', 'T' } + bool forward = g.subsubdata.is('f') || g.subsubdata.is('t'); + bool exclusive = g.subsubdata.is('t') || g.subsubdata.is('T'); + int repeat = count(); + int n = block().position() + (forward ? block().length() : -1); + const int dLocal = forward ? 1 : -1; + // FIXME: This also depends on whether 'cpositions' Vim option contains ';'. + const int skip = (repeats && repeat == 1 && exclusive) ? dLocal : 0; + int pos = position() + dLocal + skip; + + for (; repeat > 0 && (forward ? pos < n : pos > n); pos += dLocal) { + if (characterAt(pos).unicode() == key0) + --repeat; + } + + if (repeat == 0) { + setPosition(pos - dLocal - (exclusive ? dLocal : 0)); + setTargetColumn(); + return true; + } + + return false; +} + +void +FakeVimHandler::Private::moveToMatchingParanthesis() +{ + bool moved = false; + bool forward = false; + + const int anc = anchor(); + QTextCursor tc = m_cursor; + + // If no known parenthesis symbol is under cursor find one on the current line after cursor. + static const QString parenthesesChars("([{}])"); + while (!parenthesesChars.contains(characterAt(tc.position())) && !tc.atBlockEnd()) + tc.setPosition(tc.position() + 1); + + if (tc.atBlockEnd()) + tc = m_cursor; + + q->moveToMatchingParenthesis(&moved, &forward, &tc); + if (moved) { + if (forward) + tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1); + setAnchorAndPosition(anc, tc.position()); + setTargetColumn(); + } +} + +int +FakeVimHandler::Private::cursorLineOnScreen() const +{ + if (!editor()) + return 0; + const QRect rect = EDITOR(cursorRect(m_cursor)); + return rect.height() > 0 ? rect.y() / rect.height() : 0; +} + +int +FakeVimHandler::Private::linesOnScreen() const +{ + if (!editor()) + return 1; + const int h = EDITOR(cursorRect(m_cursor)).height(); + return h > 0 ? EDITOR(viewport()->height()) / h : 1; +} + +int +FakeVimHandler::Private::cursorLine() const +{ + return lineForPosition(position()) - 1; +} + +int +FakeVimHandler::Private::cursorBlockNumber() const +{ + return blockAt(qMin(anchor(), position())).blockNumber(); +} + +int +FakeVimHandler::Private::physicalCursorColumn() const +{ + return position() - block().position(); +} + +int +FakeVimHandler::Private::physicalToLogicalColumn(const int physical, const QString &line) const +{ + const int ts = static_cast<int>(s.tabStop.value()); + int p = 0; + int logical = 0; + while (p < physical) { + QChar c = line.at(p); + //if (c == ' ') + // ++logical; + //else + if (c == '\t') + logical += ts - logical % ts; + else + ++logical; + //break; + ++p; + } + return logical; +} + +int +FakeVimHandler::Private::logicalToPhysicalColumn(const int logical, const QString &line) const +{ + const int ts = static_cast<int>(s.tabStop.value()); + int physical = 0; + for (int l = 0; l < logical && physical < line.size(); ++physical) { + QChar c = line.at(physical); + if (c == '\t') + l += ts - l % ts; + else + ++l; + } + return physical; +} + +int +FakeVimHandler::Private::windowScrollOffset() const +{ + return qMin(static_cast<int>(s.scrollOff.value()), linesOnScreen() / 2); +} + +int +FakeVimHandler::Private::logicalCursorColumn() const +{ + const int physical = physicalCursorColumn(); + const QString line = block().text(); + return physicalToLogicalColumn(physical, line); +} + +Column +FakeVimHandler::Private::cursorColumn() const +{ + return Column(physicalCursorColumn(), logicalCursorColumn()); +} + +int +FakeVimHandler::Private::linesInDocument() const +{ + if (m_cursor.isNull()) + return 0; + return document()->blockCount(); +} + +void +FakeVimHandler::Private::scrollToLine(int line) +{ + // Don't scroll if the line is already at the top. + updateFirstVisibleLine(); + if (line == m_firstVisibleLine) + return; + + const QTextCursor tc = m_cursor; + + QTextCursor tc2 = tc; + tc2.setPosition(document()->lastBlock().position()); + EDITOR(setTextCursor(tc2)); + EDITOR(ensureCursorVisible()); + + int offset = 0; + const QTextBlock block = document()->findBlockByLineNumber(line); + if (block.isValid()) { + const int blockLineCount = block.layout()->lineCount(); + const int lineInBlock = line - block.firstLineNumber(); + if (0 <= lineInBlock && lineInBlock < blockLineCount) { + QTextLine textLine = block.layout()->lineAt(lineInBlock); + offset = textLine.textStart(); + } else { + // QTC_CHECK(false); + } + } + tc2.setPosition(block.position() + offset); + EDITOR(setTextCursor(tc2)); + EDITOR(ensureCursorVisible()); + + EDITOR(setTextCursor(tc)); + + m_firstVisibleLine = line; +} + +void +FakeVimHandler::Private::updateFirstVisibleLine() +{ + const QTextCursor tc = EDITOR(cursorForPosition(QPoint(0, 0))); + m_firstVisibleLine = lineForPosition(tc.position()) - 1; +} + +int +FakeVimHandler::Private::firstVisibleLine() const +{ + return m_firstVisibleLine; +} + +int +FakeVimHandler::Private::lastVisibleLine() const +{ + const int line = m_firstVisibleLine + linesOnScreen(); + const QTextBlock block = document()->findBlockByLineNumber(line); + return block.isValid() ? line : document()->lastBlock().firstLineNumber(); +} + +int +FakeVimHandler::Private::lineOnTop(int count) const +{ + const int scrollOffset = qMax(count - 1, windowScrollOffset()); + const int line = firstVisibleLine(); + return line == 0 ? count - 1 : scrollOffset + line; +} + +int +FakeVimHandler::Private::lineOnBottom(int count) const +{ + const int scrollOffset = qMax(count - 1, windowScrollOffset()); + const int line = lastVisibleLine(); + return line >= document()->lastBlock().firstLineNumber() ? line - count + 1 + : line - scrollOffset - 1; +} + +void +FakeVimHandler::Private::scrollUp(int count) +{ + scrollToLine(cursorLine() - cursorLineOnScreen() - count); +} + +void +FakeVimHandler::Private::updateScrollOffset() +{ + const int line = cursorLine(); + if (line < lineOnTop()) + scrollToLine(qMax(0, line - windowScrollOffset())); + else if (line > lineOnBottom()) + scrollToLine(firstVisibleLine() + line - lineOnBottom()); +} + +void +FakeVimHandler::Private::alignViewportToCursor(AlignmentFlag align, int line, bool moveToNonBlank) +{ + if (line > 0) + setPosition(firstPositionInLine(line)); + if (moveToNonBlank) + moveToFirstNonBlankOnLine(); + + if (align == Qt::AlignTop) + scrollUp(-cursorLineOnScreen()); + else if (align == Qt::AlignVCenter) + scrollUp(linesOnScreen() / 2 - cursorLineOnScreen()); + else if (align == Qt::AlignBottom) + scrollUp(linesOnScreen() - cursorLineOnScreen() - 1); +} + +int +FakeVimHandler::Private::lineToBlockNumber(int line) const +{ + return document()->findBlockByLineNumber(line).blockNumber(); +} + +void +FakeVimHandler::Private::setCursorPosition(const CursorPosition &p) +{ + const int firstLine = firstVisibleLine(); + const int firstBlock = lineToBlockNumber(firstLine); + const int lastBlock = lineToBlockNumber(firstLine + linesOnScreen() - 2); + bool isLineVisible = firstBlock <= p.line && p.line <= lastBlock; + setCursorPosition(&m_cursor, p); + if (!isLineVisible) + alignViewportToCursor(Qt::AlignVCenter); +} + +void +FakeVimHandler::Private::setCursorPosition(QTextCursor *tc, const CursorPosition &p) +{ + const int line = qMin(document()->blockCount() - 1, p.line); + QTextBlock block = document()->findBlockByNumber(line); + const int column = qMin(p.column, block.length() - 1); + tc->setPosition(block.position() + column, QTextCursor::KeepAnchor); +} + +int +FakeVimHandler::Private::lastPositionInDocument(bool ignoreMode) const +{ + return document()->characterCount() - (ignoreMode || isVisualMode() || isInsertMode() ? 1 : 2); +} + +QString +FakeVimHandler::Private::selectText(const Range &range) const +{ + QString contents; + const QString lineEnd = range.rangemode == RangeBlockMode ? QString('\n') : QString(); + QTextCursor tc = m_cursor; + transformText(range, tc, [&tc, &contents, &lineEnd]() { + contents.append(tc.selection().toPlainText() + lineEnd); + }); + return contents; +} + +void +FakeVimHandler::Private::yankText(const Range &range, int reg) +{ + const QString text = selectText(range); + setRegister(reg, text, range.rangemode); + + // If register is not specified or " ... + if (m_register == '"') { + // with delete and change commands set register 1 (if text contains more lines) or + // small delete register - + if (g.submode == DeleteSubMode || g.submode == ChangeSubMode) { + if (text.contains('\n')) + setRegister('1', text, range.rangemode); + else + setRegister('-', text, range.rangemode); + } else { + // copy to yank register 0 too + setRegister('0', text, range.rangemode); + } + } else if (m_register != '_') { + // Always copy to " register too (except black hole register). + setRegister('"', text, range.rangemode); + } + + const int lines = + blockAt(range.endPos).blockNumber() - blockAt(range.beginPos).blockNumber() + 1; + if (lines > 2) + showMessage(MessageInfo, Tr::tr("%n lines yanked.", nullptr, lines)); +} + +void +FakeVimHandler::Private::transformText(const Range &range, QTextCursor &tc, + const std::function<void()> &transform) const +{ + switch (range.rangemode) { + case RangeCharMode: { + // This can span multiple lines. + tc.setPosition(range.beginPos, QTextCursor::MoveAnchor); + tc.setPosition(range.endPos, QTextCursor::KeepAnchor); + transform(); + tc.setPosition(range.beginPos); + break; + } + case RangeLineMode: + case RangeLineModeExclusive: { + tc.setPosition(range.beginPos, QTextCursor::MoveAnchor); + tc.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor); + tc.setPosition(range.endPos, QTextCursor::KeepAnchor); + tc.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); + if (range.rangemode != RangeLineModeExclusive) { + // make sure that complete lines are removed + // - also at the beginning and at the end of the document + if (tc.atEnd()) { + tc.setPosition(range.beginPos, QTextCursor::MoveAnchor); + tc.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor); + if (!tc.atStart()) { + // also remove first line if it is the only one + tc.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1); + tc.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor, 1); + } + tc.setPosition(range.endPos, QTextCursor::KeepAnchor); + tc.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); + } else { + tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1); + } + } + const int posAfter = tc.anchor(); + transform(); + tc.setPosition(posAfter); + break; + } + case RangeBlockAndTailMode: + case RangeBlockMode: { + int beginColumn = columnAt(range.beginPos); + int endColumn = columnAt(range.endPos); + if (endColumn < beginColumn) + std::swap(beginColumn, endColumn); + if (range.rangemode == RangeBlockAndTailMode) + endColumn = INT_MAX - 1; + QTextBlock block = document()->findBlock(range.beginPos); + const QTextBlock lastBlock = document()->findBlock(range.endPos); + while (block.isValid() && block.position() <= lastBlock.position()) { + int bCol = qMin(beginColumn, block.length() - 1); + int eCol = qMin(endColumn + 1, block.length() - 1); + tc.setPosition(block.position() + bCol, QTextCursor::MoveAnchor); + tc.setPosition(block.position() + eCol, QTextCursor::KeepAnchor); + transform(); + block = block.next(); + } + tc.setPosition(range.beginPos); + break; + } + } +} + +void +FakeVimHandler::Private::transformText(const Range &range, const Transformation &transform) +{ + beginEditBlock(); + transformText(range, m_cursor, [this, &transform] { + m_cursor.insertText(transform(m_cursor.selection().toPlainText())); + }); + endEditBlock(); + setTargetColumn(); +} + +void +FakeVimHandler::Private::insertText(QTextCursor &tc, const QString &text) +{ + if (s.passKeys.value()) { + if (tc.hasSelection() && text.isEmpty()) { + QKeyEvent event(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier, QString()); + passEventToEditor(event, tc); + } + + for (QChar c : text) { + QKeyEvent event(QEvent::KeyPress, -1, Qt::NoModifier, QString(c)); + passEventToEditor(event, tc); + } + } else { + tc.insertText(text); + } +} + +void +FakeVimHandler::Private::insertText(const Register ®) +{ + if (reg.rangemode != RangeCharMode) { + qWarning() << "WRONG INSERT MODE: " << reg.rangemode; + return; + } + setAnchor(); + m_cursor.insertText(reg.contents); + //dump("AFTER INSERT"); +} + +void +FakeVimHandler::Private::removeText(const Range &range) +{ + transformText(range, [](const QString &) { return QString(); }); +} + +void +FakeVimHandler::Private::downCase(const Range &range) +{ + transformText(range, [](const QString &text) { return text.toLower(); }); +} + +void +FakeVimHandler::Private::upCase(const Range &range) +{ + transformText(range, [](const QString &text) { return text.toUpper(); }); +} + +void +FakeVimHandler::Private::invertCase(const Range &range) +{ + transformText(range, [](const QString &text) -> QString { + QString result = text; + for (int i = 0; i < result.length(); ++i) { + const QChar c = result[i]; + result[i] = c.isUpper() ? c.toLower() : c.toUpper(); + } + return result; + }); +} + +void +FakeVimHandler::Private::toggleComment(const Range &range) +{ + static const QMap<QString, QString> extensionToCommentString{ + { "pri", "#" }, { "pro", "#" }, { "h", "//" }, { "hpp", "//" }, { "cpp", "//" }, + }; + const QString commentString = + extensionToCommentString.value(QFileInfo(m_currentFileName).suffix(), "//"); + + transformText(range, [&commentString](const QString &text) -> QString { + QStringList lines = text.split('\n'); + + const QRegularExpression checkForComment("^\\s*" + + QRegularExpression::escape(commentString)); + + const bool firstLineIsComment = !lines.empty() && lines.front().contains(checkForComment); + + for (auto &line : lines) { + if (!line.isEmpty()) { + if (firstLineIsComment) { + const bool hasSpaceAfterCommentString = + line.contains(QRegularExpression(checkForComment.pattern() + "\\s")); + const int sizeToReplace = hasSpaceAfterCommentString ? commentString.size() + 1 + : commentString.size(); + line.replace(line.indexOf(commentString), sizeToReplace, ""); + } else { + const int indexOfFirstNonSpace = line.indexOf(QRegularExpression("[^\\s]")); + line = line.left(indexOfFirstNonSpace) + commentString + " " + + line.right(line.size() - indexOfFirstNonSpace); + } + } + } + + return lines.size() == 1 ? lines.front() : lines.join("\n"); + }); +} + +void +FakeVimHandler::Private::exchangeRange(const Range &range) +{ + if (g.exchangeRange) { + pushUndoState(false); + beginEditBlock(); + + Range leftRange = *g.exchangeRange; + Range rightRange = range; + if (leftRange.beginPos > rightRange.beginPos) + std::swap(leftRange, rightRange); + + // First replace the right range, then left one + // If we did it the other way around, we would invalidate the positions + // of the right range + const QString rightText = selectText(rightRange); + replaceText(rightRange, selectText(leftRange)); + replaceText(leftRange, rightText); + + g.exchangeRange.reset(); + + endEditBlock(); + } else { + g.exchangeRange = range; + } +} + +void +FakeVimHandler::Private::replaceWithRegister(const Range &range) +{ + replaceText(range, registerContents(m_register)); +} + +void +FakeVimHandler::Private::surroundCurrentRange(const Input &input, const QString &prefix) +{ + QString dotCommand; + if (isVisualMode()) + dotCommand = visualDotCommand() + "S" + input.asChar(); + + const bool wasVisualCharMode = isVisualCharMode(); + const bool wasVisualLineMode = isVisualLineMode(); + leaveVisualMode(); + + if (dotCommand.isEmpty()) { // i.e. we came from normal mode + dotCommand = dotCommandFromSubMode(g.submode) + + QLatin1Char(g.surroundUpperCaseS ? 'S' : 's') + g.dotCommand + input.asChar(); + } + + if (wasVisualCharMode) + setPosition(position() + 1); + + QString newFront, newBack; + + if (input.is('(') || input.is(')') || input.is('b')) { + newFront = '('; + newBack = ')'; + } else if (input.is('{') || input.is('}') || input.is('B')) { + newFront = '{'; + newBack = '}'; + } else if (input.is('[') || input.is(']')) { + newFront = '['; + newBack = ']'; + } else if (input.is('<') || input.is('>') || input.is('t')) { + newFront = '<'; + newBack = '>'; + } else if (input.is('"') || input.is('\'') || input.is('`')) { + newFront = input.asChar(); + newBack = input.asChar(); + } + + if (g.surroundUpperCaseS || wasVisualLineMode) { + // yS and cS add a new line before and after the surrounded text + newFront += "\n"; + if (wasVisualLineMode) + newBack += "\n"; + else + newBack = "\n" + newBack; + } else if (input.is('(') || input.is('{') || input.is('[') || input.is('[')) { + // Opening characters add an extra space + newFront = newFront + " "; + newBack = " " + newBack; + } + + if (!newFront.isEmpty()) { + transformText(currentRange(), [&](QString text) -> QString { + if (newFront == QChar()) + return text.mid(1, text.size() - 2); + + const QString newMiddle = + (g.submode == ChangeSurroundingSubMode) ? text.mid(1, text.size() - 2) : text; + + return prefix + newFront + newMiddle + newBack; + }); + } + + // yS, cS and VS also indent the surrounded text + if (g.surroundUpperCaseS || wasVisualLineMode) + replay(QStringLiteral("=a") + input.asChar()); + + // Indenting has changed the dotCommand, so now set it back to the correct one + g.dotCommand = dotCommand; +} + +void +FakeVimHandler::Private::replaceText(const Range &range, const QString &str) +{ + transformText(range, [&str](const QString &) { return str; }); +} + +void +FakeVimHandler::Private::pasteText(bool afterCursor) +{ + const QString text = registerContents(m_register); + const RangeMode rangeMode = registerRangeMode(m_register); + + beginEditBlock(); + + // In visual mode paste text only inside selection. + bool pasteAfter = isVisualMode() ? false : afterCursor; + + if (isVisualMode()) + cutSelectedText(g.submode == ReplaceWithRegisterSubMode ? '-' : '"'); + + switch (rangeMode) { + case RangeCharMode: { + m_targetColumn = 0; + const int pos = position() + 1; + if (pasteAfter && rightDist() > 0) + moveRight(); + insertText(text.repeated(count())); + if (text.contains('\n')) + setPosition(pos); + else + moveLeft(); + break; + } + case RangeLineMode: + case RangeLineModeExclusive: { + QTextCursor tc = m_cursor; + moveToStartOfLine(); + m_targetColumn = 0; + bool lastLine = false; + if (pasteAfter) { + lastLine = document()->lastBlock() == this->block(); + if (lastLine) { + tc.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor); + tc.insertBlock(); + } + moveDown(); + } + const int pos = position(); + if (lastLine) + insertText(text.repeated(count()).left(text.size() * count() - 1)); + else + insertText(text.repeated(count())); + setPosition(pos); + moveToFirstNonBlankOnLine(); + break; + } + case RangeBlockAndTailMode: + case RangeBlockMode: { + const int pos = position(); + if (pasteAfter && rightDist() > 0) + moveRight(); + QTextCursor tc = m_cursor; + const int col = tc.columnNumber(); + QTextBlock block = tc.block(); + const QStringList lines = text.split('\n'); + for (int i = 0; i < lines.size() - 1; ++i) { + if (!block.isValid()) { + tc.movePosition(QTextCursor::End); + tc.insertBlock(); + block = tc.block(); + } + + // resize line + int length = block.length(); + int begin = block.position(); + if (col >= length) { + tc.setPosition(begin + length - 1); + tc.insertText(QString(col - length + 1, ' ')); + } else { + tc.setPosition(begin + col); + } + + // insert text + const QString line = lines.at(i).repeated(count()); + tc.insertText(line); + + // next line + block = block.next(); + } + setPosition(pos); + if (pasteAfter) + moveRight(); + break; + } + } + + endEditBlock(); +} + +void +FakeVimHandler::Private::cutSelectedText(int reg) +{ + pushUndoState(); + + bool visualMode = isVisualMode(); + leaveVisualMode(); + + Range range = currentRange(); + if (visualMode && g.rangemode == RangeCharMode) + ++range.endPos; + + if (!reg) + reg = m_register; + + g.submode = DeleteSubMode; + yankText(range, reg); + removeText(range); + g.submode = NoSubMode; + + if (g.rangemode == RangeLineMode) + handleStartOfLine(); + else if (g.rangemode == RangeBlockMode) + setPosition(qMin(position(), anchor())); +} + +void +FakeVimHandler::Private::joinLines(int count, bool preserveSpace) +{ + int pos = position(); + const int blockNumber = m_cursor.blockNumber(); + + const QString currentLine = lineContents(blockNumber + 1); + const bool startingLineIsComment = + currentLine.contains(QRegularExpression("^\\s*\\/\\/")) // Cpp-style + || currentLine.contains(QRegularExpression("^\\s*\\/?\\*")) // C-style + || currentLine.contains(QRegularExpression("^\\s*#")); // Python/Shell-style + + for (int i = qMax(count - 2, 0); i >= 0 && blockNumber < document()->blockCount(); --i) { + moveBehindEndOfLine(); + pos = position(); + setAnchor(); + moveRight(); + if (preserveSpace) { + removeText(currentRange()); + } else { + while (characterAtCursor() == ' ' || characterAtCursor() == '\t') + moveRight(); + + // If the line we started from is a comment, remove the comment string from the next line + if (startingLineIsComment && s.formatOptions.value().contains('f')) { + if (characterAtCursor() == '/' && characterAt(position() + 1) == '/') + moveRight(2); + else if (characterAtCursor() == '*' || characterAtCursor() == '#') + moveRight(1); + + if (characterAtCursor() == ' ') + moveRight(); + } + + m_cursor.insertText(QString(' ')); + } + } + setPosition(pos); +} + +void +FakeVimHandler::Private::insertNewLine() +{ + if (m_buffer->editBlockLevel <= 1 && s.passKeys.value()) { + QKeyEvent event(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier, "\n"); + if (passEventToEditor(event, m_cursor)) + return; + } + + insertText(QString("\n")); + insertAutomaticIndentation(true); +} + +bool +FakeVimHandler::Private::handleInsertInEditor(const Input &input) +{ + if (m_buffer->editBlockLevel > 0 || !s.passKeys.value()) + return false; + + joinPreviousEditBlock(); + + QKeyEvent event(QEvent::KeyPress, input.key(), input.modifiers(), input.text()); + setAnchor(); + if (!passEventToEditor(event, m_cursor)) + return !m_textedit && !m_plaintextedit; // Mark event as handled if it has destroyed editor. + + endEditBlock(); + + setTargetColumn(); + + return true; +} + +bool +FakeVimHandler::Private::passEventToEditor(QEvent &event, QTextCursor &tc) +{ + removeEventFilter(); + q->requestDisableBlockSelection(); + + setThinCursor(); + EDITOR(setTextCursor(tc)); + + bool accepted = QApplication::sendEvent(editor(), &event); + if (!m_textedit && !m_plaintextedit) + return false; + + if (accepted) + tc = editorCursor(); + + return accepted; +} + +QString +FakeVimHandler::Private::lineContents(int line) const +{ + return document()->findBlockByLineNumber(line - 1).text(); +} + +QString +FakeVimHandler::Private::textAt(int from, int to) const +{ + QTextCursor tc(document()); + tc.setPosition(from); + tc.setPosition(to, QTextCursor::KeepAnchor); + return tc.selectedText().replace(QChar::ParagraphSeparator, '\n'); +} + +void +FakeVimHandler::Private::setLineContents(int line, const QString &contents) +{ + QTextBlock block = document()->findBlockByLineNumber(line - 1); + QTextCursor tc = m_cursor; + const int begin = block.position(); + const int len = block.length(); + tc.setPosition(begin); + tc.setPosition(begin + len - 1, QTextCursor::KeepAnchor); + tc.insertText(contents); +} + +int +FakeVimHandler::Private::blockBoundary(const QString &left, const QString &right, bool closing, + int count) const +{ + const QString &begin = closing ? left : right; + const QString &end = closing ? right : left; + + // shift cursor if it is already on opening/closing string + QTextCursor tc1 = m_cursor; + int pos = tc1.position(); + int max = document()->characterCount(); + int sz = left.size(); + int from = qMax(pos - sz + 1, 0); + int to = qMin(pos + sz, max); + tc1.setPosition(from); + tc1.setPosition(to, QTextCursor::KeepAnchor); + int i = tc1.selectedText().indexOf(left); + if (i != -1) { + // - on opening string: + tc1.setPosition(from + i + sz); + } else { + sz = right.size(); + from = qMax(pos - sz + 1, 0); + to = qMin(pos + sz, max); + tc1.setPosition(from); + tc1.setPosition(to, QTextCursor::KeepAnchor); + i = tc1.selectedText().indexOf(right); + if (i != -1) { + // - on closing string: + tc1.setPosition(from + i); + } else { + tc1 = m_cursor; + } + } + + QTextCursor tc2 = tc1; + QTextDocument::FindFlags flags(closing ? 0 : QTextDocument::FindBackward); + int level = 0; + int counter = 0; + while (true) { + tc2 = document()->find(end, tc2, flags); + if (tc2.isNull()) + return -1; + if (!tc1.isNull()) + tc1 = document()->find(begin, tc1, flags); + + while (!tc1.isNull() && (closing ? (tc1 < tc2) : (tc2 < tc1))) { + ++level; + tc1 = document()->find(begin, tc1, flags); + } + + while (level > 0 && (tc1.isNull() || (closing ? (tc2 < tc1) : (tc1 < tc2)))) { + --level; + tc2 = document()->find(end, tc2, flags); + if (tc2.isNull()) + return -1; + } + + if (level == 0 && (tc1.isNull() || (closing ? (tc2 < tc1) : (tc1 < tc2)))) { + ++counter; + if (counter >= count) + break; + } + } + + return tc2.position() - end.size(); +} + +int +FakeVimHandler::Private::lineNumber(const QTextBlock &block) const +{ + if (block.isVisible()) + return block.firstLineNumber() + 1; + + // Folded block has line number of the nearest previous visible line. + QTextBlock block2 = block; + while (block2.isValid() && !block2.isVisible()) + block2 = block2.previous(); + return block2.firstLineNumber() + 1; +} + +int +FakeVimHandler::Private::columnAt(int pos) const +{ + return pos - blockAt(pos).position(); +} + +int +FakeVimHandler::Private::blockNumberAt(int pos) const +{ + return blockAt(pos).blockNumber(); +} + +QTextBlock +FakeVimHandler::Private::blockAt(int pos) const +{ + return document()->findBlock(pos); +} + +QTextBlock +FakeVimHandler::Private::nextLine(const QTextBlock &block) const +{ + return blockAt(block.position() + block.length()); +} + +QTextBlock +FakeVimHandler::Private::previousLine(const QTextBlock &block) const +{ + return blockAt(block.position() - 1); +} + +int +FakeVimHandler::Private::firstPositionInLine(int line, bool onlyVisibleLines) const +{ + QTextBlock block = onlyVisibleLines ? document()->findBlockByLineNumber(line - 1) + : document()->findBlockByNumber(line - 1); + return block.position(); +} + +int +FakeVimHandler::Private::lastPositionInLine(int line, bool onlyVisibleLines) const +{ + QTextBlock block; + if (onlyVisibleLines) { + block = document()->findBlockByLineNumber(line - 1); + // respect folds and wrapped lines + do { + block = nextLine(block); + } while (block.isValid() && !block.isVisible()); + if (block.isValid()) { + if (line > 0) + block = block.previous(); + } else { + block = document()->lastBlock(); + } + } else { + block = document()->findBlockByNumber(line - 1); + } + + const int position = block.position() + block.length() - 1; + if (block.length() > 1 && !isVisualMode() && !isInsertMode()) + return position - 1; + return position; +} + +int +FakeVimHandler::Private::lineForPosition(int pos) const +{ + const QTextBlock block = blockAt(pos); + if (!block.isValid()) + return 0; + const int positionInBlock = pos - block.position(); + const int lineNumberInBlock = block.layout()->lineForTextPosition(positionInBlock).lineNumber(); + return block.firstLineNumber() + lineNumberInBlock + 1; +} + +void +FakeVimHandler::Private::toggleVisualMode(VisualMode visualMode) +{ + if (visualMode == g.visualMode) { + leaveVisualMode(); + } else { + m_positionPastEnd = false; + m_anchorPastEnd = false; + g.visualMode = visualMode; + m_buffer->lastVisualMode = visualMode; + } +} + +void +FakeVimHandler::Private::leaveVisualMode() +{ + if (!isVisualMode()) + return; + + if (isVisualLineMode()) { + g.rangemode = RangeLineMode; + g.movetype = MoveLineWise; + } else if (isVisualCharMode()) { + g.rangemode = RangeCharMode; + g.movetype = MoveInclusive; + } else if (isVisualBlockMode()) { + g.rangemode = m_visualTargetColumn == -1 ? RangeBlockAndTailMode : RangeBlockMode; + g.movetype = MoveInclusive; + } + + g.visualMode = NoVisualMode; +} + +void +FakeVimHandler::Private::saveLastVisualMode() +{ + if (isVisualMode() && g.mode == CommandMode && g.submode == NoSubMode) { + setMark('<', markLessPosition()); + setMark('>', markGreaterPosition()); + m_buffer->lastVisualModeInverted = anchor() > position(); + m_buffer->lastVisualMode = g.visualMode; + } +} + +QWidget * +FakeVimHandler::Private::editor() const +{ + return m_textedit ? static_cast<QWidget *>(m_textedit) + : static_cast<QWidget *>(m_plaintextedit); +} + +void +FakeVimHandler::Private::joinPreviousEditBlock() +{ + UNDO_DEBUG("JOIN"); + if (m_buffer->breakEditBlock) { + beginEditBlock(); + QTextCursor tc(m_cursor); + tc.setPosition(tc.position()); + tc.beginEditBlock(); + tc.insertText("X"); + tc.deletePreviousChar(); + tc.endEditBlock(); + m_buffer->breakEditBlock = false; + } else { + if (m_buffer->editBlockLevel == 0 && !m_buffer->undo.empty()) + m_buffer->undoState = m_buffer->undo.pop(); + beginEditBlock(); + } +} + +void +FakeVimHandler::Private::beginEditBlock(bool largeEditBlock) +{ + UNDO_DEBUG("BEGIN EDIT BLOCK" << m_buffer->editBlockLevel + 1); + if (!largeEditBlock && !m_buffer->undoState.isValid()) + pushUndoState(false); + if (m_buffer->editBlockLevel == 0) + m_buffer->breakEditBlock = true; + ++m_buffer->editBlockLevel; +} + +void +FakeVimHandler::Private::endEditBlock() +{ + UNDO_DEBUG("END EDIT BLOCK" << m_buffer->editBlockLevel); + if (m_buffer->editBlockLevel <= 0) { + qWarning("beginEditBlock() not called before endEditBlock()!"); + return; + } + --m_buffer->editBlockLevel; + if (m_buffer->editBlockLevel == 0 && m_buffer->undoState.isValid()) { + m_buffer->undo.push(m_buffer->undoState); + m_buffer->undoState = State(); + } + if (m_buffer->editBlockLevel == 0) + m_buffer->breakEditBlock = false; +} + +void +FakeVimHandler::Private::onContentsChanged(int position, int charsRemoved, int charsAdded) +{ + // Record inserted and deleted text in insert mode. + if (isInsertMode() && (charsAdded > 0 || charsRemoved > 0) && canModifyBufferData()) { + BufferData::InsertState &insertState = m_buffer->insertState; + const int oldPosition = insertState.pos2; + if (!isInsertStateValid()) { + insertState.pos1 = oldPosition; + g.dotCommand = "i"; + resetCount(); + } + + // Ignore changes outside inserted text (e.g. renaming other occurrences of a variable). + if (position + charsRemoved >= insertState.pos1 && position <= insertState.pos2) { + if (charsRemoved > 0) { + // Assume that in a manual edit operation a text can be removed only + // in front of cursor (<DELETE>) or behind it (<BACKSPACE>). + + // If the recorded amount of backspace/delete keys doesn't correspond with + // number of removed characters, assume that the document has been changed + // externally and invalidate current insert state. + + const bool wholeDocumentChanged = charsRemoved > 1 && charsAdded > 0 && + charsAdded + 1 == document()->characterCount(); + + if (position < insertState.pos1) { + // <BACKSPACE> + const int backspaceCount = insertState.pos1 - position; + if (backspaceCount != charsRemoved || + (oldPosition == charsRemoved && wholeDocumentChanged)) { + invalidateInsertState(); + } else { + const QString inserted = textAt(position, oldPosition); + const QString removed = insertState.textBeforeCursor.right(backspaceCount); + // Ignore backspaces if same text was just inserted. + if (!inserted.endsWith(removed)) { + insertState.backspaces += backspaceCount; + insertState.pos1 = position; + insertState.pos2 = qMax(position, insertState.pos2 - backspaceCount); + } + } + } else if (position + charsRemoved > insertState.pos2) { + // <DELETE> + const int deleteCount = position + charsRemoved - insertState.pos2; + if (deleteCount != charsRemoved || (oldPosition == 0 && wholeDocumentChanged)) + invalidateInsertState(); + else + insertState.deletes += deleteCount; + } + } else if (charsAdded > 0 && insertState.insertingSpaces) { + for (int i = position; i < position + charsAdded; ++i) { + const QChar c = characterAt(i); + if (c.unicode() == ' ' || c.unicode() == '\t') + insertState.spaces.insert(i); + } + } + + const int newPosition = position + charsAdded; + insertState.pos2 = qMax(insertState.pos2 + charsAdded - charsRemoved, newPosition); + insertState.textBeforeCursor = textAt(block().position(), newPosition); + } + } + + if (!m_highlighted.isEmpty()) + q->highlightMatches(m_highlighted); +} + +void +FakeVimHandler::Private::onCursorPositionChanged() +{ + if (!m_inFakeVim) { + m_cursorNeedsUpdate = true; + + // Selecting text with mouse disables the thick cursor so it's more obvious + // that extra character under cursor is not selected when moving text around or + // making operations on text outside FakeVim mode. + setThinCursor(g.mode == InsertMode || editorCursor().hasSelection()); + } +} + +void +FakeVimHandler::Private::onUndoCommandAdded() +{ + if (!canModifyBufferData()) + return; + + // Undo commands removed? + UNDO_DEBUG("Undo added" + << "previous: REV" << m_buffer->lastRevision); + if (m_buffer->lastRevision >= revision()) { + UNDO_DEBUG("UNDO REMOVED!"); + const int removed = m_buffer->lastRevision - revision(); + for (int i = m_buffer->undo.size() - 1; i >= 0; --i) { + if ((m_buffer->undo[i].revision -= removed) < 0) { + m_buffer->undo.remove(0, i + 1); + break; + } + } + } + + m_buffer->redo.clear(); + // External change while FakeVim disabled. + if (m_buffer->editBlockLevel == 0 && !m_buffer->undo.isEmpty() && !isInsertMode()) + m_buffer->undo.push(State()); +} + +void +FakeVimHandler::Private::onInputTimeout() +{ + enterFakeVim(); + EventResult result = handleKey(Input()); + leaveFakeVim(result); +} + +void +FakeVimHandler::Private::onFixCursorTimeout() +{ + if (editor()) + fixExternalCursorPosition(editor()->hasFocus() && !isCommandLineMode()); +} + +char +FakeVimHandler::Private::currentModeCode() const +{ + if (g.mode == ExMode) + return 'c'; + else if (isVisualMode()) + return 'v'; + else if (isOperatorPending()) + return 'o'; + else if (g.mode == CommandMode) + return 'n'; + else if (g.submode != NoSubMode) + return ' '; + else + return 'i'; +} + +void +FakeVimHandler::Private::undoRedo(bool undo) +{ + UNDO_DEBUG((undo ? "UNDO" : "REDO")); + + // FIXME: That's only an approximaxtion. The real solution might + // be to store marks and old userData with QTextBlock setUserData + // and retrieve them afterward. + QStack<State> &stack = undo ? m_buffer->undo : m_buffer->redo; + QStack<State> &stack2 = undo ? m_buffer->redo : m_buffer->undo; + + State state = m_buffer->undoState.isValid() ? m_buffer->undoState + : !stack.empty() ? stack.pop() + : State(); + + CursorPosition lastPos(m_cursor); + if (undo ? !document()->isUndoAvailable() : !document()->isRedoAvailable()) { + const QString msg = + undo ? Tr::tr("Already at oldest change.") : Tr::tr("Already at newest change."); + showMessage(MessageInfo, msg); + UNDO_DEBUG(msg); + return; + } + clearMessage(); + + ++m_buffer->editBlockLevel; + + // Do undo/redo [count] times to reach previous revision. + const int previousRevision = revision(); + if (undo) { + do { + EDITOR(undo()); + } while (document()->isUndoAvailable() && state.revision >= 0 && + state.revision < revision()); + } else { + do { + EDITOR(redo()); + } while (document()->isRedoAvailable() && state.revision > revision()); + } + + --m_buffer->editBlockLevel; + + if (state.isValid()) { + Marks marks = m_buffer->marks; + marks.swap(state.marks); + updateMarks(marks); + m_buffer->lastVisualMode = state.lastVisualMode; + m_buffer->lastVisualModeInverted = state.lastVisualModeInverted; + setMark('.', state.position); + setMark('\'', lastPos); + setMark('`', lastPos); + setCursorPosition(state.position); + setAnchor(); + state.revision = previousRevision; + } else { + updateFirstVisibleLine(); + pullCursor(); + } + stack2.push(state); + + setTargetColumn(); + if (atEndOfLine()) + moveLeft(); + + UNDO_DEBUG((undo ? "UNDONE" : "REDONE")); +} + +void +FakeVimHandler::Private::undo() +{ + undoRedo(true); +} + +void +FakeVimHandler::Private::redo() +{ + undoRedo(false); +} + +void +FakeVimHandler::Private::updateCursorShape() +{ + setThinCursor(g.mode == InsertMode || isVisualLineMode() || isVisualBlockMode() || + isCommandLineMode() || !editor()->hasFocus()); +} + +void +FakeVimHandler::Private::setThinCursor(bool enable) +{ + EDITOR(setOverwriteMode(!enable)); +} + +bool +FakeVimHandler::Private::hasThinCursor() const +{ + return !EDITOR(overwriteMode()); +} + +void +FakeVimHandler::Private::enterReplaceMode() +{ + enterInsertOrReplaceMode(ReplaceMode); +} + +void +FakeVimHandler::Private::enterInsertMode() +{ + enterInsertOrReplaceMode(InsertMode); +} + +void +FakeVimHandler::Private::enterInsertOrReplaceMode(Mode mode) +{ + if (mode != InsertMode && mode != ReplaceMode) { + qWarning("Unexpected mode"); + return; + } + if (g.mode == mode) + return; + + g.mode = mode; + + if (g.returnToMode == mode) { + // Returning to insert mode after <C-O>. + clearCurrentMode(); + moveToTargetColumn(); + invalidateInsertState(); + } else { + // Entering insert mode from command mode. + if (mode == InsertMode) { + // m_targetColumn shouldn't be -1 (end of line). + if (m_targetColumn == -1) + setTargetColumn(); + } + + g.submode = NoSubMode; + g.subsubmode = NoSubSubMode; + g.returnToMode = mode; + clearLastInsertion(); + } +} + +void +FakeVimHandler::Private::enterVisualInsertMode(QChar command) +{ + if (isVisualBlockMode()) { + bool append = command == 'A'; + bool change = command == 's' || command == 'c'; + + leaveVisualMode(); + + const CursorPosition lastAnchor = markLessPosition(); + const CursorPosition lastPosition = markGreaterPosition(); + CursorPosition pos(lastAnchor.line, append + ? qMax(lastPosition.column, lastAnchor.column) + 1 + : qMin(lastPosition.column, lastAnchor.column)); + + if (append) { + m_visualBlockInsert = m_visualTargetColumn == -1 ? AppendToEndOfLineBlockInsertMode + : AppendBlockInsertMode; + } else if (change) { + m_visualBlockInsert = ChangeBlockInsertMode; + beginEditBlock(); + cutSelectedText(); + endEditBlock(); + } else { + m_visualBlockInsert = InsertBlockInsertMode; + } + + setCursorPosition(pos); + if (m_visualBlockInsert == AppendToEndOfLineBlockInsertMode) + moveBehindEndOfLine(); + } else { + m_visualBlockInsert = NoneBlockInsertMode; + leaveVisualMode(); + if (command == 'I') { + if (lineForPosition(anchor()) <= lineForPosition(position())) { + setPosition(qMin(anchor(), position())); + moveToStartOfLine(); + } + } else if (command == 'A') { + if (lineForPosition(anchor()) <= lineForPosition(position())) { + setPosition(position()); + moveRight(qMin(rightDist(), 1)); + } else { + setPosition(anchor()); + moveToStartOfLine(); + } + } + } + + setAnchor(); + if (m_visualBlockInsert != ChangeBlockInsertMode) + breakEditBlock(); + enterInsertMode(); +} + +void +FakeVimHandler::Private::enterCommandMode(Mode returnToMode) +{ + if (g.isRecording && isCommandLineMode()) + record(Input(Key_Escape, NoModifier)); + + if (isNoVisualMode()) { + if (atEndOfLine()) { + m_cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor); + if (m_targetColumn != -1) + setTargetColumn(); + } + setAnchor(); + } + + g.mode = CommandMode; + clearCurrentMode(); + g.returnToMode = returnToMode; + m_positionPastEnd = false; + m_anchorPastEnd = false; +} + +void +FakeVimHandler::Private::enterExMode(const QString &contents) +{ + g.currentMessage.clear(); + g.commandBuffer.clear(); + if (isVisualMode()) + g.commandBuffer.setContents(QString("'<,'>") + contents, contents.size() + 5); + else + g.commandBuffer.setContents(contents, contents.size()); + g.mode = ExMode; + g.submode = NoSubMode; + g.subsubmode = NoSubSubMode; + unfocus(); +} + +void +FakeVimHandler::Private::recordJump(int position) +{ + CursorPosition pos = + position >= 0 ? CursorPosition(document(), position) : CursorPosition(m_cursor); + setMark('\'', pos); + setMark('`', pos); + if (m_buffer->jumpListUndo.isEmpty() || m_buffer->jumpListUndo.top() != pos) + m_buffer->jumpListUndo.push(pos); + m_buffer->jumpListRedo.clear(); + UNDO_DEBUG("jumps: " << m_buffer->jumpListUndo); +} + +void +FakeVimHandler::Private::jump(int distance) +{ + QStack<CursorPosition> &from = (distance > 0) ? m_buffer->jumpListRedo : m_buffer->jumpListUndo; + QStack<CursorPosition> &to = (distance > 0) ? m_buffer->jumpListUndo : m_buffer->jumpListRedo; + int len = qMin(qAbs(distance), from.size()); + CursorPosition m(m_cursor); + setMark('\'', m); + setMark('`', m); + for (int i = 0; i < len; ++i) { + to.push(m); + setCursorPosition(from.top()); + from.pop(); + } + setTargetColumn(); +} + +Column +FakeVimHandler::Private::indentation(const QString &line) const +{ + int ts = static_cast<int>(s.tabStop.value()); + int physical = 0; + int logical = 0; + int n = line.size(); + while (physical < n) { + QChar c = line.at(physical); + if (c == ' ') + ++logical; + else if (c == '\t') + logical += ts - logical % ts; + else + break; + ++physical; + } + return Column(physical, logical); +} + +QString +FakeVimHandler::Private::tabExpand(int n) const +{ + int ts = static_cast<int>(s.tabStop.value()); + if (s.expandTab.value() || ts < 1) + return QString(n, ' '); + return QString(n / ts, '\t') + QString(n % ts, ' '); +} + +void +FakeVimHandler::Private::insertAutomaticIndentation(bool goingDown, bool forceAutoIndent) +{ + if (!forceAutoIndent && !s.autoIndent.value() && !s.smartIndent.value()) + return; + + if (s.smartIndent.value()) { + QTextBlock bl = block(); + Range range(bl.position(), bl.position()); + indentText(range, '\n'); + } else { + QTextBlock bl = goingDown ? block().previous() : block().next(); + QString text = bl.text(); + int pos = 0; + int n = text.size(); + while (pos < n && text.at(pos).isSpace()) + ++pos; + text.truncate(pos); + // FIXME: handle 'smartindent' and 'cindent' + insertText(text); + } +} + +void +FakeVimHandler::Private::handleStartOfLine() +{ + if (s.startOfLine.value()) + moveToFirstNonBlankOnLine(); +} + +void +FakeVimHandler::Private::replay(const QString &command, int repeat) +{ + if (repeat <= 0) + return; + + //qDebug() << "REPLAY: " << quoteUnprintable(command); + clearCurrentMode(); + const Inputs inputs(command); + for (int i = 0; i < repeat; ++i) { + for (const Input &in : inputs) { + if (handleDefaultKey(in) != EventHandled) + return; + } + } +} + +QString +FakeVimHandler::Private::visualDotCommand() const +{ + QTextCursor start(m_cursor); + QTextCursor end(start); + end.setPosition(end.anchor()); + + QString command; + + if (isVisualCharMode()) + command = "v"; + else if (isVisualLineMode()) + command = "V"; + else if (isVisualBlockMode()) + command = "<c-v>"; + else + return QString(); + + const int down = qAbs(start.blockNumber() - end.blockNumber()); + if (down != 0) + command.append(QString("%1j").arg(down)); + + const int right = start.positionInBlock() - end.positionInBlock(); + if (right != 0) { + command.append(QString::number(qAbs(right))); + command.append(QLatin1Char(right < 0 && isVisualBlockMode() ? 'h' : 'l')); + } + + return command; +} + +void +FakeVimHandler::Private::selectTextObject(bool simple, bool inner) +{ + const int position1 = this->position(); + const int anchor1 = this->anchor(); + bool setupAnchor = (position1 == anchor1); + bool forward = anchor1 <= position1; + const int repeat = count(); + + // set anchor if not already set + if (setupAnchor) { + // Select nothing with 'inner' on empty line. + if (inner && atEmptyLine() && repeat == 1) { + g.movetype = MoveExclusive; + return; + } + moveToBoundaryStart(1, simple, false); + setAnchor(); + } else if (forward) { + moveToNextCharacter(); + } else { + moveToPreviousCharacter(); + } + + if (inner) { + moveToBoundaryEnd(repeat, simple); + } else { + const int direction = forward ? 1 : -1; + for (int i = 0; i < repeat; ++i) { + // select leading spaces + bool leadingSpace = characterAtCursor().isSpace(); + if (leadingSpace) { + if (forward) + moveToNextBoundaryStart(1, simple); + else + moveToNextBoundaryEnd(1, simple, false); + } + + // select word + if (forward) + moveToWordEnd(1, simple); + else + moveToWordStart(1, simple, false); + + // select trailing spaces if no leading space + QChar afterCursor = characterAt(position() + direction); + if (!leadingSpace && afterCursor.isSpace() && + afterCursor != QChar::ParagraphSeparator && !atBlockStart()) { + if (forward) + moveToNextBoundaryEnd(1, simple); + else + moveToNextBoundaryStart(1, simple, false); + } + + // if there are no trailing spaces in selection select all leading spaces + // after previous character + if (setupAnchor && (!characterAtCursor().isSpace() || atBlockEnd())) { + int min = block().position(); + int pos = anchor(); + while (pos >= min && characterAt(--pos).isSpace()) {} + if (pos >= min) + setAnchorAndPosition(pos + 1, position()); + } + + if (i + 1 < repeat) { + if (forward) + moveToNextCharacter(); + else + moveToPreviousCharacter(); + } + } + } + + if (inner) { + g.movetype = MoveInclusive; + } else { + g.movetype = MoveExclusive; + if (isNoVisualMode()) + moveToNextCharacter(); + else if (isVisualLineMode()) + g.visualMode = VisualCharMode; + } + + setTargetColumn(); +} + +void +FakeVimHandler::Private::selectWordTextObject(bool inner) +{ + selectTextObject(false, inner); +} + +void +FakeVimHandler::Private::selectWORDTextObject(bool inner) +{ + selectTextObject(true, inner); +} + +void +FakeVimHandler::Private::selectSentenceTextObject(bool inner) +{ + Q_UNUSED(inner) +} + +void +FakeVimHandler::Private::selectParagraphTextObject(bool inner) +{ + const QTextCursor oldCursor = m_cursor; + const VisualMode oldVisualMode = g.visualMode; + + const int anchorBlock = blockNumberAt(anchor()); + const int positionBlock = blockNumberAt(position()); + const bool setupAnchor = anchorBlock == positionBlock; + int repeat = count(); + + // If anchor and position are in the same block, + // start line selection at beginning of current paragraph. + if (setupAnchor) { + moveToParagraphStartOrEnd(-1); + setAnchor(); + + if (!isVisualLineMode() && isVisualMode()) + toggleVisualMode(VisualLineMode); + } + + const bool forward = anchor() <= position(); + const int dLocal = forward ? 1 : -1; + + bool startsAtParagraph = !atEmptyLine(position()); + + moveToParagraphStartOrEnd(dLocal); + + // If selection already changed, decreate count. + if ((setupAnchor && g.submode != NoSubMode) || oldVisualMode != g.visualMode || + m_cursor != oldCursor) { + --repeat; + if (!inner) { + moveDown(dLocal); + moveToParagraphStartOrEnd(dLocal); + startsAtParagraph = !startsAtParagraph; + } + } + + if (repeat > 0) { + bool isCountEven = repeat % 2 == 0; + bool endsOnParagraph = inner ? isCountEven == startsAtParagraph : startsAtParagraph; + + if (inner) { + repeat = repeat / 2; + if (!isCountEven || endsOnParagraph) + ++repeat; + } else { + if (endsOnParagraph) + ++repeat; + } + + if (!moveToNextParagraph(dLocal * repeat)) { + m_cursor = oldCursor; + g.visualMode = oldVisualMode; + return; + } + + if (endsOnParagraph && atEmptyLine()) + moveUp(dLocal); + else + moveToParagraphStartOrEnd(dLocal); + } + + if (!inner && setupAnchor && !atEmptyLine() && !atEmptyLine(anchor())) { + // If position cannot select empty lines, try to select them with anchor. + setAnchorAndPosition(position(), anchor()); + moveToNextParagraph(-dLocal); + moveToParagraphStartOrEnd(-dLocal); + setAnchorAndPosition(position(), anchor()); + } + + recordJump(oldCursor.position()); + setTargetColumn(); + g.movetype = MoveLineWise; +} + +bool +FakeVimHandler::Private::selectBlockTextObject(bool inner, QChar left, QChar right) +{ + int p1 = blockBoundary(left, right, false, count()); + if (p1 == -1) + return false; + + int p2 = blockBoundary(left, right, true, count()); + if (p2 == -1) + return false; + + g.movetype = MoveExclusive; + + if (inner) { + p1 += 1; + bool moveStart = characterAt(p1) == QChar::ParagraphSeparator; + bool moveEnd = isFirstNonBlankOnLine(p2); + if (moveStart) + ++p1; + if (moveEnd) + p2 = blockAt(p2).position() - 1; + if (moveStart && moveEnd) + g.movetype = MoveLineWise; + } else { + p2 += 1; + } + + if (isVisualMode()) + --p2; + + setAnchorAndPosition(p1, p2); + + return true; +} + +bool +FakeVimHandler::Private::changeNumberTextObject(int count) +{ + const QTextBlock block = this->block(); + const QString lineText = block.text(); + const int posMin = m_cursor.positionInBlock() + 1; + + // find first decimal, hexadecimal or octal number under or after cursor position + QRegularExpression re("(0[xX])(0*[0-9a-fA-F]+)|(0)(0*[0-7]+)(?=\\D|$)|(\\d+)"); + QRegularExpressionMatch match; + QRegularExpressionMatchIterator it = re.globalMatch(lineText); + while (true) { + if (!it.hasNext()) + return false; + match = it.next(); + if (match.capturedEnd() >= posMin) + break; + } + int pos = match.capturedStart(); + int len = match.capturedLength(); + QString prefix = match.captured(1) + match.captured(3); + bool hex = prefix.length() >= 2 && (prefix[1].toLower() == 'x'); + bool octal = !hex && !prefix.isEmpty(); + const QString num = hex ? match.captured(2) : octal ? match.captured(4) : match.captured(5); + + // parse value + bool ok; + int base = hex ? 16 : octal ? 8 : 10; + qlonglong value = 0; // decimal value + qlonglong uvalue = 0; // hexadecimal or octal value (only unsigned) + if (hex || octal) + uvalue = static_cast<qlonglong>(num.toULongLong(&ok, base)); + else + value = num.toLongLong(&ok, base); + if (!ok) { + qWarning() << "Cannot parse number:" << num << "base:" << base; + return false; + } + + // negative decimal number + if (!octal && !hex && pos > 0 && lineText[pos - 1] == '-') { + value = -value; + --pos; + ++len; + } + + // result to string + QString repl; + if (hex || octal) + repl = QString::number(uvalue + count, base); + else + repl = QString::number(value + count, base); + + // convert hexadecimal number to upper-case if last letter was upper-case + if (hex) { + const int lastLetter = num.lastIndexOf(QRegularExpression("[a-fA-F]")); + if (lastLetter != -1 && num[lastLetter].isUpper()) + repl = repl.toUpper(); + } + + // preserve leading zeroes + if ((octal || hex) && repl.size() < num.size()) + prefix.append(QString("0").repeated(num.size() - repl.size())); + repl.prepend(prefix); + + pos += block.position(); + pushUndoState(); + setAnchorAndPosition(pos, pos + len); + replaceText(currentRange(), repl); + setPosition(pos + repl.size() - 1); + + return true; +} + +bool +FakeVimHandler::Private::selectQuotedStringTextObject(bool inner, const QString "e) +{ + QTextCursor tc = m_cursor; + int sz = quote.size(); + + QTextCursor tc1; + QTextCursor tc2(document()); + while (tc2 <= tc) { + tc1 = document()->find(quote, tc2); + if (tc1.isNull()) + return false; + tc2 = document()->find(quote, tc1); + if (tc2.isNull()) + return false; + } + + int p1 = tc1.position(); + int p2 = tc2.position(); + if (inner) { + p2 = qMax(p1, p2 - sz); + if (characterAt(p1) == QChar::ParagraphSeparator) + ++p1; + } else { + p1 -= sz; + p2 -= sz - 1; + } + + if (isVisualMode()) + --p2; + + setAnchorAndPosition(p1, p2); + g.movetype = MoveExclusive; + + return true; +} + +bool +FakeVimHandler::Private::selectArgumentTextObject(bool inner) +{ + // We are just interested whether we're currently inside angled brackets, + // but selectBlockTextObject also moves the cursor, so set it back to + // its original position afterwards + QTextCursor prevCursor = m_cursor; + const bool insideTemplateParameter = selectBlockTextObject(true, '<', '>'); + m_cursor = prevCursor; + + int openAngleBracketCount = insideTemplateParameter ? 1 : 0; + + QTextCursor tcStart(m_cursor); + while (true) { + if (tcStart.atStart()) + return true; + + const QChar currentChar = characterAt(tcStart.position()); + + if (openAngleBracketCount == 0 && (currentChar == '(' || currentChar == ',')) + break; + + if (currentChar == '<') + openAngleBracketCount--; + else if (currentChar == '>') + openAngleBracketCount++; + + tcStart.setPosition(tcStart.position() - 1); + } + + QTextCursor tcEnd(m_cursor); + openAngleBracketCount = insideTemplateParameter ? 1 : 0; + int openParanthesisCount = 0; + + while (true) { + if (tcEnd.atEnd()) { + return true; + } + + const QChar currentChar = characterAt(tcEnd.position()); + if (openAngleBracketCount == 0 && openParanthesisCount == 0 && + (currentChar == ')' || currentChar == ',')) + break; + + if (currentChar == '<') + openAngleBracketCount++; + else if (currentChar == '>') + openAngleBracketCount--; + else if (currentChar == '(') + openParanthesisCount++; + else if (currentChar == ')') + openParanthesisCount--; + + tcEnd.setPosition(tcEnd.position() + 1); + } + + if (!inner && characterAt(tcEnd.position()) == ',' && characterAt(tcStart.position()) == '(') { + tcEnd.setPosition(tcEnd.position() + 1); + if (characterAt(tcEnd.position()) == ' ') + tcEnd.setPosition(tcEnd.position() + 1); + } + + // Never include the opening paranthesis + if (characterAt(tcStart.position()) == '(') { + tcStart.setPosition(tcStart.position() + 1); + } else if (inner) { + tcStart.setPosition(tcStart.position() + 1); + if (characterAt(tcStart.position()) == ' ') + tcStart.setPosition(tcStart.position() + 1); + } + + if (isVisualMode()) + tcEnd.setPosition(tcEnd.position() - 1); + + g.movetype = MoveExclusive; + + setAnchorAndPosition(tcStart.position(), tcEnd.position()); + return true; +} + +Mark +FakeVimHandler::Private::mark(QChar code) const +{ + if (isVisualMode()) { + if (code == '<') + return CursorPosition(document(), qMin(anchor(), position())); + if (code == '>') + return CursorPosition(document(), qMax(anchor(), position())); + } + + if (code.isUpper()) + return g.marks.value(code); + + return m_buffer->marks.value(code); +} + +void +FakeVimHandler::Private::setMark(QChar code, CursorPosition position) +{ + if (code.isUpper()) + g.marks[code] = Mark(position, m_currentFileName); + else + m_buffer->marks[code] = Mark(position); +} + +bool +FakeVimHandler::Private::jumpToMark(QChar mark, bool backTickMode) +{ + Mark m = this->mark(mark); + if (!m.isValid()) { + showMessage(MessageError, msgMarkNotSet(mark)); + return false; + } + if (!m.isLocal(m_currentFileName)) { + q->requestJumpToGlobalMark(mark, backTickMode, m.fileName()); + return false; + } + + if ((mark == '\'' || mark == '`') && !m_buffer->jumpListUndo.isEmpty()) + m_buffer->jumpListUndo.pop(); + recordJump(); + setCursorPosition(m.position(document())); + if (!backTickMode) + moveToFirstNonBlankOnLine(); + if (g.submode == NoSubMode) + setAnchor(); + setTargetColumn(); + + return true; +} + +void +FakeVimHandler::Private::updateMarks(const Marks &newMarks) +{ + for (auto it = newMarks.cbegin(), end = newMarks.cend(); it != end; ++it) + m_buffer->marks[it.key()] = it.value(); +} + +RangeMode +FakeVimHandler::Private::registerRangeMode(int reg) const +{ + bool isClipboard; + bool isSelection; + getRegisterType(®, &isClipboard, &isSelection); + + if (isClipboard || isSelection) { + QClipboard *clipboard = QApplication::clipboard(); + QClipboard::Mode mode = isClipboard ? QClipboard::Clipboard : QClipboard::Selection; + + // Use range mode from Vim's clipboard data if available. + const QMimeData *data = clipboard->mimeData(mode); + if (data && data->hasFormat(vimMimeText)) { + QByteArray bytes = data->data(vimMimeText); + if (bytes.length() > 0) + return static_cast<RangeMode>(bytes.at(0)); + } + + // If register content is clipboard: + // - return RangeLineMode if text ends with new line char, + // - return RangeCharMode otherwise. + QString text = clipboard->text(mode); + return (text.endsWith('\n') || text.endsWith('\r')) ? RangeLineMode : RangeCharMode; + } + + return g.registers[reg].rangemode; +} + +void +FakeVimHandler::Private::setRegister(int reg, const QString &contents, RangeMode mode) +{ + bool copyToClipboard; + bool copyToSelection; + bool append; + getRegisterType(®, ©ToClipboard, ©ToSelection, &append); + + QString contents2 = contents; + if ((mode == RangeLineMode || mode == RangeLineModeExclusive) && !contents2.endsWith('\n')) { + contents2.append('\n'); + } + + if (copyToClipboard || copyToSelection) { + if (copyToClipboard) + setClipboardData(contents2, mode, QClipboard::Clipboard); + if (copyToSelection) + setClipboardData(contents2, mode, QClipboard::Selection); + } else { + if (append) + g.registers[reg].contents.append(contents2); + else + g.registers[reg].contents = contents2; + g.registers[reg].rangemode = mode; + } +} + +QString +FakeVimHandler::Private::registerContents(int reg) const +{ + bool copyFromClipboard; + bool copyFromSelection; + getRegisterType(®, ©FromClipboard, ©FromSelection); + + if (copyFromClipboard || copyFromSelection) { + QClipboard *clipboard = QApplication::clipboard(); + if (copyFromClipboard) + return clipboard->text(QClipboard::Clipboard); + if (copyFromSelection) + return clipboard->text(QClipboard::Selection); + } + + return g.registers[reg].contents; +} + +void +FakeVimHandler::Private::getRegisterType(int *reg, bool *isClipboard, bool *isSelection, + bool *append) const +{ + bool clipboard = false; + bool selection = false; + + // If register is uppercase, append content to lower case register on yank/delete. + const QChar c(*reg); + if (append != nullptr) + *append = c.isUpper(); + if (c.isUpper()) + *reg = c.toLower().unicode(); + + if (c == '"') { + QStringList list = s.clipboard.value().split(','); + clipboard = list.contains("unnamedplus"); + selection = list.contains("unnamed"); + } else if (c == '+') { + clipboard = true; + } else if (c == '*') { + selection = true; + } + + // selection (primary) is clipboard on systems without selection support + if (selection && !QApplication::clipboard()->supportsSelection()) { + clipboard = true; + selection = false; + } + + if (isClipboard != nullptr) + *isClipboard = clipboard; + if (isSelection != nullptr) + *isSelection = selection; +} + +/////////////////////////////////////////////////////////////////////// +// +// FakeVimHandler +// +/////////////////////////////////////////////////////////////////////// + +FakeVimHandler::FakeVimHandler(QWidget *widget, QObject *parent) + : QObject(parent) + , d(new Private(this, widget)) +{ +} + +FakeVimHandler::~FakeVimHandler() { delete d; } + +// gracefully handle that the parent editor is deleted +void +FakeVimHandler::disconnectFromEditor() +{ + d->m_textedit = nullptr; + d->m_plaintextedit = nullptr; +} + +void +FakeVimHandler::updateGlobalMarksFilenames(const QString &oldFileName, const QString &newFileName) +{ + for (Mark &mark : Private::g.marks) { + if (mark.fileName() == oldFileName) + mark.setFileName(newFileName); + } +} + +bool +FakeVimHandler::eventFilter(QObject *ob, QEvent *ev) +{ +#ifndef FAKEVIM_STANDALONE + if (!fakeVimSettings()->useFakeVim.value()) + return QObject::eventFilter(ob, ev); +#endif + + if (ev->type() == QEvent::Shortcut) { + d->passShortcuts(false); + return false; + } + + if (ev->type() == QEvent::KeyPress && + (ob == d->editor() || + (Private::g.mode == ExMode || Private::g.subsubmode == SearchSubSubMode))) { + auto kev = static_cast<QKeyEvent *>(ev); + KEY_DEBUG("KEYPRESS" << kev->key() << kev->text() << QChar(kev->key())); + EventResult res = d->handleEvent(kev); + //if (Private::g.mode == InsertMode) + // completionRequested(); + // returning false core the app see it + //KEY_DEBUG("HANDLED CODE:" << res); + //return res != EventPassedToCore; + //return true; + return res == EventHandled || res == EventCancelled; + } + + if (ev->type() == QEvent::ShortcutOverride && + (ob == d->editor() || + (Private::g.mode == ExMode || Private::g.subsubmode == SearchSubSubMode))) { + auto kev = static_cast<QKeyEvent *>(ev); + if (d->wantsOverride(kev)) { + KEY_DEBUG("OVERRIDING SHORTCUT" << kev->key()); + ev->accept(); // accepting means "don't run the shortcuts" + return true; + } + KEY_DEBUG("NO SHORTCUT OVERRIDE" << kev->key()); + return true; + } + + if (ev->type() == QEvent::FocusOut && ob == d->editor()) { + d->unfocus(); + return false; + } + + if (ev->type() == QEvent::FocusIn && ob == d->editor()) + d->focus(); + + return QObject::eventFilter(ob, ev); +} + +void +FakeVimHandler::installEventFilter() +{ + d->installEventFilter(); +} + +void +FakeVimHandler::setupWidget() +{ + d->setupWidget(); +} + +void +FakeVimHandler::restoreWidget(int tabSize) +{ + d->restoreWidget(tabSize); +} + +void +FakeVimHandler::handleCommand(const QString &cmd) +{ + d->enterFakeVim(); + d->handleCommand(cmd); + d->leaveFakeVim(); +} + +void +FakeVimHandler::handleReplay(const QString &keys) +{ + d->enterFakeVim(); + d->replay(keys); + d->leaveFakeVim(); +} + +void +FakeVimHandler::handleInput(const QString &keys) +{ + const Inputs inputs(keys); + d->enterFakeVim(); + for (const Input &input : inputs) + d->handleKey(input); + d->leaveFakeVim(); +} + +void +FakeVimHandler::enterCommandMode() +{ + d->enterCommandMode(); +} + +void +FakeVimHandler::setCurrentFileName(const QString &fileName) +{ + d->m_currentFileName = fileName; +} + +QString +FakeVimHandler::currentFileName() const +{ + return d->m_currentFileName; +} + +void +FakeVimHandler::showMessage(MessageLevel level, const QString &msg) +{ + d->showMessage(level, msg); +} + +QWidget * +FakeVimHandler::widget() +{ + return d->editor(); +} + +// Test only +int +FakeVimHandler::physicalIndentation(const QString &line) const +{ + Column ind = d->indentation(line); + return ind.physical; +} + +int +FakeVimHandler::logicalIndentation(const QString &line) const +{ + Column ind = d->indentation(line); + return ind.logical; +} + +QString +FakeVimHandler::tabExpand(int n) const +{ + return d->tabExpand(n); +} + +void +FakeVimHandler::miniBufferTextEdited(const QString &text, int cursorPos, int anchorPos) +{ + d->miniBufferTextEdited(text, cursorPos, anchorPos); +} + +void +FakeVimHandler::setTextCursorPosition(int position) +{ + int pos = qMax(0, qMin(position, d->lastPositionInDocument())); + if (d->isVisualMode()) + d->setPosition(pos); + else + d->setAnchorAndPosition(pos, pos); + d->setTargetColumn(); + + if (!d->m_inFakeVim) + d->commitCursor(); +} + +QTextCursor +FakeVimHandler::textCursor() const +{ + return d->m_cursor; +} + +void +FakeVimHandler::setTextCursor(const QTextCursor &cursor) +{ + d->m_cursor = cursor; +} + +bool +FakeVimHandler::jumpToLocalMark(QChar mark, bool backTickMode) +{ + return d->jumpToMark(mark, backTickMode); +} + +} // namespace FakeVim::Internal + +Q_DECLARE_METATYPE(FakeVim::Internal::FakeVimHandler::Private::BufferDataPtr) diff --git a/src/UI/FakeVim/FakeVimHandler.hh b/src/UI/FakeVim/FakeVimHandler.hh new file mode 100644 index 0000000000000000000000000000000000000000..1894680b7715fd951180df9af6772252b5b89b01 --- /dev/null +++ b/src/UI/FakeVim/FakeVimHandler.hh @@ -0,0 +1,171 @@ +#pragma once + +#define FAKEVIM_STANDALONE + +#include <QObject> +#include <QTextEdit> + +#include <functional> +#include <vector> + +namespace FakeVim::Internal +{ +enum RangeMode { + // Reordering first three enum items here will break + // compatibility with clipboard format stored by Vim. + RangeCharMode, // v + RangeLineMode, // V + RangeBlockMode, // Ctrl-v + RangeLineModeExclusive, + RangeBlockAndTailMode // Ctrl-v for D and X +}; + +struct Range { + Range() = default; + Range(int b, int e, RangeMode m = RangeCharMode); + QString toString() const; + bool isValid() const; + + int beginPos = -1; + int endPos = -1; + RangeMode rangemode = RangeCharMode; + + // Just some convenience functions for compatibility with optional + // which is apparently allowed in qtcreator. + operator bool() const { return isValid(); } + void reset() + { + beginPos = -1; + endPos = -1; + } + Range &operator*() { return *this; } +}; + +struct ExCommand { + ExCommand() = default; + ExCommand(const QString &cmd, const QString &args = QString(), const Range &range = Range()); + + bool matches(const QString &min, const QString &full) const; + + QString cmd; + bool hasBang = false; + QString args; + Range range; + int count = 1; +}; + +// message levels sorted by severity +enum MessageLevel { + MessageMode, // show current mode (format "-- %1 --") + MessageCommand, // show last Ex command or search + MessageInfo, // result of a command + MessageWarning, // warning + MessageError, // error + MessageShowCmd // partial command +}; + +template <typename Type> class Signal { +public: + using Callable = std::function<Type>; + + void connect(const Callable &callable) { m_callables.push_back(callable); } + + template <typename... Args> void operator()(Args... args) const + { + for (const Callable &callable : m_callables) + callable(args...); + } + +private: + std::vector<Callable> m_callables; +}; + +class FakeVimHandler : public QObject { + Q_OBJECT + +public: + explicit FakeVimHandler(QWidget *widget, QObject *parent = nullptr); + ~FakeVimHandler() override; + + QWidget *widget(); + + // call before widget is deleted + void disconnectFromEditor(); + + static void updateGlobalMarksFilenames(const QString &oldFileName, const QString &newFileName); + +public: + void setCurrentFileName(const QString &fileName); + QString currentFileName() const; + + void showMessage(MessageLevel level, const QString &msg); + + // This executes an "ex" style command taking context + // information from the current widget. + void handleCommand(const QString &cmd); + void handleReplay(const QString &keys); + void handleInput(const QString &keys); + void enterCommandMode(); + + void installEventFilter(); + + // Convenience + void setupWidget(); + void restoreWidget(int tabSize); + + // Test only + int physicalIndentation(const QString &line) const; + int logicalIndentation(const QString &line) const; + QString tabExpand(int n) const; + + void miniBufferTextEdited(const QString &text, int cursorPos, int anchorPos); + + // Set text cursor position. Keeps anchor if in visual mode. + void setTextCursorPosition(int position); + + QTextCursor textCursor() const; + void setTextCursor(const QTextCursor &cursor); + + bool jumpToLocalMark(QChar mark, bool backTickMode); + + bool eventFilter(QObject *ob, QEvent *ev) override; + + Signal<void(const QString &msg, int cursorPos, int anchorPos, int messageLevel)> + commandBufferChanged; + Signal<void(const QString &msg)> statusDataChanged; + Signal<void(const QString &msg)> extraInformationChanged; + Signal<void(const QList<QTextEdit::ExtraSelection> &selection)> selectionChanged; + Signal<void(const QString &needle)> highlightMatches; + Signal<void(bool *moved, bool *forward, QTextCursor *cursor)> moveToMatchingParenthesis; + Signal<void(bool *result, QChar c)> checkForElectricCharacter; + Signal<void(int beginLine, int endLine, QChar typedChar)> indentRegion; + Signal<void(const QString &needle, bool forward)> simpleCompletionRequested; + Signal<void(const QString &key, int count)> windowCommandRequested; + Signal<void(bool reverse)> findRequested; + Signal<void(bool reverse)> findNextRequested; + Signal<void(bool *handled, const ExCommand &cmd)> handleExCommandRequested; + Signal<void()> requestDisableBlockSelection; + Signal<void(const QTextCursor &cursor)> requestSetBlockSelection; + Signal<void(QTextCursor *cursor)> requestBlockSelection; + Signal<void(bool *on)> requestHasBlockSelection; + Signal<void(int depth)> foldToggle; + Signal<void(bool fold)> foldAll; + Signal<void(int depth, bool dofold)> fold; + Signal<void(int count, bool current)> foldGoTo; + Signal<void(QChar mark, bool backTickMode, const QString &fileName)> requestJumpToLocalMark; + Signal<void(QChar mark, bool backTickMode, const QString &fileName)> requestJumpToGlobalMark; + Signal<void()> completionRequested; + Signal<void()> tabPreviousRequested; + Signal<void()> tabNextRequested; + +public: + // Avoid weak tables + class Private; + +private: + Private *d; +}; + +} // namespace FakeVim::Internal + +Q_DECLARE_METATYPE(FakeVim::Internal::ExCommand) diff --git a/src/UI/FakeVim/FakeVimTr.hh b/src/UI/FakeVim/FakeVimTr.hh new file mode 100644 index 0000000000000000000000000000000000000000..b298d4f77421612544e68be9f83d66aca95fed24 --- /dev/null +++ b/src/UI/FakeVim/FakeVimTr.hh @@ -0,0 +1,11 @@ +#pragma once + +#include <QCoreApplication> + +namespace FakeVim +{ +struct Tr { + Q_DECLARE_TR_FUNCTIONS(FakeVim) +}; + +} // namespace FakeVim diff --git a/src/UI/MainWindow.cc b/src/UI/MainWindow.cc index b89262cdca7f46e4b925612725a253e312795f72..31d2a7cc395210bc8797868b9741dfa4640e2a22 100644 --- a/src/UI/MainWindow.cc +++ b/src/UI/MainWindow.cc @@ -79,6 +79,22 @@ MainWindow::MainWindow() noexcept ACTION_ADD_SHORTCUT(openDocument, QKeySequence::Open); ACTION_ADD_SHORTCUT(saveFile, QKeySequence::Save); + // Custom useFakeVim action + { + editMenu->addSeparator(); + QAction *useFakeVim = new QAction(tr("Use FakeVim"), this); + useFakeVim->setStatusTip("Use FakeVim with integrated text editors"); + useFakeVim->setCheckable(true); + editMenu->addAction(useFakeVim); + connect(useFakeVim, &QAction::toggled, this, [this](bool checked) noexcept -> void { + const bool oldState = vivyApp->getUseFakeVimEditor(); + if (oldState != checked) { + vivyApp->setUseFakeVimEditor(checked); + updateFakeVimUsage(checked); + } + }); + } + // Setup the tabs to display the documents documents = new QTabWidget(this); documents->setMovable(true); @@ -87,7 +103,8 @@ MainWindow::MainWindow() noexcept documents->setUsesScrollButtons(true); documents->setDocumentMode(true); documents->setTabBarAutoHide(true); - connect(documents, &QTabWidget::tabCloseRequested, this, &MainWindow::closeDocument); + connect(documents, &QTabWidget::tabCloseRequested, this, + [this](int index) noexcept -> void { closeDocument(index); }); connect(documents, &QTabWidget::tabBarDoubleClicked, this, &MainWindow::openProperties); setCentralWidget(documents); centralWidget()->setContentsMargins(0, 0, 0, 0); @@ -149,6 +166,21 @@ MainWindow::MainWindow() noexcept newDocument(); } +void +MainWindow::updateFakeVimUsage(bool yes) noexcept +{ + const int count = documents->count(); + for (int index = 0; index < count; ++index) { + AbstractDocumentView *docView = getTab(index); + const bool documentExists = docView && docView->getDocument(); + const bool isScriptEditor = + documentExists && (docView->getDocument()->getType() == AbstractDocument::Type::Script); + + if (isScriptEditor) + static_cast<ScriptDocumentView *const>(docView)->setUseFakeVimEditor(yes); + } +} + void MainWindow::openProperties(int index) noexcept { @@ -213,6 +245,21 @@ MainWindow::saveFileAs() noexcept } } +void +MainWindow::closeDocument(AbstractDocumentView *const view) noexcept +{ + if (view == nullptr || !view->getDocument()) + return; + + const int count = documents->count(); + for (int index = 0; index < count; ++index) { + const AbstractDocumentView *const documentView = getTab(index); + const bool documentExists = documentView && documentView->getDocument(); + if (documentExists && (*documentView->getDocument()) == (*view->getDocument())) + closeDocument(index); + } +} + void MainWindow::closeDocument(int index) noexcept { @@ -225,6 +272,7 @@ MainWindow::closeDocument(int index) noexcept &MainWindow::documentViewActionsChanged); if (documentToClose) { + qDebug() << "Delete document view" << documentToClose->getDocumentTabName(); documentToClose->closeDocument(); delete documentToClose; } diff --git a/src/UI/MainWindow.hh b/src/UI/MainWindow.hh index 05d2292d0255aabb1c079f3e21cf3f6bcc636482..3a5d921232f76d2a2808f3bb36eabda6627d772b 100644 --- a/src/UI/MainWindow.hh +++ b/src/UI/MainWindow.hh @@ -40,12 +40,16 @@ public: } } +public slots: + void closeDocument(AbstractDocumentView *const) noexcept; + private: void addTab(AbstractDocumentView *); AbstractDocumentView *getTab(const int) const noexcept; AbstractDocumentView *getCurrentDocumentView() const; int findFirstUntouchedDocument() const noexcept; + void updateFakeVimUsage(bool yes) noexcept; QString dialogOpenFileName(const QString &title, const QString &folder, const QString &filter) noexcept; diff --git a/src/UI/ScriptDocumentView.cc b/src/UI/ScriptDocumentView.cc index eea7873654e64e945d79c751c4c5c4673942d2ed..b91e3830cc759f1ca3df43d7ab3e572a159ede0a 100644 --- a/src/UI/ScriptDocumentView.cc +++ b/src/UI/ScriptDocumentView.cc @@ -23,7 +23,80 @@ ScriptDocumentView::ScriptDocumentView(std::shared_ptr<ScriptDocument> ptr, QWid setCentralWidget(editor); editor->setFocus(Qt::OtherFocusReason); + setUseFakeVimEditor(vivyApp->getUseFakeVimEditor()); + connect(this, &ScriptDocumentView::luaErrorFound, editor, &ScriptEditor::updateLastLuaError); + + // Same style as the editor + setStyleSheet(QStringLiteral("* {" + " background-color: #232629;" + " font-family: \"FiraCode\";" + " font-size: 10pt" + "}")); +} + +ScriptDocumentView::~ScriptDocumentView() +{ + setUseFakeVimEditor(false); + qDebug() << "~ScriptDocumentView"; +} + +void +ScriptDocumentView::setUseFakeVimEditor(bool yes) noexcept +{ + if (yes && !isUsingFakeVim) { + MainWindow *const mw = vivyApp->getMainWindow(); + handler = new FakeVimHandler(editor); + proxy = EditorProxy::connectSignals(handler, editor); + isUsingFakeVim = true; + connect(proxy, &EditorProxy::handleInput, handler, &FakeVimHandler::handleInput); + connect(proxy, &EditorProxy::requestQuit, this, [this, mw]() noexcept -> void { + mw->closeDocument(static_cast<AbstractDocumentView *>(this)); + }); + TODO(Implement the save and save + quit things) + // connect(proxy, &EditorProxy::requestSave, document, &AbstractDocument::save); // TODO + // connect(proxy, &EditorProxy::requestSaveAndQuit, document, &AbstractDocument::save + &MainWindow::closeTab); // TODO + initHandler(handler); + clearUndoRedo(editor); + } + + else if (!yes) { + if (handler) { + handler->disconnectFromEditor(); + delete handler; + } + + if (proxy) + delete proxy; + + clearUndoRedo(editor); + editor->setOverwriteMode(false); + isUsingFakeVim = false; + proxy = nullptr; + handler = nullptr; + } +} + +void +ScriptDocumentView::initHandler(FakeVimHandler *handler) noexcept +{ + handler->handleCommand(QStringLiteral("set nopasskeys")); + handler->handleCommand(QStringLiteral("set nopasscontrolkey")); + handler->installEventFilter(); + handler->setupWidget(); + + handler->handleCommand(QStringLiteral("set expandtab")); + handler->handleCommand(QStringLiteral("set shiftwidth=4")); + handler->handleCommand(QStringLiteral("set tabstop=4")); + handler->handleCommand(QStringLiteral("set autoindent")); + handler->handleCommand(QStringLiteral("set smartindent")); +} + +void +ScriptDocumentView::clearUndoRedo(QPlainTextEdit *scriptEditor) noexcept +{ + scriptEditor->setUndoRedoEnabled(false); + scriptEditor->setUndoRedoEnabled(true); } void diff --git a/src/UI/ScriptDocumentView.hh b/src/UI/ScriptDocumentView.hh index 8e595086bfeae7deba8cf65de2421da827c56eab..7f2c333bae28c31c6be079cf0faa5839ed40850e 100644 --- a/src/UI/ScriptDocumentView.hh +++ b/src/UI/ScriptDocumentView.hh @@ -11,18 +11,32 @@ #include <QString> #include <memory> +class QPlainTextEdit; + +namespace FakeVim::Internal +{ +class FakeVimHandler; +} + namespace Vivy { class ScriptEditor; class ScriptHighlighter; class ScriptDocument; +class EditorProxy; +} +namespace Vivy +{ class ScriptDocumentView final : public AbstractDocumentView { Q_OBJECT VIVY_UNMOVABLE_OBJECT(ScriptDocumentView) + using FakeVimHandler = FakeVim::Internal::FakeVimHandler; + public: explicit ScriptDocumentView(std::shared_ptr<ScriptDocument>, QWidget *parent = nullptr); + ~ScriptDocumentView() override; void closeDocument() noexcept override; void openProperties() noexcept override; @@ -32,15 +46,23 @@ public: QIcon getDocumentTabIcon() const noexcept override; AbstractDocument *getDocument() const noexcept override; + void setUseFakeVimEditor(bool yes) noexcept; + signals: void luaErrorFound(int, QString); private: ScriptEditor *editor{ nullptr }; + EditorProxy *proxy{ nullptr }; ScriptHighlighter *syntax{ nullptr }; + FakeVimHandler *handler{ nullptr }; std::shared_ptr<ScriptDocument> document{ nullptr }; QString lastLuaErrorMsg{}; int lastLuaErrorLine{ -1 }; + bool isUsingFakeVim{ false }; + + static void initHandler(FakeVimHandler *handler) noexcept; + static void clearUndoRedo(QPlainTextEdit *editor) noexcept; }; } diff --git a/src/UI/ScriptViews/EditorProxy.cc b/src/UI/ScriptViews/EditorProxy.cc new file mode 100644 index 0000000000000000000000000000000000000000..16845293ca0bb5e1e2a9f40be4d2f82a4e6f699c --- /dev/null +++ b/src/UI/ScriptViews/EditorProxy.cc @@ -0,0 +1,326 @@ +#include "EditorProxy.hh" +#include "../FakeVim/FakeVimHandler.hh" +#include "../FakeVim/FakeVimActions.hh" +#include "../../VivyApplication.hh" + +#include <QMessageBox> +#include <QStatusBar> +#include <QMainWindow> +#include <QTemporaryFile> + +using namespace Vivy; + +Vivy::EditorProxy * +EditorProxy::connectSignals(FakeVimHandler *handler, QPlainTextEdit *editor) noexcept +{ + EditorProxy *proxy = new EditorProxy(editor); + + handler->commandBufferChanged.connect([proxy](const QString &contents, int cursorPos, + int /* anchorPos */, + int /* messageLevel */) noexcept -> void { + proxy->changeStatusMessage(contents, cursorPos); + }); + + handler->extraInformationChanged.connect( + [proxy](const QString &text) noexcept -> void { proxy->changeExtraInformation(text); }); + handler->statusDataChanged.connect( + [proxy](const QString &text) noexcept -> void { proxy->changeStatusData(text); }); + handler->highlightMatches.connect( + [proxy](const QString &needle) noexcept -> void { proxy->highlightMatches(needle); }); + handler->handleExCommandRequested.connect( + [proxy](bool *handled, const ExCommand &cmd) noexcept -> void { + proxy->handleExCommand(handled, cmd); + }); + handler->requestSetBlockSelection.connect([proxy](const QTextCursor &cursor) noexcept -> void { + proxy->requestSetBlockSelection(cursor); + }); + handler->requestDisableBlockSelection.connect( + [proxy]() noexcept -> void { proxy->requestDisableBlockSelection(); }); + handler->requestHasBlockSelection.connect( + [proxy](bool *on) noexcept -> void { proxy->requestHasBlockSelection(on); }); + + handler->indentRegion.connect( + [proxy](int beginBlock, int endBlock, QChar typedChar) noexcept -> void { + proxy->indentRegion(beginBlock, endBlock, typedChar); + }); + handler->checkForElectricCharacter.connect([proxy](bool *result, QChar c) noexcept -> void { + proxy->checkForElectricCharacter(result, c); + }); + + return proxy; +} + +EditorProxy::EditorProxy(QPlainTextEdit *widg) noexcept + : QObject() + , widget(widg) +{ +} + +EditorProxy::~EditorProxy() { qDebug() << "~EditorProxy"; } + +void +EditorProxy::changeStatusData(const QString &info) noexcept +{ + statusData = info; + updateStatusBar(); +} + +void +EditorProxy::highlightMatches(const QString &pattern) noexcept +{ + QTextDocument *doc = widget->document(); + Q_ASSERT(doc); + + QTextEdit::ExtraSelection selection; + selection.format.setBackground(Qt::yellow); + selection.format.setForeground(Qt::black); + + // Highlight matches. + QRegExp re(pattern); + QTextCursor cur = doc->find(re); + searchSelection.clear(); + + int a = cur.position(); + while (!cur.isNull()) { + if (cur.hasSelection()) { + selection.cursor = cur; + searchSelection.append(selection); + } else { + cur.movePosition(QTextCursor::NextCharacter); + } + + cur = doc->find(re, cur); + int b = cur.position(); + + if (a == b) { + cur.movePosition(QTextCursor::NextCharacter); + cur = doc->find(re, cur); + b = cur.position(); + if (a == b) + break; + } + + a = b; + } + + updateExtraSelections(); +} + +void +EditorProxy::changeStatusMessage(const QString &contents, int cursorPos) noexcept +{ + statusMessage = (cursorPos == -1) + ? contents + : contents.left(cursorPos) + QChar(10073) + contents.mid(cursorPos); + updateStatusBar(); +} + +void +EditorProxy::changeExtraInformation(const QString &info) noexcept +{ + QMessageBox::information(widget, tr("Information"), info); +} + +void +EditorProxy::updateStatusBar() noexcept +{ + vivyApp->getMainWindow()->statusBar()->showMessage(statusMessage); // + statusData +} + +void +EditorProxy::handleExCommand(bool *handled, const ExCommand &cmd) noexcept +{ + // :wq + if (wantSaveAndQuit(cmd)) + emit requestSaveAndQuit(); + + // :w + else if (wantSave(cmd)) + emit requestSave(); + + else if (wantQuit(cmd)) { + // :q! + if (cmd.hasBang) + emit requestQuit(); + + // :q + else + emit requestQuit(); + } + + else if (wantRun(cmd)) + emit requestRun(); + + else { + *handled = false; + return; + } + + *handled = true; +} + +void +EditorProxy::requestSetBlockSelection(const QTextCursor &tc) noexcept +{ + const QPalette pal = widget->parentWidget() != nullptr ? widget->parentWidget()->palette() + : QApplication::palette(); + + blockSelection.clear(); + clearSelection.clear(); + + QTextCursor cur = tc; + + QTextEdit::ExtraSelection selection; + selection.format.setBackground(pal.color(QPalette::Base)); + selection.format.setForeground(pal.color(QPalette::Text)); + selection.cursor = cur; + clearSelection.append(selection); + + selection.format.setBackground(pal.color(QPalette::Highlight)); + selection.format.setForeground(pal.color(QPalette::HighlightedText)); + + const int from = cur.positionInBlock(); + const int to = cur.anchor() - cur.document()->findBlock(cur.anchor()).position(); + const int min = qMin(cur.position(), cur.anchor()); + const int max = qMax(cur.position(), cur.anchor()); + for (QTextBlock block = cur.document()->findBlock(min); + block.isValid() && block.position() < max; block = block.next()) { + cur.setPosition(block.position() + qMin(from, block.length())); + cur.setPosition(block.position() + qMin(to, block.length()), QTextCursor::KeepAnchor); + selection.cursor = cur; + blockSelection.append(selection); + } + + disconnect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection); + widget->setTextCursor(tc); + connect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection); + + QPalette pal2 = widget->palette(); + pal2.setColor(QPalette::Highlight, Qt::transparent); + pal2.setColor(QPalette::HighlightedText, Qt::transparent); + widget->setPalette(pal2); + + updateExtraSelections(); +} + +void +EditorProxy::requestDisableBlockSelection() noexcept +{ + const QPalette pal = widget->parentWidget() != nullptr ? widget->parentWidget()->palette() + : QApplication::palette(); + + blockSelection.clear(); + clearSelection.clear(); + widget->setPalette(pal); + + disconnect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection); + + updateExtraSelections(); +} + +void +EditorProxy::updateBlockSelection() noexcept +{ + requestSetBlockSelection(widget->textCursor()); +} + +void +EditorProxy::requestHasBlockSelection(bool *on) noexcept +{ + *on = !blockSelection.isEmpty(); +} + +void +EditorProxy::indentRegion(int beginBlock, int endBlock, QChar typedChar) noexcept +{ + QTextDocument *doc = widget->document(); + Q_ASSERT(doc); + + const int indentSize = + static_cast<int>(FakeVim::Internal::fakeVimSettings()->shiftWidth.value()); + QTextBlock startBlock = doc->findBlockByNumber(beginBlock); + + // Record line lenghts for mark adjustments + QVector<int> lineLengths(endBlock - beginBlock + 1); + QTextBlock block = startBlock; + + for (int i = beginBlock; i <= endBlock; ++i) { + const QString line = block.text(); + lineLengths[i - beginBlock] = line.length(); + + if (typedChar.unicode() == 0 && line.simplified().isEmpty()) { + // clear empty lines + QTextCursor cursor(block); + while (!cursor.atBlockEnd()) + cursor.deleteChar(); + } + + else { + const QTextBlock previousBlock = block.previous(); + const QString previousLine = previousBlock.isValid() ? previousBlock.text() : QString(); + + int indent = firstNonSpace(previousLine); + if (typedChar == '}') + indent = std::max(0, indent - indentSize); + else if (previousLine.endsWith("{")) + indent += indentSize; + const QString indentString = QString(" ").repeated(indent); + + QTextCursor cursor(block); + cursor.beginEditBlock(); + cursor.movePosition(QTextCursor::StartOfBlock); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, + firstNonSpace(line)); + cursor.removeSelectedText(); + cursor.insertText(indentString); + cursor.endEditBlock(); + } + + block = block.next(); + } +} + +void +EditorProxy::checkForElectricCharacter(bool *result, QChar c) noexcept +{ + *result = c == '{' || c == '}'; +} + +int +EditorProxy::firstNonSpace(const QString &text) noexcept +{ + int indent = 0; + while (indent < text.length() && text.at(indent) == ' ') + ++indent; + return indent; +} + +void +EditorProxy::updateExtraSelections() noexcept +{ + widget->setExtraSelections(clearSelection + searchSelection + blockSelection); +} + +bool +EditorProxy::wantSaveAndQuit(const ExCommand &cmd) noexcept +{ + return cmd.cmd == "wq"; +} + +bool +EditorProxy::wantSave(const ExCommand &cmd) noexcept +{ + return cmd.matches("w", "write") || cmd.matches("wa", "wall"); +} + +bool +EditorProxy::wantQuit(const ExCommand &cmd) noexcept +{ + return cmd.matches("q", "quit") || cmd.matches("qa", "qall"); +} + +bool +EditorProxy::wantRun(const ExCommand &cmd) noexcept +{ + return cmd.matches("run", "run") || cmd.matches("make", "make"); +} diff --git a/src/UI/ScriptViews/EditorProxy.hh b/src/UI/ScriptViews/EditorProxy.hh new file mode 100644 index 0000000000000000000000000000000000000000..1c2e4bdadf2ca56f044915d10f43d2928b8518d1 --- /dev/null +++ b/src/UI/ScriptViews/EditorProxy.hh @@ -0,0 +1,76 @@ +#pragma once + +#include <QObject> +#include <QTextEdit> +#include "ScriptEditor.hh" + +class QMainWindow; +class QTextDocument; +class QString; +class QWidget; +class QTextCursor; + +namespace FakeVim::Internal +{ +class FakeVimHandler; +struct ExCommand; +} + +namespace Vivy +{ +class EditorProxy; + +class EditorProxy final : public QObject { + Q_OBJECT + VIVY_UNMOVABLE_OBJECT(EditorProxy) + + explicit EditorProxy(QPlainTextEdit *widget) noexcept; + using FakeVimHandler = FakeVim::Internal::FakeVimHandler; + using ExCommand = FakeVim::Internal::ExCommand; + +public: + ~EditorProxy() override; + +signals: + void handleInput(const QString &keys); + void requestSave(); + void requestSaveAndQuit(); + void requestQuit(); + void requestRun(); + +public slots: + void changeStatusData(const QString &info) noexcept; + void highlightMatches(const QString &pattern) noexcept; + void changeStatusMessage(const QString &contents, int cursorPos) noexcept; + void changeExtraInformation(const QString &info) noexcept; + void updateStatusBar() noexcept; + void handleExCommand(bool *handled, const ExCommand &cmd) noexcept; + void requestSetBlockSelection(const QTextCursor &tc) noexcept; + void requestDisableBlockSelection() noexcept; + void updateBlockSelection() noexcept; + void requestHasBlockSelection(bool *on) noexcept; + void indentRegion(int beginBlock, int endBlock, QChar typedChar) noexcept; + void checkForElectricCharacter(bool *result, QChar c) noexcept; + +private: + static int firstNonSpace(const QString &text) noexcept; + + void updateExtraSelections() noexcept; + bool wantSaveAndQuit(const ExCommand &cmd) noexcept; + bool wantSave(const ExCommand &cmd) noexcept; + bool wantQuit(const ExCommand &cmd) noexcept; + bool wantRun(const ExCommand &cmd) noexcept; + + QPlainTextEdit *widget; + QString statusMessage; + QString statusData; + + QList<QTextEdit::ExtraSelection> searchSelection; + QList<QTextEdit::ExtraSelection> clearSelection; + QList<QTextEdit::ExtraSelection> blockSelection; + +public: + static EditorProxy *connectSignals(FakeVimHandler *handler, QPlainTextEdit *editor) noexcept; +}; + +} diff --git a/src/UI/ScriptViews/ScriptEditor.cc b/src/UI/ScriptViews/ScriptEditor.cc index 7b8a73d94e2231a98f6a4542a6b70fac5d55cabc..56dff1a6c6a6e6483c44437f4e49c8dcba29efcd 100644 --- a/src/UI/ScriptViews/ScriptEditor.cc +++ b/src/UI/ScriptViews/ScriptEditor.cc @@ -44,8 +44,46 @@ ScriptEditor::ScriptEditor(QWidget *parent) noexcept textFormat.setForeground(QBrush(Qt::white)); mergeCurrentCharFormat(textFormat); + QPlainTextEdit::setCursorWidth(0); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setBackgroundVisible(true); updateLineNumberAreaWidth(0); + setObjectName(QStringLiteral("Editor")); + setFocus(); +} + +void +ScriptEditor::paintEvent(QPaintEvent *e) noexcept +{ + QPlainTextEdit::paintEvent(e); + + if (!cursorRect.isNull() && e->rect().intersects(cursorRect)) { + QRect rect = cursorRect; + cursorRect = QRect(); + QPlainTextEdit::viewport()->update(rect); + } + + // Draw text cursor. + QRect rect = QPlainTextEdit::cursorRect(); + if (e->rect().intersects(rect)) { + QPainter painter(QPlainTextEdit::viewport()); + + if (QPlainTextEdit::overwriteMode()) { + QFontMetrics fm(QPlainTextEdit::font()); + const int position = QPlainTextEdit::textCursor().position(); + const QChar c = QPlainTextEdit::document()->characterAt(position); + rect.setWidth(fm.horizontalAdvance(c)); + painter.setPen(Qt::NoPen); + painter.setBrush(QPlainTextEdit::palette().color(QPalette::Base)); + painter.setCompositionMode(QPainter::CompositionMode_Difference); + } else { + rect.setWidth(QPlainTextEdit::cursorWidth()); + painter.setPen(QPlainTextEdit::palette().color(QPalette::Text)); + } + + painter.drawRect(rect); + cursorRect = rect; + } } void diff --git a/src/UI/ScriptViews/ScriptEditor.hh b/src/UI/ScriptViews/ScriptEditor.hh index 01c01769c60d3d324cacb35bb2a55c0684d957d6..491a427d8bf9b76636764bb20701488724604d67 100644 --- a/src/UI/ScriptViews/ScriptEditor.hh +++ b/src/UI/ScriptViews/ScriptEditor.hh @@ -42,8 +42,9 @@ public slots: void updateLastLuaError(int, QString); protected: - void resizeEvent(QResizeEvent *event) noexcept override; - void keyPressEvent(QKeyEvent *e) noexcept override; + void resizeEvent(QResizeEvent *) noexcept override; + void keyPressEvent(QKeyEvent *) noexcept override; + void paintEvent(QPaintEvent *) noexcept override; private slots: void updateLineNumberAreaWidth(int newBlockCount) noexcept; @@ -51,5 +52,6 @@ private slots: private: QWidget *lineNumberArea{ nullptr }; + QRect cursorRect{}; }; } diff --git a/src/VivyApplication.cc b/src/VivyApplication.cc index f3e17144af99bdce902167f4f286bf1f7d7a1a92..075638a9b25784b45ea64be5195a208210b3e330 100644 --- a/src/VivyApplication.cc +++ b/src/VivyApplication.cc @@ -99,3 +99,15 @@ VivyApplication::getCurrentDocument() const throw std::logic_error("No main window in the graphic VivyApplication"); return mainWindowPtr.get()->getCurrentDocument(); } + +bool +VivyApplication::getUseFakeVimEditor() const noexcept +{ + return useFakeVim; +} + +void +VivyApplication::setUseFakeVimEditor(bool ok) noexcept +{ + useFakeVim = ok; +} diff --git a/src/VivyApplication.hh b/src/VivyApplication.hh index 1ba91685512f43b533e4e3cd06a511bb871c35ef..3826063a9838fcb173a0dcc0ff72b6575ce3d5ec 100644 --- a/src/VivyApplication.hh +++ b/src/VivyApplication.hh @@ -66,15 +66,19 @@ private: int fontIdBoldItalic; std::unique_ptr<MainWindow> mainWindowPtr{ nullptr }; + bool useFakeVim{ false }; public: VivyApplication(int &argc, char **argv); int exec() noexcept; - QFont getApplicationFont(Font) const noexcept; - [[nodiscard("handle-it")]] MainWindow *getMainWindow() const; - [[nodiscard("handle-it")]] AbstractDocument *getCurrentDocument() const; + [[nodiscard]] QFont getApplicationFont(Font) const noexcept; + [[nodiscard]] MainWindow *getMainWindow() const; + [[nodiscard]] AbstractDocument *getCurrentDocument() const; + [[nodiscard]] bool getUseFakeVimEditor() const noexcept; + + void setUseFakeVimEditor(bool) noexcept; void setTheme(Theme) noexcept; };