commit 94f7b722c75b178882b6596f9041ea768e12a201 Author: Buddy Sandidge Date: Wed Mar 8 14:26:55 2017 -0800 Squashed 'vim/bundle/ultisnips/' content from commit f95ce290b git-subtree-dir: vim/bundle/ultisnips git-subtree-split: f95ce290bc6394d25cd51d0db2e197cd14d6d07b diff --git a/.bzrignore b/.bzrignore new file mode 100644 index 0000000..a64bfd0 --- /dev/null +++ b/.bzrignore @@ -0,0 +1,2 @@ +doc/tags +.bzr-repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb169b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*.swp +doc/tags diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7cd60de --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: python + +python: + - 2.7 + - 3.3 + - 3.4 +env: + - VIM_VERSION="74" + - VIM_VERSION="mercurial" + # - VIM_VERSION="NEOVIM" + +install: + # Some of these commands fail transiently. We keep retrying them until they succeed. + - until sudo add-apt-repository ppa:kalakris/tmux -y; do sleep 10; done + - until sudo add-apt-repository ppa:neovim-ppa/unstable -y; do sleep 10; done + - until sudo apt-get update -qq; do sleep 10; done + - until sudo apt-get install -qq -y --force-yes tmux xclip gdb neovim mercurial; do sleep 10; done + - ./travis_install.sh + +script: + - ./travis_test.sh + +notifications: + webhooks: + urls: + - https://webhooks.gitter.im/e/558acac434012ba838cd + on_success: change # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: false # default: false diff --git a/COPYING.txt b/COPYING.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, 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 +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..fc1dcd3 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,167 @@ +version 3.1 (07-Dec-2015): + - This is the last release done by @SirVer. The new maintainer of UltiSnips + is @seletskiy. The repository remains https://github.com/SirVer/ultisnips, + so this should not affect any users. This is also the last release to be + published on vim.org. Please follow the master branch on GitHub for the + latest stable version. + - New option `e`: Context aware snippets. This gives very precise and + powerful control over which snippet should be expanded depending on + surrounding code. *UltiSnips-context-snippets* + - New option `m`: Trim whitespace in all snippet lines. + - Very powerful, freely configurable pre/post-expand and post-jump actions + allow for transforming the buffer outside the snippet. *UltiSnips-snippet-actions* + - Automatic triggering of snippets without the need to press the expand + trigger. *UltiSnips-autotrigger* + - Better error reporting for snippet errors including python stacktraces + and listing of executed code. + - Undo is more granular. Each jump and expand is now a separate undo step. + - UltiSnips now emits autocommands on certain events. *UltiSnips-custom-autocommands* + - clearsnippets now clears all snippets below the current priority. This + fits better with the priority system introduced in 3.0. + - snipMate snippets support can be disabled. *UltiSnipsEnableSnipMate* + - UltiSnipsEditSplit got a new value 'context'. *UltiSnipsEditSplit* + - Improved syntax highlighting for snippets filetype. + - Mappings and autocommands are now only established when needed, i.e. when + a snippet is active. This boosts performance outside of snippets. + - New integration with Unite, TagBar, and deoplete. + - New Ctags configuration file for snippet definitions. + - Bug fixes, performance improvements, code cleanups and refactorings. + - No longer supports Vim < 7.4. + +version 3.0 (02-Mar-2014): + - Organisational changes: The project is now hosted on github. Snippets are + now shipped separately - please track honza/vim-snippets. + - UltiSnips is now a drop in replacement for snipMate - it parses snipMate + snippets and expands them emulating snipMates smaller feature set. + - Filetype tab completion for UltiSnipsEdit. + - UltiSnipsEdit now only edits private snippet files. Use UltiSnipsEdit! if + you want to edit shipped files. + - New option 's' which strips trailing whitespace before jumping to next + tabstop + - New option 'a' which converts non-ascii characters into ascii characters + in transformations. + - New keyword in snippet files: priority defines which snippets should + overwrite others. This deprecates the '!' option. + *UltiSnips-adding-snippets* + - Remove common whitespace of visual line selections before inserting in an + indented tabstop. + - Support for overwriting the snippet directory name on a per buffer basis + to support per project snippets. *UltiSnips-snippet-search-path* + - The keymaps for jumping in snippets are now only mapped when a snippet is + active, allowing them to be used for something else otherwise. + - Expanding and jumping no longer overwrites the unnamed register. + - Integration with Valloric/YouCompleteMe and Shougo/neocomplete.vim. + - Other plugins can add sources for snippets to create snippets on the fly. + *UltiSnips-extending* + - Vim functions now indicates if it did any work. + *UltiSnips-trigger-functions* + - For python extensions: UltiSnips adds itself to the sys.path and can be + easily imported if it is available. *UltiSnips-python-module-path* + - A new function giving programmatic access to the snippets currently + available for expansion for other plugins integrating with UltiSnips. + *UltiSnips_SnippetsInCurrentScope* + - New or improved snippets (now in a different repo): all, bib, c, cpp, cs, + d, django, eruby, go, haskell, html, html, htmljinja, java, javascript, + js, ledger, ocaml, perl, php, puppet, python, ruby, scss, sh, tex, vim, + xml, zsh. + +version 2.2 (01-Sep-2012): + - Support to silence Python-not-found warnings. *UltiSnips-python-warning* + - Matchit support for snippet files. + - Improvements to syntax file. + - Various smaller bug fixes. + - New command to manually add a filetype to the list for the current + buffer. *:UltiSnipsAddFiletypes* + - New or improved snippets: all, snippets, haskell, bindzone, python, golang, + json, html, coffee, coffee_jasmine, javascript_jasmine, ruby, php, + markdown. + +version 2.1 (14-Feb-2012): + - Python interpolation access to text from visual selection via snip.v. + - Support for transformations of ${VISUAL} texts. + - New or improved snippets: python, tex, texmath, ruby, rails, html, django + +version 2.0 (05-Feb-2012): + - Backwards incompatible change: Support for normal mode editing. Snippets + are no longer exited when leaving insert mode but only by leaving the + text span of the snippets. This allows usage of normal mode commands and + autoformatting. It also increases compatibility with other plugins. + - Backwards incompatible change: Changed glob patterns for snippets to + behave more like Vim *UltiSnips-adding-snippets* + - Backwards incompatible change: Zero Tabstop is no longer removed in + nested snippets + - Support for ${VISUAL:default text} placeholder. *UltiSnips-visual-placeholder* + - Improved handling of utf-8 characters in files and snippet definitions. + - Full support for :py3. UltiSnips now works with python >= 2.6 or >= 3.2. + - New or improved snippets: python, all + +version 1.6 (30-Dec-2011): + - Significant speed improvements and a few bugs fixed. + - Better handling of non ASCII chars in snippets by assuming UTF-8 encoding + when no other information is available. + - Contributions for UltiSnips are now also accepted on GitHub: https://github.com/SirVer/ultisnips/ + - New or improved snippets: ruby, rails, xhtml + +version 1.5 (24-Sep-2011): + - Some critical bug fixes for new vim versions. + - New or improved snippets: tex, texmath, python, jinja2, go, puppet, xhtml + - Configuration of search path for snippets *UltiSnips-snippet-search-path* + - New parser implementation: A little faster, more flexible and less bugged. + +version 1.4 (17-Jul-2011): + - New or improved snippets: php, html, djangohtml, mako, lua + - Snippets are now listed alphabetically by their trigger, no longer in + order of appearance + - Snippet files are now automatically reloaded when they change. + - Support for other directory names for snippets beside + "UltiSnips" *UltiSnips-snippet-search-path* + - Errors are now shown in a scratch window. + - Now fully supports Windows with python >= 2.6. UltiSnips should now work + on all systems that Vim runs on. + - a syntax file was added for snippets files with nice highlighting. + - snippets definition files now have the filetype 'snippets'. It used to be + 'snippet'. + +version 1.3 (14-Feb-2011): + - Erlang snippets (g0rdin) + - Other VimScripts can now define and immediately expand anonymous snippets + ( *UltiSnips_Anon* ) (Ryan Wooden) + - Other VimScripts can now define new snippets via a function + ( *UltiSnips_AddSnippet* ) (Ryan Wooden) + - New Snippets for eruby and rails (Ches Martin). + - A new Option 't' has been added to snippets that avoid expanding tabstops. + Be also more consistent with how indenting is handled. (Ryan Wooden) + - Added a ftplugin script for .snippets files. Syntax highlighting still + missing. (Rupa Deadwyler) + - Added UltiSnipsReset and UltiSnipsEdit (Idea by JCEB) + +version 1.2 (24-Aug-2010): + - many bugs were fixed + - smode mappings for printable characters are now removed before expanding a + snippet. This is configurable. *UltiSnips-warning-smappings* + - all shipped snippets are now fully compatible with UltiSnips + - added support for global snippets which enhance python interpolation even + more *UltiSnips-globals* + - added support for multi word and regular expression triggers. Very + powerful in combination with python interpolation. + - Python interpolation became much more powerful *UltiSnips-python* + - added support for clearsnippets command *UltiSnips-clearing-snippets* + - added support for option w which is a little more strict than i. + - added support for listing of valid triggers. Defaults to . + - added support for option i (inword expansion) + - extends keyword is now supported on the first line of snippet files. This makes it easy to + define special cases, for example cpp extends c: a cpp trigger is useless + in c, but a c trigger is valuable for cpp. + - UltiSnips now adheres to expandtab and tabstop options of vim + +version 1.1 (21-Jul-2009): + - Made triggers configurable. You can also use the same trigger for + expanding and tabbing. The TextMate configuration and is now + possible. + - Conditional Inserts can now be nested + - Added support for b option. This only considers a snippet at the beginning + of a line ( *UltiSnips-adding-snippets* ) + - Added support for ! option. This overwrites previously defined snippets + with the same tab trigger ( *UltiSnips-adding-snippets* ) + - Support for dotted filetype syntax. Now snippets for more than one filetype + can be active ( *UltiSnips-adding-snippets* ) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d03d6eb --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +[![Build Status](https://travis-ci.org/SirVer/ultisnips.svg?branch=master)](https://travis-ci.org/SirVer/ultisnips) +[![Stories in Ready](https://badge.waffle.io/SirVer/ultisnips.png?label=ready&title=Ready)](https://waffle.io/SirVer/ultisnips) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SirVer/ultisnips?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +UltiSnips +========= + +UltiSnips is the ultimate solution for snippets in Vim. It has tons of features +and is very fast. + +![GIF Demo](https://raw.github.com/SirVer/ultisnips/master/doc/demo.gif) + +In this demo I am editing a python file. I first expand the `#!` snippet, then +the `class` snippet. The completion menu comes from +[YouCompleteMe](https://github.com/Valloric/YouCompleteMe), UltiSnips also +integrates with [neocomplete](https://github.com/Shougo/neocomplete.vim). I can +jump through placeholders and add text while the snippet inserts text in other +places automatically: when I add `Animal` as a base class, `__init__` gets +updated to call the base class constructor. When I add arguments to the +constructor, they automatically get assigned to instance variables. I then +insert my personal snippet for `print` debugging. Note that I left insert mode, +inserted another snippet and went back to add an additional argument to +`__init__` and the class snippet was still active and added another instance +variable. + +The official home of UltiSnips is at . +Please add pull requests and issues there. + +UltiSnips was started in Jun 2009 by @SirVer. In Dec 2015, maintenance was +handed over to @seletskiy. + +Quick Start +----------- + +This assumes you are using [Vundle](https://github.com/gmarik/Vundle.vim). Adapt +for your plugin manager of choice. Put this into your `.vimrc`. + + " Track the engine. + Plugin 'SirVer/ultisnips' + + " Snippets are separated from the engine. Add this if you want them: + Plugin 'honza/vim-snippets' + + " Trigger configuration. Do not use if you use https://github.com/Valloric/YouCompleteMe. + let g:UltiSnipsExpandTrigger="" + let g:UltiSnipsJumpForwardTrigger="" + let g:UltiSnipsJumpBackwardTrigger="" + + " If you want :UltiSnipsEdit to split your window. + let g:UltiSnipsEditSplit="vertical" + +UltiSnips comes with comprehensive +[documentation](https://github.com/SirVer/ultisnips/blob/master/doc/UltiSnips.txt). +As there are more options and tons of features I suggest you at least skim it. + +Screencasts +----------- + +From a gentle introduction to really advanced in a few minutes: The blog posts +of the screencasts contain more advanced examples of the things discussed in the +videos. + +- [Episode 1: What are snippets and do I need them?](http://www.sirver.net/blog/2011/12/30/first-episode-of-ultisnips-screencast/) +- [Episode 2: Creating Basic Snippets](http://www.sirver.net/blog/2012/01/08/second-episode-of-ultisnips-screencast/) +- [Episode 3: What's new in version 2.0](http://www.sirver.net/blog/2012/02/05/third-episode-of-ultisnips-screencast/) +- [Episode 4: Python Interpolation](http://www.sirver.net/blog/2012/03/31/fourth-episode-of-ultisnips-screencast/) + +Also the excellent [Vimcasts](http://vimcasts.org) dedicated three episodes to +UltiSnips: + +- [Meet UltiSnips](http://vimcasts.org/episodes/meet-ultisnips/) +- [Using Python interpolation in UltiSnips snippets](http://vimcasts.org/episodes/ultisnips-python-interpolation/) +- [Using selected text in UltiSnips snippets](http://vimcasts.org/episodes/ultisnips-visual-placeholder/) diff --git a/after/plugin/UltiSnips_after.vim b/after/plugin/UltiSnips_after.vim new file mode 100644 index 0000000..33ad9e0 --- /dev/null +++ b/after/plugin/UltiSnips_after.vim @@ -0,0 +1,8 @@ +" Called after everything else to reclaim keys (Needed for Supertab) + +if exists("b:did_after_plugin_ultisnips_after") || !exists("g:_uspy") + finish +endif +let b:did_after_plugin_ultisnips_after = 1 + +call UltiSnips#map_keys#MapKeys() diff --git a/autoload/UltiSnips.vim b/autoload/UltiSnips.vim new file mode 100644 index 0000000..8fdf9c1 --- /dev/null +++ b/autoload/UltiSnips.vim @@ -0,0 +1,158 @@ +if exists("b:did_autoload_ultisnips") || !exists("g:_uspy") + " Define no-op function, called via ftdetect/UltiSnips.vim. + " TODO(sirver): Add a test for that using a bad g:UltiSnipsPythonVersion + " setting. Without this fix moving the cursor will spam errors, with this + " it should not. + function! UltiSnips#FileTypeChanged() + endfunction + + finish +endif +let b:did_autoload_ultisnips = 1 + +" Also import vim as we expect it to be imported in many places. +exec g:_uspy "import vim" +exec g:_uspy "from UltiSnips import UltiSnips_Manager" + +function! s:compensate_for_pum() + """ The CursorMovedI event is not triggered while the popup-menu is visible, + """ and it's by this event that UltiSnips updates its vim-state. The fix is + """ to explicitly check for the presence of the popup menu, and update + """ the vim-state accordingly. + if pumvisible() + exec g:_uspy "UltiSnips_Manager._cursor_moved()" + endif +endfunction + +function! UltiSnips#Edit(bang, ...) + if a:0 == 1 && a:1 != '' + let type = a:1 + else + let type = "" + endif + exec g:_uspy "vim.command(\"let file = '%s'\" % UltiSnips_Manager._file_to_edit(vim.eval(\"type\"), vim.eval('a:bang')))" + + if !len(file) + return + endif + + let mode = 'e' + if exists('g:UltiSnipsEditSplit') + if g:UltiSnipsEditSplit == 'vertical' + let mode = 'vs' + elseif g:UltiSnipsEditSplit == 'horizontal' + let mode = 'sp' + elseif g:UltiSnipsEditSplit == 'context' + let mode = 'vs' + if winwidth(0) <= 2 * (&tw ? &tw : 80) + let mode = 'sp' + endif + endif + endif + exe ':'.mode.' '.escape(file, ' ') +endfunction + +function! UltiSnips#AddFiletypes(filetypes) + exec g:_uspy "UltiSnips_Manager.add_buffer_filetypes('" . a:filetypes . ".all')" + return "" +endfunction + +function! UltiSnips#FileTypeComplete(arglead, cmdline, cursorpos) + let ret = {} + let items = map( + \ split(globpath(&runtimepath, 'syntax/*.vim'), '\n'), + \ 'fnamemodify(v:val, ":t:r")' + \ ) + call insert(items, 'all') + for item in items + if !has_key(ret, item) && item =~ '^'.a:arglead + let ret[item] = 1 + endif + endfor + + return sort(keys(ret)) +endfunction + +function! UltiSnips#ExpandSnippet() + exec g:_uspy "UltiSnips_Manager.expand()" + return "" +endfunction + +function! UltiSnips#ExpandSnippetOrJump() + call s:compensate_for_pum() + exec g:_uspy "UltiSnips_Manager.expand_or_jump()" + return "" +endfunction + +function! UltiSnips#ListSnippets() + exec g:_uspy "UltiSnips_Manager.list_snippets()" + return "" +endfunction + +function! UltiSnips#SnippetsInCurrentScope() + let g:current_ulti_dict = {} + exec g:_uspy "UltiSnips_Manager.snippets_in_current_scope()" + return g:current_ulti_dict +endfunction + +function! UltiSnips#SaveLastVisualSelection() + exec g:_uspy "UltiSnips_Manager._save_last_visual_selection()" + return "" +endfunction + +function! UltiSnips#JumpBackwards() + call s:compensate_for_pum() + exec g:_uspy "UltiSnips_Manager.jump_backwards()" + return "" +endfunction + +function! UltiSnips#JumpForwards() + call s:compensate_for_pum() + exec g:_uspy "UltiSnips_Manager.jump_forwards()" + return "" +endfunction + +function! UltiSnips#FileTypeChanged() + exec g:_uspy "UltiSnips_Manager.reset_buffer_filetypes()" + exec g:_uspy "UltiSnips_Manager.add_buffer_filetypes('" . &ft . "')" + return "" +endfunction + + +function! UltiSnips#AddSnippetWithPriority(trigger, value, description, options, filetype, priority) + exec g:_uspy "trigger = vim.eval(\"a:trigger\")" + exec g:_uspy "value = vim.eval(\"a:value\")" + exec g:_uspy "description = vim.eval(\"a:description\")" + exec g:_uspy "options = vim.eval(\"a:options\")" + exec g:_uspy "filetype = vim.eval(\"a:filetype\")" + exec g:_uspy "priority = vim.eval(\"a:priority\")" + exec g:_uspy "UltiSnips_Manager.add_snippet(trigger, value, description, options, filetype, priority)" + return "" +endfunction + +function! UltiSnips#Anon(value, ...) + " Takes the same arguments as SnippetManager.expand_anon: + " (value, trigger="", description="", options="") + exec g:_uspy "args = vim.eval(\"a:000\")" + exec g:_uspy "value = vim.eval(\"a:value\")" + exec g:_uspy "UltiSnips_Manager.expand_anon(value, *args)" + return "" +endfunction + + +function! UltiSnips#CursorMoved() + exec g:_uspy "UltiSnips_Manager._cursor_moved()" +endf + +function! UltiSnips#LeavingBuffer() + exec g:_uspy "UltiSnips_Manager._leaving_buffer()" +endf + +function! UltiSnips#LeavingInsertMode() + exec g:_uspy "UltiSnips_Manager._leaving_insert_mode()" +endfunction + +function! UltiSnips#TrackChange() + exec g:_uspy "UltiSnips_Manager._track_change()" +endfunction +" }}} diff --git a/autoload/UltiSnips/map_keys.vim b/autoload/UltiSnips/map_keys.vim new file mode 100644 index 0000000..218b507 --- /dev/null +++ b/autoload/UltiSnips/map_keys.vim @@ -0,0 +1,72 @@ +if exists("b:did_autoload_ultisnips_map_keys") || !exists("g:_uspy") + finish +endif +let b:did_autoload_ultisnips_map_keys = 1 + +" The trigger used to expand a snippet. +" NOTE: expansion and forward jumping can, but needn't be the same trigger +if !exists("g:UltiSnipsExpandTrigger") + let g:UltiSnipsExpandTrigger = "" +endif + +" The trigger used to display all triggers that could possible +" match in the current position. +if !exists("g:UltiSnipsListSnippets") + let g:UltiSnipsListSnippets = "" +endif + +" The trigger used to jump forward to the next placeholder. +" NOTE: expansion and forward jumping can be the same trigger. +if !exists("g:UltiSnipsJumpForwardTrigger") + let g:UltiSnipsJumpForwardTrigger = "" +endif + +" The trigger to jump backward inside a snippet +if !exists("g:UltiSnipsJumpBackwardTrigger") + let g:UltiSnipsJumpBackwardTrigger = "" +endif + +" Should UltiSnips unmap select mode mappings automagically? +if !exists("g:UltiSnipsRemoveSelectModeMappings") + let g:UltiSnipsRemoveSelectModeMappings = 1 +end + +" If UltiSnips should remove Mappings, which should be ignored +if !exists("g:UltiSnipsMappingsToIgnore") + let g:UltiSnipsMappingsToIgnore = [] +endif + +" UltiSnipsEdit will use this variable to decide if a new window +" is opened when editing. default is "normal", allowed are also +" "vertical", "horizontal", and "context". +if !exists("g:UltiSnipsEditSplit") + let g:UltiSnipsEditSplit = 'normal' +endif + +" A list of directory names that are searched for snippets. +if !exists("g:UltiSnipsSnippetDirectories") + let g:UltiSnipsSnippetDirectories = [ "UltiSnips" ] +endif + +" Enable or Disable snipmate snippet expansion. +if !exists("g:UltiSnipsEnableSnipMate") + let g:UltiSnipsEnableSnipMate = 1 +endif + +function! UltiSnips#map_keys#MapKeys() + if g:UltiSnipsExpandTrigger == g:UltiSnipsJumpForwardTrigger + exec "inoremap " . g:UltiSnipsExpandTrigger . " =UltiSnips#ExpandSnippetOrJump()" + exec "snoremap " . g:UltiSnipsExpandTrigger . " :call UltiSnips#ExpandSnippetOrJump()" + else + exec "inoremap " . g:UltiSnipsExpandTrigger . " =UltiSnips#ExpandSnippet()" + exec "snoremap " . g:UltiSnipsExpandTrigger . " :call UltiSnips#ExpandSnippet()" + endif + exec "xnoremap " . g:UltiSnipsExpandTrigger. " :call UltiSnips#SaveLastVisualSelection()gvs" + exec "inoremap " . g:UltiSnipsListSnippets . " =UltiSnips#ListSnippets()" + exec "snoremap " . g:UltiSnipsListSnippets . " :call UltiSnips#ListSnippets()" + + snoremap c + snoremap c + snoremap c + snoremap "_c +endf diff --git a/autoload/neocomplete/sources/ultisnips.vim b/autoload/neocomplete/sources/ultisnips.vim new file mode 100644 index 0000000..5f45e5e --- /dev/null +++ b/autoload/neocomplete/sources/ultisnips.vim @@ -0,0 +1,32 @@ +let s:save_cpo = &cpo +set cpo&vim + +let s:source = { + \ 'name' : 'ultisnips', + \ 'kind' : 'keyword', + \ 'mark' : '[US]', + \ 'rank' : 8, + \ 'matchers' : + \ (g:neocomplete#enable_fuzzy_completion ? + \ ['matcher_fuzzy'] : ['matcher_head']), + \ } + +function! s:source.gather_candidates(context) + let suggestions = [] + let snippets = UltiSnips#SnippetsInCurrentScope() + for trigger in keys(snippets) + let description = get(snippets, trigger) + call add(suggestions, { + \ 'word' : trigger, + \ 'menu' : self.mark . ' '. description + \ }) + endfor + return suggestions +endfunction + +function! neocomplete#sources#ultisnips#define() + return s:source +endfunction + +let &cpo = s:save_cpo +unlet s:save_cpo diff --git a/autoload/unite/sources/ultisnips.vim b/autoload/unite/sources/ultisnips.vim new file mode 100644 index 0000000..58b94c0 --- /dev/null +++ b/autoload/unite/sources/ultisnips.vim @@ -0,0 +1,67 @@ +let s:save_cpo = &cpo +set cpo&vim + +let s:unite_source = { + \ 'name': 'ultisnips', + \ 'hooks': {}, + \ 'action_table': {}, + \ 'default_action': 'expand', + \ } + +let s:unite_source.action_table.preview = { + \ 'description' : 'ultisnips snippets', + \ 'is_quit' : 0, + \ } + +function! s:unite_source.action_table.preview.func(candidate) + " no nice preview at this point, cannot get snippet text + let snippet_preview = a:candidate['word'] + echo snippet_preview +endfunction + +let s:unite_source.action_table.expand = { + \ 'description': 'expand the current snippet', + \ 'is_quit': 1 + \} + +function! s:unite_source.action_table.expand.func(candidate) + let delCurrWord = (getline(".")[col(".")-1] == " ") ? "" : "diw" + exe "normal " . delCurrWord . "a" . a:candidate['trigger'] . " " + call UltiSnips#ExpandSnippet() + return '' +endfunction + +function! s:unite_source.get_longest_snippet_len(snippet_list) + let longest = 0 + for snip in items(a:snippet_list) + if strlen(snip['word']) > longest + let longest = strlen(snip['word']) + endif + endfor + return longest +endfunction + +function! s:unite_source.gather_candidates(args, context) + let default_val = {'word': '', 'unite__abbr': '', 'is_dummy': 0, 'source': + \ 'ultisnips', 'unite__is_marked': 0, 'kind': 'command', 'is_matched': 1, + \ 'is_multiline': 0} + let snippet_list = UltiSnips#SnippetsInCurrentScope() + let max_len = s:unite_source.get_longest_snippet_len(snippet_list) + let canditates = [] + for snip in items(snippet_list) + let curr_val = copy(default_val) + let curr_val['word'] = printf('%-*s', max_len, snip[0]) . " " . snip[1] + let curr_val['trigger'] = snip[0] + call add(canditates, curr_val) + endfor + return canditates +endfunction + +function! unite#sources#ultisnips#define() + return s:unite_source +endfunction + +"unlet s:unite_source + +let &cpo = s:save_cpo +unlet s:save_cpo diff --git a/ctags/UltiSnips.cnf b/ctags/UltiSnips.cnf new file mode 100644 index 0000000..c30aa8b --- /dev/null +++ b/ctags/UltiSnips.cnf @@ -0,0 +1,3 @@ +--langdef=UltiSnips +--langmap=UltiSnips:.snippets +--regex-UltiSnips=/^snippet (.*)/\1/s,snippet/ diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt new file mode 100644 index 0000000..c6b3ace --- /dev/null +++ b/doc/UltiSnips.txt @@ -0,0 +1,1778 @@ +*UltiSnips.txt* For Vim version 7.0 or later. + + The Ultimate Plugin for Snippets in Vim~ + +UltiSnips *snippet* *snippets* *UltiSnips* + +1. Description |UltiSnips-description| + 1.1 Requirements |UltiSnips-requirements| + 1.2 Acknowledgments |UltiSnips-acknowledgments| +2. Installation and Updating |UltiSnips-installnupdate| +3. Settings & Commands |UltiSnips-settings| + 3.1 Commands |UltiSnips-commands| + 3.2 Triggers |UltiSnips-triggers| + 3.2.1 Using your own trigger functions |UltiSnips-trigger-functions| + 3.2.2 Custom autocommands |UltiSnips-custom-autocommands| + 3.2.3 Path to Python Module |UltiSnips-python-module-path| + 3.3 Snippet Search Path |UltiSnips-snippet-search-path| + 3.4 Warning About Select Mode Mappings |UltiSnips-warning-smappings| + 3.5 Functions |UltiSnips-functions| + 3.5.1 UltiSnips#AddSnippetWithPriority |UltiSnips#AddSnippetWithPriority| + 3.5.2 UltiSnips#Anon |UltiSnips#Anon| + 3.5.3 UltiSnips#SnippetsInCurrentScope |UltiSnips#SnippetsInCurrentScope| + 3.6 Missing python support |UltiSnips-python-warning| +4. Syntax |UltiSnips-syntax| + 4.1 Adding Snippets |UltiSnips-adding-snippets| + 4.1.1 Character Escaping |UltiSnips-character-escaping| + 4.2 Plaintext Snippets |UltiSnips-plaintext-snippets| + 4.3 Visual Placeholder |UltiSnips-visual-placeholder| + 4.4 Interpolation |UltiSnips-interpolation| + 4.4.1 Shellcode |UltiSnips-shellcode| + 4.4.2 VimScript |UltiSnips-vimscript| + 4.4.3 Python |UltiSnips-python| + 4.4.4 Global Snippets |UltiSnips-globals| + 4.5 Tabstops and Placeholders |UltiSnips-tabstops| + 4.6 Mirrors |UltiSnips-mirrors| + 4.7 Transformations |UltiSnips-transformations| + 4.7.1 Replacement String |UltiSnips-replacement-string| + 4.7.2 Demos |UltiSnips-demos| + 4.8 Clearing snippets |UltiSnips-clearing-snippets| + 4.9 Context snippets |UltiSnips-context-snippets| + 4.10 Snippet actions |UltiSnips-snippet-actions| + 4.10.1 Pre-expand actions |UltiSnips-pre-expand-actions| + 4.10.2 Post-expand actions |UltiSnips-post-expand-actions| + 4.10.3 Post-jump actions |UltiSnips-post-jump-actions| + 4.11 Autotrigger |UltiSnips-autotrigger| +5. UltiSnips and Other Plugins |UltiSnips-other-plugins| + 5.1 Existing Integrations |UltiSnips-integrations| + 5.2 Extending UltiSnips |UltiSnips-extending| +6. Helping Out |UltiSnips-helping| +7. Contributors |UltiSnips-contributors| + +This plugin only works if 'compatible' is not set. +{Vi does not have any of these features} +{only available when |+python| or |+python3| have been enabled at compile time} + + +============================================================================== +1. Description *UltiSnips-description* + +UltiSnips provides snippet management for the Vim editor. A snippet is a short +piece of text that is either re-used often or contains a lot of redundant +text. UltiSnips allows you to insert a snippet with only a few key strokes. +Snippets are common in structured text like source code but can also be used +for general editing like, for example, inserting a signature in an email or +inserting the current date in a text file. + +@SirVer posted several short screencasts which make a great introduction to +UltiSnips, illustrating its features and usage. + +http://www.sirver.net/blog/2011/12/30/first-episode-of-ultisnips-screencast/ +http://www.sirver.net/blog/2012/01/08/second-episode-of-ultisnips-screencast/ +http://www.sirver.net/blog/2012/02/05/third-episode-of-ultisnips-screencast/ +http://www.sirver.net/blog/2012/03/31/fourth-episode-of-ultisnips-screencast/ + +Also the excellent [Vimcasts](http://vimcasts.org) dedicated three episodes to +UltiSnips: + +http://vimcasts.org/episodes/meet-ultisnips/ +http://vimcasts.org/episodes/ultisnips-python-interpolation/ +http://vimcasts.org/episodes/ultisnips-visual-placeholder/ + +1.1 Requirements *UltiSnips-requirements* +---------------- + +This plugin works with Vim version 7.4 or later. It only works if the +'compatible' setting is not set. + +This plugin is tested against Python 2.7, 3.3 or 3.4. All other versions are +unsupported, but might work. + +The Python 2.x or Python 3.x interface must be available. In other words, Vim +must be compiled with either the |+python| feature or the |+python3| feature. +The following commands show how to test if you have python compiled in Vim. +They print '1' if the python version is compiled in, '0' if not. + +Test if Vim is compiled with python version 2.x: > + :echo has("python") +The python version Vim is linked against can be found with: > + :py import sys; print(sys.version) + +Test if Vim is compiled with python version 3.x: > + :echo has("python3") +The python version Vim is linked against can be found with: > + :py3 import sys; print(sys.version) + +Note that Vim is maybe not using your system-wide installed python version, so +make sure to check the Python version inside of Vim. + +UltiSnips attempts to auto-detect which python version is compiled into Vim. +Unfortunately, in some versions of Vim this detection does not work. +In that case you have to explicitly tell UltiSnips which version to use using +the 'UltiSnipsUsePythonVersion' global variable. + +To use python version 2.x: > + let g:UltiSnipsUsePythonVersion = 2 + +To use python version 3.x: > + let g:UltiSnipsUsePythonVersion = 3 + + +1.2 Acknowledgments *UltiSnips-acknowledgments* +------------------- + +UltiSnips was inspired by the snippets feature of TextMate +(http://macromates.com/), the GUI text editor for Mac OS X. Managing snippets +in Vim is not new. I want to thank Michael Sanders, the author of snipMate, +for some implementation details I borrowed from his plugin and for the +permission to use his snippets. + + +============================================================================= +2. Installation and Updating *UltiSnips-installnupdate* + +The recommended way of getting UltiSnips is to track SirVer/ultisnips on +github. The master branch is always stable. + +Using Pathogen: *UltiSnips-using-pathogen* + +If you are a pathogen user, you can track the official mirror of UltiSnips on +github: > + + $ cd ~/.vim/bundle && git clone git://github.com/SirVer/ultisnips.git + +If you also want the default snippets, also track > + + $ cd ~/.vim/bundle && git clone git://github.com/honza/vim-snippets.git + +See the pathogen documentation for more details on how to update a bundle. + + +Using a downloaded packet: *UltiSnips-using-a-downloaded-packet* + +Download the packet and unpack into a directory of your choice. Then add this +directory to your Vim runtime path by adding this line to your vimrc file. > + set runtimepath+=~/.vim/ultisnips_rep + +UltiSnips also needs that Vim sources files from the ftdetect/ directory. +Unfortunately, Vim only allows this directory in the .vim directory. You +therefore have to symlink/copy the files: > + mkdir -p ~/.vim/ftdetect/ + ln -s ~/.vim/ultisnips_rep/ftdetect/* ~/.vim/ftdetect/ + +Restart Vim and UltiSnips should work. To access the help, use > + :helptags ~/.vim/ultisnips_rep/doc + :help UltiSnips + +UltiSnips comes without snippets. The default snippets can be found here: +https://github.com/honza/vim-snippets + +============================================================================= +3. Settings & Commands *UltiSnips-settings* + +3.1 Commands *UltiSnips-commands* +------------ + *:UltiSnipsEdit* +The UltiSnipsEdit command opens a private snippet definition file for the +current filetype. If no snippet file exists, a new file is created. If used as +UltiSnipsEdit! all public snippet files are taken into account too. If +multiple files match the search, the user gets to choose the file. + +There are several variables associated with the UltiSnipsEdit command. + + *g:UltiSnipsEditSplit* +g:UltiSnipsEditSplit Defines how the edit window is opened. Possible + values: + |normal| Default. Opens in the current window. + |horizontal| Splits the window horizontally. + |vertical| Splits the window vertically. + |context| Splits the window vertically or + horizontally depending on context. + + *g:UltiSnipsSnippetsDir* +g:UltiSnipsSnippetsDir + Defines the directory private snippet definition + files are stored in. For example, if the variable + is set to "~/.vim/mydir/UltiSnips" and the current + 'filetype' is "cpp", then :UltiSnipsEdit will open + "~/.vim/mydir/UltiSnips/cpp.snippets". Note that + directories named "snippets" are reserved for + snipMate snippets and cannot be used. + + *g:UltiSnipsSnippetDirectories* +g:UltiSnipsSnippetDirectories + Defines the directories for looking for snippets. + Do not mix up this variable with previous one. + More information about that variable can + be found at section |UltiSnips-snippet-search-path|. + + *g:UltiSnipsEnableSnipMate* +g:UltiSnipsEnableSnipMate + Enable looking for SnipMate snippets in + &runtimepath. UltiSnips will search only for + directories named 'snippets' while looking for + SnipMate snippets. Defaults to "1", so UltiSnips + will look for SnipMate snippets. + + + *:UltiSnipsAddFiletypes* +The UltiSnipsAddFiletypes command allows for explicit merging of other snippet +filetypes for the current buffer. For example, if you edit a .rst file but +also want the Lua snippets to be available you can issue the command > + + :UltiSnipsAddFiletypes rst.lua + +using the dotted filetype syntax. Order is important, the first filetype in +this list will be the one used for UltiSnipsEdit and the list is +ordered by evaluation priority. Consequently, you might add this to your +ftplugin/rails.vim > + + :UltiSnipsAddFiletypes rails.ruby + +I mention rails first because I want to edit rails snippets when using +UltiSnipsEdit and because rails snippets should overwrite equivalent ruby +snippets. The priority will now be rails -> ruby -> all. If you have some +special programming snippets that should have lower priority than your ruby +snippets you can call > + + :UltiSnipsAddFiletypes ruby.programming + +The priority will then be rails -> ruby -> programming -> all. + +3.2 Triggers *UltiSnips-triggers* +------------ + + *g:UltiSnipsExpandTrigger* *g:UltiSnipsListSnippets* + *g:UltiSnipsJumpForwardTrigger* *g:UltiSnipsJumpBackwardTrigger* +You can define the keys used to trigger UltiSnips actions by setting global +variables. Variables define the keys used to expand a snippet, jump forward +and jump backwards within a snippet, and list all available snippets in the +current expand context. Be advised, that some terminal emulators don't send + to the running program. The variables with their default values are: > + g:UltiSnipsExpandTrigger + g:UltiSnipsListSnippets + g:UltiSnipsJumpForwardTrigger + g:UltiSnipsJumpBackwardTrigger + +UltiSnips will only map the jump triggers while a snippet is active to +interfere as little as possible with other mappings. + +The default value for g:UltiSnipsJumpBackwardTrigger interferes with the +built-in complete function: |i_CTRL-X_CTRL-K|. A workaround is to add the +following to your vimrc file or switching to a plugin like Supertab or +YouCompleteMe. > + inoremap + +3.2.1 Using your own trigger functions *UltiSnips-trigger-functions* +-------------------------------------- + +For advanced users there are four functions that you can map directly to a +key and that correspond to some of the triggers previously defined: + g:UltiSnipsExpandTrigger <--> UltiSnips#ExpandSnippet + g:UltiSnipsJumpForwardTrigger <--> UltiSnips#JumpForwards + g:UltiSnipsJumpBackwardTrigger <--> UltiSnips#JumpBackwards + +If you have g:UltiSnipsExpandTrigger and g:UltiSnipsJumpForwardTrigger set +to the same value then the function you are actually going to use is +UltiSnips#ExpandSnippetOrJump. + +Each time any of the functions UltiSnips#ExpandSnippet, +UltiSnips#ExpandSnippetOrJump, UltiSnips#JumpForwards or +UltiSnips#JumpBackwards is called a global variable is set that contains the +return value of the corresponding function. + +The corresponding variables and functions are: +UltiSnips#ExpandSnippet --> g:ulti_expand_res (0: fail, 1: success) +UltiSnips#ExpandSnippetOrJump --> g:ulti_expand_or_jump_res (0: fail, + 1: expand, 2: jump) +UltiSnips#JumpForwards --> g:ulti_jump_forwards_res (0: fail, 1: success) +UltiSnips#JumpBackwards --> g:ulti_jump_backwards_res (0: fail, 1: success) + +To see how these return values may come in handy, suppose that you want to map +a key to expand or jump, but if none of these actions is successful you want +to call another function. UltiSnips already does this automatically for +supertab, but this allows you individual fine tuning of your Tab key usage. + +Usage is as follows: You define a function > + + let g:ulti_expand_or_jump_res = 0 "default value, just set once + function! Ulti_ExpandOrJump_and_getRes() + call UltiSnips#ExpandSnippetOrJump() + return g:ulti_expand_or_jump_res + endfunction + +then you define your mapping as > + + inoremap =(Ulti_ExpandOrJump_and_getRes() > 0)?"":IMAP_Jumpfunc('', 0) + +and if the you can't expand or jump from the current location then the +alternative function IMAP_Jumpfunc('', 0) is called. + +3.2.2 Custom autocommands *UltiSnips-custom-autocommands* +------------------------- + +Note Autocommands must *not* change the buffer in any way. If lines are added, +deleted, or modified it will confuse UltiSnips which might scramble your +snippets contents. + + *UltiSnipsEnterFirstSnippet* *UltiSnipsExitLastSnippet* +For maximum compatibility with other plug-ins, UltiSnips sets up some special +state, include mappings and autocommands, when a snippet starts being +expanded, and tears them down once the last snippet has been exited. In order +to make it possible to override these "inner" settings, it fires the following +"User" autocommands: + +UltiSnipsEnterFirstSnippet +UltiSnipsExitLastSnippet + +For example, to call a pair of custom functions in response to these events, +you might do: > + + autocmd! User UltiSnipsEnterFirstSnippet + autocmd User UltiSnipsEnterFirstSnippet call CustomInnerKeyMapper() + autocmd! User UltiSnipsExitLastSnippet + autocmd User UltiSnipsExitLastSnippet call CustomInnerKeyUnmapper() + +Note that snippet expansion may be nested, in which case +|UltiSnipsEnterFirstSnippet| will fire only as the first (outermost) snippet +is entered, and |UltiSnipsExitLastSnippet| will only fire once the last +(outermost) snippet have been exited. + + + +3.2.3 Path to Python module *UltiSnips-python-module-path* +--------------------------- + +For even more advanced usage, you can directly write python functions using +UltiSnip's python modules. + +Here is a small example funtion that expands a snippet: > + + function! s:Ulti_ExpandSnip() + Python << EOF + import sys, vim + from UltiSnips import UltiSnips_Manager + UltiSnips_Manager.expand() + EOF + return "" + endfunction + +3.3 Snippet Search Path *UltiSnips-snippet-search-path* +----------------------- + +UltiSnips snippet definition files are stored in one or more directories. +There are several variables used to indicate those directories and to define +how UltiSnips loads snippets. + +Snippet definition files are stored in snippet directories. A snippet +directory must be a subdirectory of a directory defined in the 'runtimepath' +option. The variable g:UltiSnipsSnippetDirectories defines a list of names +used for snippet directories. Note that "snippets" is reserved for snipMate +snippets and cannot be used. The default is shown below. > + + let g:UltiSnipsSnippetDirectories=["UltiSnips"] + +UltiSnips will search each 'runtimepath' directory for the subdirectory names +defined in g:UltiSnipsSnippetDirectories in the order they are defined. For +example, if you keep your snippets in a .vim subdirectory called +"mycoolsnippets" and you want to make use of the default snippets that come +with UltiSnips, add the following to your vimrc file. > + let g:UltiSnipsSnippetDirectories=["UltiSnips", "mycoolsnippets"] +If you do not want to use the third party snippets that come with plugins, +define the variable accordingly: > + let g:UltiSnipsSnippetDirectories=["mycoolsnippets"] + +You can also redefine the search path on a buffer by buffer basis by setting +the variable b:UltiSnipsSnippetDirectories. This variable takes precedence +over the global variable. + +|UltiSnips-adding-snippets| explains which files are parsed for a given filetype. + +If only one directory is specified in this variable and this directory is +specified by absolute path, UltiSnips will not look for snippets in +&runtimepath, which can lead to significant speedup. So, the common case is: + + let g:UltiSnipsSnippetDirectories=[$HOME.'/.vim/UltiSnips'] + +However, you will not able to use snippets that are shipped with third party +plugins out of the box. You'll need to copy them into your chosen directory. + + +3.4 Warning About Select Mode Mappings *UltiSnips-warning-smappings* +-------------------------------------- + +Vim's help document for |mapmode-s| states: > + NOTE: Mapping a printable character in Select mode may confuse the user. + It's better to explicitly use :xmap and :smap for printable characters. Or + use :sunmap after defining the mapping. + +However, most Vim plugins, including some default Vim plugins, do not adhere +to this. UltiSnips uses Select mode to mark tabstops in snippets for +overwriting. Existing Visual+Select mode mappings will interfere. Therefore, +UltiSnips issues a |:sunmap| command to remove each Select mode mapping for +printable characters. No other mappings are touched. In particular, UltiSnips +does not change existing normal, insert or visual mode mappings. + +If this behavior is not desired, you can disable it by adding this line to +your vimrc file. > + let g:UltiSnipsRemoveSelectModeMappings = 0 + +If you want to disable this feature for specific mappings only, add them to +the list of mappings to be ignored. For example, the following lines in your +vimrc file will unmap all Select mode mappings except those mappings +containing either the string "somePlugin" or the string "otherPlugin" in its +complete definition as listed by the |:smap| command. > + + let g:UltiSnipsRemoveSelectModeMappings = 1 + let g:UltiSnipsMappingsToIgnore = [ "somePlugin", "otherPlugin" ] + + +3.5 Functions *UltiSnips-functions* +------------- + +UltiSnips provides some functions for extending core functionality. + + + 3.5.1 UltiSnips#AddSnippetWithPriority *UltiSnips#AddSnippetWithPriority* + +The first function is UltiSnips#AddSnippetWithPriority(trigger, value, description, +options, filetyp, priority). It adds a new snippet with the provided trigger, value, +description, and options to the current list of snippets. See +|UltiSnips-syntax| for details on the meaning of the function arguments. The +Priority is a number that defines which snippet should be preferred over +others. See the priority keyword in|UltiSnips-add-snippets|. + + + 3.5.2 UltiSnips#Anon *UltiSnips#Anon* + +The second function is UltiSnips#Anon(value, ...). It expands an anonymous +snippet. Anonymous snippets are defined on the spot, expanded and immediately +discarded again. Anonymous snippets are not added to the global list of +snippets, so they cannot be expanded a second time unless the function is +called again. The function takes three optional arguments, in order: trigger, +description, options. Arguments coincide with the arguments of the +|UltiSnips#AddSnippetWithPriority| function of the same name. The trigger and +options arguments can change the way the snippet expands. The description is +unused at this point. + +An example use case might be this line from a reStructuredText plugin file: + + inoremap $$ $$=UltiSnips#Anon(':latex:\`$1\`', '$$') + +This expands the snippet whenever two $ signs are typed. +Note: The right-hand side of the mapping starts with an immediate retype of +the '$$' trigger and passes '$$' to the function as the trigger argument. +This is required in order for UltiSnips to have access to the characters +typed so it can determine if the trigger matches or not. + + 3.5.3 UltiSnips#SnippetsInCurrentScope *UltiSnips#SnippetsInCurrentScope* + +A third function is UltiSnips#SnippetsInCurrentScope which is the equivalent +of snipmate GetSnipsInCurrentScope function. +This function simply returns a vim dictionary with the snippets whose trigger +matches the current word. +This function does not add any new functionality to ultisnips directly but +allows to use third party plugins to integrate the current available snippets. + +An example of such third party plugin is SnippetCompleteSnipMate which uses +the function GetSnipsInCurrentScope to integrate the current available +snippets with user defined abbreviations and provides these and a completion +menu. +This script is located in +http://www.vim.org/scripts/script.php?script_id=4276. +Note: If you check the above website it lists two dependencies: the +SnippetComplete plugin and snipmate. +You do need the SnippetComplete plugin but you obviously don't need snipmate, +you just have to define the function GetSnipsInCurrentScope. Put the following +in your vimrc: + +function! GetSnipsInCurrentScope() + return UltiSnips#SnippetsInCurrentScope() +endfunction + + +As a second example on how to use this function consider the following +function and mapping definition: + +function! ExpandPossibleShorterSnippet() + if len(UltiSnips#SnippetsInCurrentScope()) == 1 "only one candidate... + let curr_key = keys(UltiSnips#SnippetsInCurrentScope())[0] + normal diw + exe "normal a" . curr_key + exe "normal a " + return 1 + endif + return 0 +endfunction +inoremap =(ExpandPossibleShorterSnippet() == 0? '': UltiSnips#ExpandSnippet()) + +If the trigger for your snippet is lorem, you type lor, and you have no other +snippets whose trigger matches lor then hitting will expand to whatever +lorem expands to. + + +3.6 Warning about missing python support *UltiSnips-python-warning* +---------------------------------------- + +When UltiSnips is loaded, it will check that the running Vim was compiled with +python support. If no support is detected, a warning will be displayed and +loading of UltiSnips will be skipped. + +If you would like to suppress this warning message, you may add the following +line to your vimrc file. + + let g:UltiSnipsNoPythonWarning = 1 + +This may be useful if your Vim configuration files are shared across several +systems where some of them may not have Vim compiled with python support. + +============================================================================= +4. Syntax *UltiSnips-syntax* + +This chapter describes how to write your own snippets and snippet definition +syntax. Examples are used to help illustrate. + + +4.1 Adding Snippets *UltiSnips-adding-snippets* +------------------- + +See |UltiSnips-snippet-search-path| for an explanation of where directories +with snippet definitions should be located. + +Using a strategy similar to how Vim detects |ftplugins|, UltiSnips iterates +over the snippet definition directories looking for files with names of the +following patterns: ft.snippets, ft_*.snippets, or ft/*, where "ft" is the +'filetype' of the current document and "*" is a shell-like wildcard matching +any string including the empty string. The following table shows some typical +snippet filenames and their associated filetype. + + snippet filename filetype ~ + ruby.snippets ruby + perl.snippets perl + c.snippets c + c_my.snippets c + c/a c + c/b.snippets c + all.snippets *all + all/a.snippets *all + +* The 'all' filetype is unique. It represents snippets available for use when +editing any document regardless of the filetype. A date insertion snippet, for +example, would fit well in the all.snippets file. + +UltiSnips understands Vim's dotted filetype syntax. For example, if you define +a dotted filetype for the CUDA C++ framework, e.g. ":set ft=cuda.cpp", then +UltiSnips will search for and activate snippets for both the cuda and cpp +filetypes. + +The snippets file syntax is simple. All lines starting with a # character are +considered comments. Comments are ignored by UltiSnips. Use them to document +snippets. + +A line beginning with the keyword 'extends' provides a way of combining +snippet files. When the 'extends' directive is included in a snippet file, it +instructs UltiSnips to include all snippets from the indicated filetypes. + +The syntax looks like this: > + extends ft1, ft2, ft3 + +For example, the first line in cpp.snippets looks like this: > + extends c +When UltiSnips activates snippets for a cpp file, it first looks for all c +snippets and activates them as well. This is a convenient way to create +specialized snippet files from more general ones. Multiple 'extends' lines are +permitted in a snippet file, and they can be included anywhere in the file. + + +A line beginning with the keyword 'priority' sets the priority for all +snippets defined in the current file after this line. The default priority for +a file is always 0. When a snippet should be expanded, UltiSnips will collect +all snippet definitions from all sources that match the trigger and keep only +the ones with the highest priority. For example, all shipped snippets have a +priority < 0, so that user defined snippets always overwrite shipped snippets. + + +A line beginning with the keyword 'snippet' marks the beginning of snippet +definition and a line starting with the keyword 'endsnippet' marks the end. +The snippet definition is placed between the lines. Here is a snippet of an +'if' statement for the Unix shell (sh) filetype. + + snippet if "if ... then (if)" + if ${2:[[ ${1:condition} ]]}; then + ${0:#statements} + fi + endsnippet + +The start line takes the following form: > + + snippet tab_trigger [ "description" [ options ] ] + +The tab_trigger is required, but the description and options are optional. + +The 'tab_trigger' is the word or string sequence used to trigger the snippet. +Generally a single word is used but the tab_trigger can include spaces. If you +wish to include spaces, you must wrap the tab trigger in quotes. > + + snippet "tab trigger" [ "description" [ options ] ] + +The quotes are not part of the trigger. To activate the snippet type: tab trigger +followed by the snippet expand character. + +It is not technically necessary to use quotes to wrap a trigger with spaces. +Any matching characters will do. For example, this is a valid snippet starting +line. > + snippet !tab trigger! [ "description" [ options ] ] + +Quotes can be included as part of the trigger by wrapping the trigger in +another character. > + snippet !"tab trigger"! [ "description" [ options ] ] + +To activate this snippet one would type: "tab trigger" + +The 'description' is a string describing the trigger. It is helpful for +documenting the snippet and for distinguishing it from other snippets with the +same tab trigger. When a snippet is activated and more than one tab trigger +match, UltiSnips displays a list of the matching snippets with their +descriptions. The user then selects the snippet they want. + +The 'options' control the behavior of the snippet. Options are indicated by +single characters. The 'options' characters for a snippet are combined into +a word without spaces. + +The options currently supported are: > + b Beginning of line - A snippet with this option is expanded only if the + tab trigger is the first word on the line. In other words, if only + whitespace precedes the tab trigger, expand. The default is to expand + snippets at any position regardless of the preceding non-whitespace + characters. + + i In-word expansion - By default a snippet is expanded only if the tab + trigger is the first word on the line or is preceded by one or more + whitespace characters. A snippet with this option is expanded + regardless of the preceding character. In other words, the snippet can + be triggered in the middle of a word. + + w Word boundary - With this option, the snippet is expanded if + the tab trigger start matches a word boundary and the tab trigger end + matches a word boundary. In other words the tab trigger must be + preceded and followed by non-word characters. Word characters are + defined by the 'iskeyword' setting. Use this option, for example, to + permit expansion where the tab trigger follows punctuation without + expanding suffixes of larger words. + + r Regular expression - With this option, the tab trigger is expected to + be a python regular expression. The snippet is expanded if the recently + typed characters match the regular expression. Note: The regular + expression MUST be quoted (or surrounded with another character) like a + multi-word tab trigger (see above) whether it has spaces or not. A + resulting match is passed to any python code blocks in the snippet + definition as the local variable "match". + + t Do not expand tabs - If a snippet definition includes leading tab + characters, by default UltiSnips expands the tab characters honoring + the Vim 'shiftwidth', 'softtabstop', 'expandtab' and 'tabstop' + indentation settings. (For example, if 'expandtab' is set, the tab is + replaced with spaces.) If this option is set, UltiSnips will ignore the + Vim settings and insert the tab characters as is. This option is useful + for snippets involved with tab delimited formats, for example. + + s Remove whitespace immediately before the cursor at the end of a line + before jumping to the next tabstop. This is useful if there is a + tabstop with optional text at the end of a line. + + m Trim all whitespaces from right side of snippet lines. Useful when + snippet contains empty lines which should remain empty after expanding. + Without this option empty lines in snippets definition will have + indentation too. + + e Context snippets - With this option expansion of snippet can be + controlled not only by previous characters in line, but by any given + python expression. This option can be specified along with other + options, like 'b'. See |UltiSnips-context-snippets| for more info. + + A Snippet will be triggered automatically, when condition matches. + See |UltiSnips-autotrigger| for more info. + +The end line is the 'endsnippet' keyword on a line by itself. > + + endsnippet + +When parsing snippet files, UltiSnips chops the trailing newline character +from the 'endsnippet' end line. + + + 4.1.1 Character Escaping: *UltiSnips-character-escaping* + +In snippet definitions, the characters '`', '{', '$' and '\' have special +meaning. If you want to insert one of these characters literally, escape them +with a backslash, '\'. + + +4.2 Plaintext Snippets *UltiSnips-plaintext-snippets* +---------------------- + +To illustrate plaintext snippets, let's begin with a simple example. You can +try the examples yourself. Simply edit a new file with Vim. Example snippets +will be added to the 'all.snippets' file, so you'll want to open it in Vim for +editing as well. > + ~/.vim/UltiSnips/all.snippets + +Add this snippet to 'all.snippets' and save the file. + +------------------- SNIP ------------------- +snippet bye "My mail signature" +Good bye, Sir. Hope to talk to you soon. +- Arthur, King of Britain +endsnippet +------------------- SNAP ------------------- + +UltiSnips detects when you write changes to a snippets file and automatically +makes the changes active. So in the empty buffer, type the tab trigger 'bye' +and then press the key. + +bye --> +Good bye, Sir. Hope to talk to you soon. +- Arthur, King of Britain + +The word 'bye' will be replaced with the text of the snippet definition. + + +4.3 Visual Placeholder *UltiSnips-visual-placeholder* +---------------------- + +Snippets can contain a special placeholder called ${VISUAL}. The ${VISUAL} +variable is expanded with the text selected just prior to expanding the +snippet. + +To see how a snippet with a ${VISUAL} placeholder works, define a snippet with +the placeholder, use Vim's Visual mode to select some text, and then press the +key you use to trigger expanding a snippet (see g:UltiSnipsExpandTrigger). The +selected text is deleted, and you are dropped into Insert mode. Now type the +snippet tab trigger and press the key to trigger expansion. As the snippet +expands, the previously selected text is printed in place of the ${VISUAL} +placeholder. + +The ${VISUAL} placeholder can contain default text to use when the snippet has +been triggered when not in Visual mode. The syntax is: > + ${VISUAL:default text} + +The ${VISUAL} placeholder can also define a transformation (see +|UltiSnips-transformations|). The syntax is: > + ${VISUAL:default/search/replace/option}. + +Here is a simple example illustrating a visual transformation. The snippet +will take selected text, replace every instance of "should" within it with +"is" , and wrap the result in tags. + +------------------- SNIP ------------------- +snippet t +${VISUAL:inside text/should/is/g} +endsnippet +------------------- SNAP ------------------- + +Start with this line of text: > + this should be cool + +Position the cursor on the word "should", then press the key sequence: viw +(visual mode -> select inner word). Then press , type "t" and press +again. The result is: > + -> this is be cool + +If you expand this snippet while not in Visual mode (e.g., in Insert mode type +t), you will get: > + inside text + + +4.4 Interpolation *UltiSnips-interpolation* +----------------- + + 4.4.1 Shellcode: *UltiSnips-shellcode* + +Snippets can include shellcode. Put a shell command in a snippet and when the +snippet is expanded, the shell command is replaced by the output produced when +the command is executed. The syntax for shellcode is simple: wrap the code in +backticks, '`'. When a snippet is expanded, UltiSnips runs shellcode by first +writing it to a temporary script and then executing the script. The shellcode +is replaced by the standard output. Anything you can run as a script can be +used in shellcode. Include a shebang line, for example, #!/usr/bin/perl, and +your snippet has the ability to run scripts using other programs, perl, for +example. + +Here are some examples. This snippet uses a shell command to insert the +current date. + +------------------- SNIP ------------------- +snippet today +Today is the `date +%d.%m.%y`. +endsnippet +------------------- SNAP ------------------- + +today -> +Today is the 15.07.09. + + +This example inserts the current date using perl. + +------------------- SNIP ------------------- +snippet today +Today is `#!/usr/bin/perl +@a = localtime(); print $a[3] . '.' . $a[4] . '.' . ($a[5]+1900);`. +endsnippet +------------------- SNAP ------------------- +today -> +Today is 15.6.2009. + + + 4.4.2 VimScript: *UltiSnips-vimscript* + +You can also use Vim scripts (sometimes called VimL) in interpolation. The +syntax is similar to shellcode. Wrap the code in backticks and to distinguish +it as a Vim script, start the code with '!v'. Here is an example that counts +the indent of the current line: + +------------------- SNIP ------------------- +snippet indent +Indent is: `!v indent(".")`. +endsnippet +------------------- SNAP ------------------- + (note the 4 spaces in front): indent -> + (note the 4 spaces in front): Indent is: 4. + + + 4.4.3 Python: *UltiSnips-python* + +Python interpolation is by far the most powerful. The syntax is similar to Vim +scripts except code is started with '!p'. Python scripts can be run using the +python shebang '#!/usr/bin/python', but using the '!p' format comes with some +predefined objects and variables, which can simplify and shorten code. For +example, a 'snip' object instance is implied in python code. Python code using +the '!p' indicator differs in another way. Generally when a snippet is +expanded the standard output of code replaces the code. With python code the +value of the 'rv' property of the 'snip' instance replaces the code. Standard +output is ignored. + +The variables automatically defined in python code are: > + + fn - The current filename + path - The complete path to the current file + t - The values of the placeholders, t[1] is the text of ${1}, etc. + snip - UltiSnips.TextObjects.SnippetUtil object instance. Has methods + that simplify indentation handling. + context - Result of context condition. See |UltiSnips-context-snippets|. + +The 'snip' object provides the following methods: > + + snip.mkline(line="", indent=None): + Returns a line ready to be appended to the result. If indent + is None, then mkline prepends spaces and/or tabs appropriate to the + current 'tabstop' and 'expandtab' variables. + + snip.shift(amount=1): + Shifts the default indentation level used by mkline right by the + number of spaces defined by 'shiftwidth', 'amount' times. + + snip.unshift(amount=1): + Shifts the default indentation level used by mkline left by the + number of spaces defined by 'shiftwidth', 'amount' times. + + snip.reset_indent(): + Resets the indentation level to its initial value. + + snip.opt(var, default): + Checks if the Vim variable 'var' has been set. If so, it returns the + variable's value; otherwise, it returns the value of 'default'. + +The 'snip' object provides some properties as well: > + + snip.rv: + 'rv' is the return value, the text that will replace the python block + in the snippet definition. It is initialized to the empty string. This + deprecates the 'res' variable. + + snip.c: + The text currently in the python block's position within the snippet. + It is set to empty string as soon as interpolation is completed. Thus + you can check if snip.c is != "" to make sure that the interpolation + is only done once. This deprecates the "cur" variable. + + snip.v: + Data related to the ${VISUAL} placeholder. The property has two + attributes: + snip.v.mode ('v', 'V', '^V', see |visual-mode| ) + snip.v.text The text that was selected. + + snip.fn: + The current filename. + + snip.basename: + The current filename with the extension removed. + + snip.ft: + The current filetype. + +For your convenience, the 'snip' object also provides the following +operators: > + + snip >> amount: + Equivalent to snip.shift(amount) + snip << amount: + Equivalent to snip.unshift(amount) + snip += line: + Equivalent to "snip.rv += '\n' + snip.mkline(line)" + +Any variables defined in a python block can be used in other python blocks +that follow within the same snippet. Also, the python modules 'vim', 're', +'os', 'string' and 'random' are pre-imported within the scope of snippet code. +Other modules can be imported using the python 'import' command. + +Python code allows for very flexible snippets. For example, the following +snippet mirrors the first tabstop value on the same line but right aligned and +in uppercase. + +------------------- SNIP ------------------- +snippet wow +${1:Text}`!p snip.rv = (75-2*len(t[1]))*' '+t[1].upper()` +endsnippet +------------------- SNAP ------------------- +wowHello World -> +Hello World HELLO WORLD + +The following snippet uses the regular expression option and illustrates +regular expression grouping using python's match object. It shows that the +expansion of a snippet can depend on the tab trigger used to define the +snippet, and that tab trigger itself can vary. + +------------------- SNIP ------------------- +snippet "be(gin)?( (\S+))?" "begin{} / end{}" br +\begin{${1:`!p +snip.rv = match.group(3) if match.group(2) is not None else "something"`}} + ${2:${VISUAL}} +\end{$1}$0 +endsnippet +------------------- SNAP ------------------- +becenter -> +\begin{center} + +\end{center} +------------------- SNAP ------------------- +be center -> +\begin{center} + +\end{center} + +The second form is a variation of the first; both produce the same result, +but it illustrates how regular expression grouping works. Using regular +expressions in this manner has some drawbacks: +1. If you use the key for both expanding snippets and completion then + if you typed "be form" expecting the completion "be formatted", you + would end up with the above SNAP instead, not what you want. +2. The snippet is harder to read. + + + 4.4.4 Global Snippets: *UltiSnips-globals* + +Global snippets provide a way to reuse common code in multiple snippets. +Currently, only python code is supported. The result of executing the contents +of a global snippet is put into the globals of each python block in the +snippet file. To create a global snippet, use the keyword 'global' in place of +'snippet', and for python code, you use '!p' for the trigger. For example, the +following snippet produces the same output as the last example . However, with +this syntax the 'upper_right' snippet can be reused by other snippets. + +------------------- SNIP ------------------- +global !p +def upper_right(inp): + return (75 - 2 * len(inp))*' ' + inp.upper() +endglobal + +snippet wow +${1:Text}`!p snip.rv = upper_right(t[1])` +endsnippet +------------------- SNAP ------------------- +wowHello World -> +Hello World HELLO WORLD + +Python global functions can be stored in a python module and then imported. +This makes global functions easily accessible to all snippet files. Since Vim +7.4 you can just drop python files into ~/.vim/pythonx and import them +directly inside your snippets. For example to use +~/.vim/pythonx/my_snippets_helpers.py > + + global !p + from my_snippet_helpers import * + endglobal + + +4.5 Tabstops and Placeholders *UltiSnips-tabstops* *UltiSnips-placeholders* +----------------------------- + +Snippets are used to quickly insert reused text into a document. Often the +text has a fixed structure with variable components. Tabstops are used to +simplify modifying the variable content. With tabstops you can easily place +the cursor at the point of the variable content, enter the content you want, +then jump to the next variable component, enter that content, and continue +until all the variable components are complete. + +The syntax for a tabstop is the dollar sign followed by a number, for example, +'$1'. Tabstops start at number 1 and are followed in sequential order. The +'$0' tabstop is a special tabstop. It is always the last tabstop in the +snippet no matter how many tabstops are defined. If there is no '$0' defined, +'$0' tabstop will be defined at the end of snippet. + +Here is a simple example. + +------------------- SNIP ------------------- +snippet letter +Dear $1, +$0 +Yours sincerely, +$2 +endsnippet +------------------- SNAP ------------------- +letterBenPaulThanks for suggesting UltiSnips!-> +Dear Ben, +Thanks for suggesting UltiSnips! +Yours sincerely, +Paul + +You can use to jump to the next tabstop, and to jump to the +previous. The key was not used for jumping forward because many people +(myself included) use for completion. See |UltiSnips-triggers| for +help on defining different keys for tabstops. + +It is often useful to have some default text for a tabstop. The default text +may be a value commonly used for the variable component, or it may be a word +or phrase that reminds you what is expected for the variable component. To +include default text, the syntax is '${1:value}'. + +The following example illustrates a snippet for the shell 'case' statement. +The tabstops use default values to remind the user of what value is expected. + +------------------- SNIP ------------------- +snippet case +case ${1:word} in + ${2:pattern} ) $0;; +esac +endsnippet +------------------- SNAP ------------------- + +case$option-vverbose=true +case $option in + -v ) verbose=true;; +esac + + +Sometimes it is useful to have a tabstop within a tabstop. To do this, simply +include the nested tabstop as part of the default text. Consider the following +example illustrating an HTML anchor snippet. + +------------------- SNIP ------------------- +snippet a + + $0 + +endsnippet +------------------- SNAP ------------------- + +When this snippet is expanded, the first tabstop has a default value of +'http://www.example.com'. If you want the 'http://' schema, jump to the next +tabstop. It has a default value of 'example.com'. This can be replaced by +typing whatever domain you want. + +agoogle.comGoogle -> + + Google + + +If at the first tabstop you want a different url schema or want to replace the +default url with a named anchor, '#name', for example, just type the value you +want. + +a#topTop -> + + Top + + +In the last example, typing any text at the first tabstop replaces the default +value, including the second tabstop, with the typed text. So the second +tabstop is essentially deleted. When a tabstop jump is triggered, UltiSnips +moves to the next remaining tabstop '$0'. This feature can be used +intentionally as a handy way for providing optional tabstop values to the +user. Here is an example to illustrate. + +------------------- SNIP ------------------- +snippet a + + $0 + +endsnippet +------------------- SNAP ------------------- + +Here, '$1' marks the first tabstop. It is assumed you always want to add a +value for the 'href' attribute. After entering the url and pressing , the +snippet will jump to the second tabstop, '$2'. This tabstop is optional. The +default text is ' class="link"'. You can press to accept the tabstop, +and the snippet will jump to the third tabstop, '$3', and you can enter the +class attribute value, or, at the second tabstop you can press the backspace +key thereby replacing the second tabstop default with an empty string, +essentially removing it. In either case, continue by pressing and the +snippet will jump to the final tabstop inside the anchor. + +ahttp://www.google.comvisitedGoogle -> + + Google + + +ahttp://www.google.comGoogle -> + + Google + + +The default text of tabstops can also contain mirrors, transformations or +interpolation. + + +4.6 Mirrors *UltiSnips-mirrors* +----------- + +Mirrors repeat the content of a tabstop. During snippet expansion when you +enter the value for a tabstop, all mirrors of that tabstop are replaced with +the same value. To mirror a tabstop simply insert the tabstop again using the +"dollar sign followed by a number" syntax, e.g., '$1'. + +A tabstop can be mirrored multiple times in one snippet, and more than one +tabstop can be mirrored in the same snippet. A mirrored tabstop can have a +default value defined. Only the first instance of the tabstop need have a +default value. Mirrored tabstop will take on the default value automatically. + +Mirrors are handy for start-end tags, for example, TeX 'begin' and 'end' tag +labels, XML and HTML tags, and C code #ifndef blocks. Here are some snippet +examples. + +------------------- SNIP ------------------- +snippet env +\begin{${1:enumerate}} + $0 +\end{$1} +endsnippet +------------------- SNAP ------------------- +envitemize -> +\begin{itemize} + +\end{itemize} + +------------------- SNIP ------------------- +snippet ifndef +#ifndef ${1:SOME_DEFINE} +#define $1 +$0 +#endif /* $1 */ +endsnippet +------------------- SNAP ------------------- +ifndefWIN32 -> +#ifndef WIN32 +#define WIN32 + +#endif /* WIN32 */ + + +4.7 Transformations *UltiSnips-transformations* +------------------- + +Note: Transformations are a bit difficult to grasp so this chapter is divided +into two sections. The first describes transformations and their syntax, and +the second illustrates transformations with demos. + +Transformations are like mirrors but instead of just copying text from the +original tabstop verbatim, a regular expression is matched to the content of +the referenced tabstop and a transformation is then applied to the matched +pattern. The syntax and functionality of transformations in UltiSnips follow +very closely to TextMate transformations. + +A transformation has the following syntax: > + ${ + tab_stop_no - The number of the tabstop to reference + regular_expression - The regular expression the value of the referenced + tabstop is matched on + replacement - The replacement string, explained in detail below + options - Options for the regular expression + +The options can be any combination of > + g - global replace + By default, only the first match of the regular expression is + replaced. With this option all matches are replaced. + i - case insensitive + By default, regular expression matching is case sensitive. With this + option, matching is done without regard to case. + a - ascii conversion + By default, transformation are made on the raw utf-8 string. With + this option, matching is done on the corresponding ASCII string + instead, for example 'à' will become 'a'. + This option required the python package 'unidecode'. + +The syntax of regular expressions is beyond the scope of this document. Python +regular expressions are used internally, so the python 're' module can be used +as a guide. See http://docs.python.org/library/re.html. + +The syntax for the replacement string is unique. The next paragraph describes +it in detail. + + + 4.7.1 Replacement String: *UltiSnips-replacement-string* + +The replacement string can contain $no variables, e.g., $1, which reference +matched groups in the regular expression. The $0 variable is special and +yields the whole match. The replacement string can also contain special escape +sequences: > + \u - Uppercase next letter + \l - Lowercase next letter + \U - Uppercase everything till the next \E + \L - Lowercase everything till the next \E + \E - End upper or lowercase started with \L or \U + \n - A newline + \t - A literal tab + +Finally, the replacement string can contain conditional replacements using the +syntax (?no:text:other text). This reads as follows: if the group $no has +matched, insert "text", otherwise insert "other text". "other text" is +optional and if not provided defaults to the empty string, "". This feature +is very powerful. It allows you to add optional text into snippets. + + + 4.7.2 Demos: *UltiSnips-demos* + +Transformations are very powerful but often the syntax is convoluted. +Hopefully the demos below help illustrate transformation features. + +Demo: Uppercase one character +------------------- SNIP ------------------- +snippet title "Title transformation" +${1:a text} +${1/\w+\s*/\u$0/} +endsnippet +------------------- SNAP ------------------- +titlebig small -> +big small +Big small + + +Demo: Uppercase one character and global replace +------------------- SNIP ------------------- +snippet title "Titlelize in the Transformation" +${1:a text} +${1/\w+\s*/\u$0/g} +endsnippet +------------------- SNAP ------------------- +titlethis is a title -> +this is a title +This Is A Title + + +Demo: ASCII transformation +------------------- SNIP ------------------- +snippet ascii "Replace non ascii chars" +${1: an accentued text} +${1/.*/$0/a} +endsnippet +------------------- SNAP ------------------- +asciià la pêche aux moules +à la pêche aux moules +a la peche aux moules + + +Demo: Regular expression grouping + This is a clever c-like printf snippet, the second tabstop is only shown + when there is a format (%) character in the first tabstop. + +------------------- SNIP ------------------- +snippet printf +printf("${1:%s}\n"${1/([^%]|%%)*(%.)?.*/(?2:, :\);)/}$2${1/([^%]|%%)*(%.)?.*/(?2:\);)/} +endsnippet +------------------- SNAP ------------------- +printfHello // End of line -> +printf("Hello\n"); // End of line + +But +printfA is: %sA // End of line -> +printf("A is: %s\n", A); // End of line + + +There are many more examples of what can be done with transformations in the +bundled snippets. + + +4.8 Clearing snippets *UltiSnips-clearing-snippets* + +To remove snippets for the current file type, use the 'clearsnippets' +directive. + +------------------- SNIP ------------------- +clearsnippets +------------------- SNAP ------------------- + +'clearsnippets' removes all snippets with a priority lower than the current +one. For example, the following cleares all snippets that have priority <= 1, +even though the example snippet is defined after the 'clearsnippets'. + +------------------- SNIP ------------------- +priority 1 +clearsnippets + +priority -1 +snippet example "Cleared example" + This will never be expanded. +endsnippet +------------------- SNAP ------------------- + +To clear one or more specific snippet, provide the triggers of the snippets as +arguments to the 'clearsnippets' command. The following example will clear the +snippets 'trigger1' and 'trigger2'. + +------------------- SNIP ------------------- +clearsnippets trigger1 trigger2 +------------------- SNAP ------------------- + + +4.9 Context snippets *UltiSnips-context-snippets* + +Context snippets can be enabled by using 'e' option in snippet definition. + +In that case snippet should be defined using this syntax: > + + snippet tab_trigger "description" "expression" options + +The 'expression' can be any python expression. If 'expression' evaluates to +'True', then this snippet will be chosen for expansion. The 'expression' must +be wrapped with double-quotes. + +The following python modules are automatically imported into the scope before +'expression' is evaluated: 're', 'os', 'vim', 'string', 'random'. + +Global variable `snip` will be available with following properties: + 'snip.window' - alias for 'vim.current.window' + 'snip.buffer' - alias for 'vim.current.window.buffer' + 'snip.cursor' - cursor object, which behaves like + 'vim.current.window.cursor', but zero-indexed and with following + additional methods: + - 'preserve()' - special method for executing pre/post/jump actions; + - 'set(line, column)' - sets cursor to specified line and column; + - 'to_vim_cursor()' - returns 1-indexed cursor, suitable for assigning + to 'vim.current.window.cursor'; + 'snip.line' and 'snip.column' - aliases for cursor position (zero-indexed); + + +------------------- SNIP ------------------- +snippet r "return" "re.match('^\s+if err ', snip.buffer[snip.line-1])" be +return err +endsnippet +------------------- SNAP ------------------- + +That snippet will expand to 'return err' only if the previous line is starting +from 'if err' prefix. + +Note: context snippets prioritized over non-context ones. It makes possible to +use non-context snippets as fallback, if no context matched: + +------------------- SNIP ------------------- +snippet i "if ..." b +if $1 { + $2 +} +endsnippet + +snippet i "if err != nil" "re.match('^\s+[^=]*err\s*:?=', snip.buffer[snip.line-1])" be +if err != nil { + $1 +} +endsnippet +------------------- SNAP ------------------- + +That snippet will expand into 'if err != nil' if previous line will +match 'err :=' prefix, otherwise the default 'if' snippet will be expanded. + +It's a good idea to move context conditions to a separate module, so it can be +used by other UltiSnips users. In that case, module should be imported +using 'global' keyword, like this: + +------------------- SNIP ------------------- +global !p +import my_utils +endglobal + +snippet , "return ..., nil/err" "my_utils.is_return_argument(snip)" ie +, `!p if my_utils.is_in_err_condition(): + snip.rv = "err" +else: + snip.rv = "nil"` +endsnippet +------------------- SNAP ------------------- + +That snippet will expand only if the cursor is located in the return statement, +and then it will expand either to 'err' or to 'nil' depending on which 'if' +statement it's located. 'is_return_argument' and 'is_in_err_condition' are +part of custom python module which is called 'my_utils' in this example. + +Context condition can return any value which python can use as condition in +it's 'if' statement, and if it's considered 'True', then snippet will be +expanded. The evaluated value of 'condition' is available in the 'snip.context' +variable inside the snippet: + +------------------- SNIP ------------------- +snippet + "var +=" "re.match('\s*(.*?)\s*:?=', snip.buffer[snip.line-1])" ie +`!p snip.rv = snip.context.group(1)` += $1 +endsnippet +------------------- SNAP ------------------- + +That snippet will expand to 'var1 +=' after line, which begins from 'var1 :='. + + +4.10 Snippets actions *UltiSnips-snippet-actions* +--------------------- + +Snippet actions is an arbitrary python code which can be executed at specific +points in lifetime of the snippet. + +There are three types of actions: + +* Pre-expand - invoked just after trigger condition was matched, but before + snippet actually expanded; +* Post-expand - invoked after snippet was expanded and interpolations + was applied for the first time, but before jump on the first placeholder. +* Jump - invoked just after jump to the next/prev placeholder. + +Specified code will be evaluated at stages defined above and same global +variables and modules will be available that are stated in +the |UltiSnips-context-snippets| section. + + *UltiSnips-buffer-proxy* + +Note: special variable called 'snip.buffer' should be used for all buffer +modifications. Not 'vim.current.buffer' and not 'vim.command("...")', because +of in that case UltiSnips will not be able to track changes buffer from +actions. + +'snip.buffer' has the same interface as 'vim.current.window.buffer'. + +4.10.1 Pre-expand actions *UltiSnips-pre-expand-actions* + +Pre-expand actions can be used to match snippet in one location and then +expand it in the different location. Some useful cases are: correcting +indentation for snippet; expanding snippet for function declaration in another +function body with moving expansion point beyond initial function; performing +extract method refactoring via expanding snippet in different place. + +Pre-expand action declared as follows: > + pre_expand "python code here" + snippet ... + endsnippet + +Buffer can be modified in pre-expand action code through variable called +'snip.buffer', snippet expansion position will be automatically adjusted. + +If cursor line (where trigger was matched) need to be modified, then special +variable method 'snip.cursor.set(line, column)' must be called with the +desired cursor position. In that case UltiSnips will not remove any matched +trigger text and it should be done manually in action code. + +To addition to the scope variables defined above 'snip.visual_content' will be +also declared and will contain text that was selected before snippet expansion +(similar to $VISUAL placeholder). + +Following snippet will be expanded at 4 spaces indentation level no matter +where it was triggered. + +------------------- SNIP ------------------- +pre_expand "snip.buffer[snip.line] = ' '*4; snip.cursor.set(line, 4)" +snippet d +def $1(): + $0 +endsnippet +------------------- SNAP ------------------- + +Following snippet will move the selected code to the end of file and create +new method definition for it: + +------------------- SNIP ------------------- +pre_expand "del snip.buffer[snip.line]; snip.buffer.append(''); snip.cursor.set(len(snip.buffer)-1, 0)" +snippet x +def $1(): + ${2:${VISUAL}} +endsnippet +------------------- SNAP ------------------- + +4.10.2 Post-expand actions *UltiSnips-post-expand-actions* + +Post-expand actions can be used to perform some actions based on the expanded +snippet text. Some cases are: code style formatting (e.g. inserting newlines +before and after method declaration), apply actions depending on python +interpolation result. + +Post-expand action declared as follows: > + post_expand "python code here" + snippet ... + endsnippet + +Buffer can be modified in post-expand action code through variable called +'snip.buffer', snippet expansion position will be automatically adjusted. + +Variables 'snip.snippet_start' and 'snip.snippet_end' will be defined at the +action code scope and will point to positions of the start and end of expanded +snippet accordingly in the form '(line, column)'. + +Note: 'snip.snippet_start' and 'snip.snippet_end' will automatically adjust to +the correct positions if post-action will insert or delete lines before +expansion. + +Following snippet will expand to method definition and automatically insert +additional newline after end of the snippet. It's very useful to create a +function that will insert as many newlines as required in specific context. + +------------------- SNIP ------------------- +post_expand "snip.buffer[snip.snippet_end[0]+1:snip.snippet_end[0]+1] = ['']" +snippet d "Description" b +def $1(): + $2 +endsnippet +------------------- SNAP ------------------- + +4.10.3 Post-jump actions *UltiSnips-post-jump-actions* + +Post-jump actions can be used to trigger some code based on user input into +the placeholders. Notable use cases: expand another snippet after jump or +anonymous snippet after last jump (e.g. perform move method refactoring and +then insert new method invokation); insert heading into TOC after last jump. + +Jump-expand action declared as follows: > + post_jump "python code here" + snippet ... + endsnippet + +Buffer can be modified in post-expand action code through variable called +'snip.buffer', snippet expansion position will be automatically adjusted. + +Next variables and methods will be also defined in the action code scope: +* 'snip.tabstop' - number of tabstop jumped onto; +* 'snip.jump_direction' - '1' if jumped forward and '-1' otherwise; +* 'snip.tabstops' - list with tabstop objects, see above; +* 'snip.snippet_start' - (line, column) of start of the expanded snippet; +* 'snip.snippet_end' - (line, column) of end of the expanded snippet; +* 'snip.expand_anon()' - alias for 'UltiSnips_Manager.expand_anon()'; + +Tabstop object has several useful properties: +* 'start' - (line, column) of the starting position of the tabstop (also + accessible as 'tabstop.line' and 'tabstop.col'). +* 'end' - (line, column) of the ending position; +* 'current_text' - text inside the tabstop. + +Following snippet will insert section in the Table of Contents in the vim-help +file: + +------------------- SNIP ------------------- +post_jump "if snip.tabstop == 0: insert_toc_item(snip.tabstops[1], snip.buffer)" +snippet s "section" b +`!p insert_delimiter_0(snip, t)`$1`!p insert_section_title(snip, t)` +`!p insert_delimiter_1(snip, t)` +$0 +endsnippet +------------------- SNAP ------------------- + +'insert_toc_item' will be called after first jump and will add newly entered +section into the TOC for current file. + +Note: It is also possible to trigger snippet expansion from the jump action. +In that case method 'snip.cursor.preserve()' should be called, so UltiSnips +will know that cursor is already at the required position. + +Following example will insert method call at the end of file after user jump +out of method declaration snippet. + +------------------- SNIP ------------------- +global !p +def insert_method_call(name): + vim.command('normal G') + snip.expand_anon(name + '($1)\n') +endglobal + +post_jump "if snip.tabstop == 0: insert_method_call(snip.tabstops[1].current_text)" +snippet d "method declaration" b +def $1(): + $2 +endsnippet +------------------- SNAP ------------------- + +4.11 Autotrigger *UltiSnips-autotrigger* +---------------- + +Note: vim should be newer than 7.4.214 to support this feature. + +Many language constructs can occur only at specific places, so it's +possible to use snippets without manually triggering them. + +Snippet can be marked as autotriggered by specifying 'A' option in the snippet +definition. + +After snippet is defined as being autotriggered, snippet condition will be +checked on every typed character and if condition matches, then snippet will +be triggered. + +*Warning:* using of this feature can lead to significant vim slowdown. If you +discovered that, report an issue to the github.com/SirVer/UltiSnips. + +Consider following snippets, that can be usefull in Go programming: +------------------- SNIP ------------------- +snippet "^p" "package" rbA +package ${1:main} +endsnippet + +snippet "^m" "func main" rbA +func main() { + $1 +} +endsnippet +------------------- SNAP ------------------- + +When "p" character will occur in the beginning of the line, it will be +automatically expanded into "package main". Same with "m" character. There is +no need to press trigger key after "m"" + +============================================================================== +5. UltiSnips and Other Plugins *UltiSnips-other-plugins* + +5.1 Existing Integrations *UltiSnips-integrations* +------------------------- + +UltiSnips has built-in support for some common plugins and there are others +that are aware of UltiSnips and use it to improve the user experience. This is +an incomplete list - if you want to have your plugin listed here, just send a +pull request. + + *UltiSnips-snipMate* + +snipMate - UltiSnips is a drop-in replacement for snipMate. It has many more +features, so porting snippets is still a good idea, but switching has low +friction now. UltiSnips is trying hard to truly emulate snipMate, for example +recursive tabstops are not supported in snipMate snippets (but of course in +UltiSnips snippets). + +YouCompleteMe - comes with out of the box completion support for UltiSnips. It +offers a really nice completion dialogue for snippets. + +neocomplete - UltiSnips ships with a source for neocomplete and therefore +offers out of the box completion dialogue support for it too. + +unite - UltiSnips has a source for unite. As an example of how you can use +it add the following function and mappings to your vimrc: > + + function! UltiSnipsCallUnite() + Unite -start-insert -winheight=100 -immediately -no-empty ultisnips + return '' + endfunction + + inoremap =(pumvisible()? "\C-E>":"")=UltiSnipsCallUnite() + nnoremap a=(pumvisible()? "\C-E>":"")=UltiSnipsCallUnite() + +When typing in either insert or normal mode you will get the unite +interface with matching snippets. Pressing enter will expand the corresponding +snippet. If only one snippet matches the text in front of the cursor will be +expanded when you press the key. + +Supertab - UltiSnips has built-in support for Supertab. Just use a recent +enough version of both plugins and will either expand a snippet or defer +to Supertab for expansion. + +5.2 Extending UltiSnips *UltiSnips-extending* +------------------------- + +UltiSnips allows other plugins to add new snippets on the fly. Since UltiSnips +is written in python, the integration is also on a python basis. A small +example can be found in `test.py`, search for AddNewSnippetSource. Please +contact us on github if you integrate UltiSnips with your plugin so it can be +listed in the docs. + +============================================================================= +6. Helping Out *UltiSnips-helping* + +UltiSnips needs the help of the Vim community to keep improving. Please +consider joining this effort by providing new features or bug reports. + +* Clone the repository on GitHub (git clone git@github.com:SirVer/ultisnips.git), + make your changes and send a pull request on GitHub. +* Make a patch, report a bug/feature request (see below) and attach the patch + to it. + +You can contribute by fixing or reporting bugs in our issue tracker: +https://github.com/sirver/ultisnips/issues + +============================================================================= +7. Contributors *UltiSnips-contributors* + +UltiSnips has been started and maintained from Jun 2009 - Dec 2015 by Holger +Rapp (@SirVer, SirVer@gmx.de). It is now maintained by Stanislav Seletskiy +(@seletskiy). + +This is the list of contributors pre-git in chronological order. For a full +list of contributors take the union of this set and the authors according to +git log. + + JCEB - Jan Christoph Ebersbach + Michael Henry + Chris Chambers + Ryan Wooden + rupa - Rupa Deadwyler + Timo Schmiade + blueyed - Daniel Hahler + expelledboy - Anthony Jackson + allait - Alexey Bezhan + peacech - Charles Gunawan + guns - Sung Pae + shlomif - Shlomi Fish + pberndt - Phillip Berndt + thanatermesis-elive - Thanatermesis + rico-ambiescent - Rico Sta. Cruz + Cody Frazer + suy - Alejandro Exojo + grota - Giuseppe Rota + iiijjjii - Jim Karsten + fgalassi - Federico Galassi + lucapette + Psycojoker - Laurent Peuch + aschrab - Aaron Schrab + stardiviner - NagatoPain + skeept - Jorge Rodrigues + buztard + stephenmckinney - Steve McKinney + Pedro Algarvio - s0undt3ch + Eric Van Dewoestine - ervandew + Matt Patterson - fidothe + Mike Morearty - mmorearty + Stanislav Golovanov - JazzCore + David Briscoe - DavidBriscoe + Keith Welch - paralogiki + Zhao Cai - zhaocai + John Szakmeister - jszakmeister + Jonas Diemer - diemer + Romain Giot - rgiot + Sergey Alexandrov - taketwo + Brian Mock - saikobee + Gernot Höflechner - LFDM + Marcelo D Montu - mMontu + Karl Yngve Lervåg - lervag + Pedro Ferrari - petobens + Ches Martin - ches + Christian - Oberon00 + Andrew Ruder - aeruder + Mathias Fußenegger - mfussenegger + Kevin Ballard - kballard + Ahbong Chang - cwahbong + Glenn Griffin - ggriffiniii + Michael - Pyrohh + Stanislav Seletskiy - seletskiy + Pawel Palucki - ppalucki + Dettorer - dettorer + Zhao Jiarong - kawing-chiu + Ye Ding - dyng + Greg Hurrell - wincent + +vim:tw=78:ts=8:ft=help:norl: diff --git a/doc/demo.gif b/doc/demo.gif new file mode 100644 index 0000000..32affe4 Binary files /dev/null and b/doc/demo.gif differ diff --git a/ftdetect/UltiSnips.vim b/ftdetect/UltiSnips.vim new file mode 100644 index 0000000..81af3a1 --- /dev/null +++ b/ftdetect/UltiSnips.vim @@ -0,0 +1,17 @@ +" This has to be called before ftplugins are loaded. Therefore +" it is here in ftdetect though it maybe shouldn't + +" This is necessary to prevent errors when using vim as a pager. +if exists("vimpager") + finish +endif + +if has("autocmd") && &loadplugins + augroup UltiSnipsFileType + autocmd! + autocmd FileType * call UltiSnips#FileTypeChanged() + augroup END + + " restore 'filetypedetect' group declaration + augroup filetypedetect +endif diff --git a/ftdetect/snippets.vim b/ftdetect/snippets.vim new file mode 100644 index 0000000..2a783ce --- /dev/null +++ b/ftdetect/snippets.vim @@ -0,0 +1,4 @@ +" recognize .snippet files +if has("autocmd") + autocmd BufNewFile,BufRead *.snippets setf snippets +endif diff --git a/ftplugin/snippets.vim b/ftplugin/snippets.vim new file mode 100644 index 0000000..8479a45 --- /dev/null +++ b/ftplugin/snippets.vim @@ -0,0 +1,47 @@ +" Set some sane defaults for snippet files + +if exists('b:did_ftplugin') + finish +endif +let b:did_ftplugin = 1 + +let s:save_cpo = &cpo +set cpo&vim + +" Fold by syntax, but open all folds by default +setlocal foldmethod=syntax +setlocal foldlevel=99 + +setlocal commentstring=#%s + +setlocal noexpandtab +setlocal autoindent nosmartindent nocindent + +" Define match words for use with matchit plugin +" http://www.vim.org/scripts/script.php?script_id=39 +if exists("loaded_matchit") && !exists("b:match_words") + let b:match_ignorecase = 0 + let b:match_words = '^snippet\>:^endsnippet\>,^global\>:^endglobal\>,\${:}' + let s:set_match_words = 1 +endif + +" Add TagBar support +let g:tagbar_type_snippets = { + \ 'ctagstype': 'UltiSnips', + \ 'kinds': [ + \ 's:snippets', + \ ], + \ 'deffile': expand(':p:h:h') . '/ctags/UltiSnips.cnf', + \ } + +" don't unset g:tagbar_type_snippets, it serves no purpose +let b:undo_ftplugin = " + \ setlocal foldmethod< foldlevel< commentstring< + \|setlocal expandtab< autoindent< smartindent< cindent< + \|if get(s:, 'set_match_words') + \|unlet! b:match_ignorecase b:match_words s:set_match_words + \|endif + \" + +let &cpo = s:save_cpo +unlet s:save_cpo diff --git a/plugin/UltiSnips.vim b/plugin/UltiSnips.vim new file mode 100644 index 0000000..f7be308 --- /dev/null +++ b/plugin/UltiSnips.vim @@ -0,0 +1,57 @@ +if exists('did_plugin_ultisnips') || &cp + finish +endif +let did_plugin_ultisnips=1 + +if version < 704 + echohl WarningMsg + echom "UltiSnips requires Vim >= 7.4" + echohl None + finish +endif + +if !exists("g:UltiSnipsUsePythonVersion") + let g:_uspy=":py3 " + if !has("python3") + if !has("python") + if !exists("g:UltiSnipsNoPythonWarning") + echohl WarningMsg + echom "UltiSnips requires py >= 2.7 or py3" + echohl None + endif + unlet g:_uspy + finish + endif + let g:_uspy=":py " + endif +else + " Use user-provided value, but check if it's available. + " This uses `has()`, because e.g. `exists(":python3")` is always 2. + if g:UltiSnipsUsePythonVersion == 2 && has('python') + let g:_uspy=":python " + elseif g:UltiSnipsUsePythonVersion == 3 && has('python3') + let g:_uspy=":python3 " + endif + if !exists('g:_uspy') + echohl WarningMsg + echom "UltiSnips: the Python version from g:UltiSnipsUsePythonVersion (".g:UltiSnipsUsePythonVersion.") is not available." + echohl None + finish + endif +endif + +" The Commands we define. +command! -bang -nargs=? -complete=customlist,UltiSnips#FileTypeComplete UltiSnipsEdit + \ :call UltiSnips#Edit(, ) + +command! -nargs=1 UltiSnipsAddFiletypes :call UltiSnips#AddFiletypes() + +augroup UltiSnips_AutoTrigger + au! + au InsertCharPre * call UltiSnips#TrackChange() + au TextChangedI * call UltiSnips#TrackChange() +augroup END + +call UltiSnips#map_keys#MapKeys() + +" vim: ts=8 sts=4 sw=4 diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..9d986cf --- /dev/null +++ b/pylintrc @@ -0,0 +1,269 @@ +[MASTER] + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook='import sys; sys.path.append("pythonx/")' + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS,compatibility_py3 + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable= + attribute-defined-outside-init, + fixme, + redefined-builtin, + too-few-public-methods, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-locals, + too-many-public-methods, + too-many-return-statements, + too-many-statements, + bad-continuation, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" + + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=_?[a-z_][a-z0-9_]{2,50}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,50}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,50}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{1,50}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{1,50}$ + +# Regular expression which should only match correct attribute names in class +# bodies +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,a,b,x,y,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|wrapper) + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/pythonx/UltiSnips/__init__.py b/pythonx/UltiSnips/__init__.py new file mode 100644 index 0000000..dfd3f48 --- /dev/null +++ b/pythonx/UltiSnips/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Entry point for all thinks UltiSnips.""" + +from UltiSnips.snippet_manager import UltiSnips_Manager diff --git a/pythonx/UltiSnips/_diff.py b/pythonx/UltiSnips/_diff.py new file mode 100644 index 0000000..42a5383 --- /dev/null +++ b/pythonx/UltiSnips/_diff.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Commands to compare text objects and to guess how to transform from one to +another.""" + +from collections import defaultdict +import sys + +from UltiSnips import _vim +from UltiSnips.position import Position + + +def is_complete_edit(initial_line, original, wanted, cmds): + """Returns true if 'original' is changed to 'wanted' with the edit commands + in 'cmds'. + + Initial line is to change the line numbers in 'cmds'. + + """ + buf = original[:] + for cmd in cmds: + ctype, line, col, char = cmd + line -= initial_line + if ctype == 'D': + if char != '\n': + buf[line] = buf[line][:col] + buf[line][col + len(char):] + else: + if line + 1 < len(buf): + buf[line] = buf[line] + buf[line + 1] + del buf[line + 1] + else: + del buf[line] + elif ctype == 'I': + buf[line] = buf[line][:col] + char + buf[line][col:] + buf = '\n'.join(buf).split('\n') + return (len(buf) == len(wanted) and + all(j == k for j, k in zip(buf, wanted))) + + +def guess_edit(initial_line, last_text, current_text, vim_state): + """Try to guess what the user might have done by heuristically looking at + cursor movement, number of changed lines and if they got longer or shorter. + This will detect most simple movements like insertion, deletion of a line + or carriage return. 'initial_text' is the index of where the comparison + starts, 'last_text' is the last text of the snippet, 'current_text' is the + current text of the snippet and 'vim_state' is the cached vim state. + + Returns (True, edit_cmds) when the edit could be guessed, (False, + None) otherwise. + + """ + if not len(last_text) and not len(current_text): + return True, () + pos = vim_state.pos + ppos = vim_state.ppos + + # All text deleted? + if (len(last_text) and + (not current_text or + (len(current_text) == 1 and not current_text[0])) + ): + es = [] + if not current_text: + current_text = [''] + for i in last_text: + es.append(('D', initial_line, 0, i)) + es.append(('D', initial_line, 0, '\n')) + es.pop() # Remove final \n because it is not really removed + if is_complete_edit(initial_line, last_text, current_text, es): + return True, es + if ppos.mode == 'v': # Maybe selectmode? + sv = list(map(int, _vim.eval("""getpos("'<")"""))) + sv = Position(sv[1] - 1, sv[2] - 1) + ev = list(map(int, _vim.eval("""getpos("'>")"""))) + ev = Position(ev[1] - 1, ev[2] - 1) + if 'exclusive' in _vim.eval('&selection'): + ppos.col -= 1 # We want to be inclusive, sorry. + ev.col -= 1 + es = [] + if sv.line == ev.line: + es.append(('D', sv.line, sv.col, + last_text[sv.line - initial_line][sv.col:ev.col + 1])) + if sv != pos and sv.line == pos.line: + es.append(('I', sv.line, sv.col, + current_text[sv.line - initial_line][sv.col:pos.col + 1])) + if is_complete_edit(initial_line, last_text, current_text, es): + return True, es + if pos.line == ppos.line: + if len(last_text) == len(current_text): # Movement only in one line + llen = len(last_text[ppos.line - initial_line]) + clen = len(current_text[pos.line - initial_line]) + if ppos < pos and clen > llen: # maybe only chars have been added + es = ( + ('I', ppos.line, ppos.col, + current_text[ppos.line - initial_line] + [ppos.col:pos.col]), + ) + if is_complete_edit(initial_line, last_text, current_text, es): + return True, es + if clen < llen: + if ppos == pos: # 'x' or DEL or dt or something + es = ( + ('D', pos.line, pos.col, + last_text[ppos.line - initial_line] + [ppos.col:ppos.col + (llen - clen)]), + ) + if is_complete_edit(initial_line, last_text, + current_text, es): + return True, es + if pos < ppos: # Backspacing or dT dF? + es = ( + ('D', pos.line, pos.col, + last_text[pos.line - initial_line] + [pos.col:pos.col + llen - clen]), + ) + if is_complete_edit(initial_line, last_text, + current_text, es): + return True, es + elif len(current_text) < len(last_text): + # where some lines deleted? (dd or so) + es = [] + for i in range(len(last_text) - len(current_text)): + es.append(('D', pos.line, 0, + last_text[pos.line - initial_line + i])) + es.append(('D', pos.line, 0, '\n')) + if is_complete_edit(initial_line, last_text, + current_text, es): + return True, es + else: + # Movement in more than one line + if ppos.line + 1 == pos.line and pos.col == 0: # Carriage return? + es = (('I', ppos.line, ppos.col, '\n'),) + if is_complete_edit(initial_line, last_text, + current_text, es): + return True, es + return False, None + + +def diff(a, b, sline=0): + """ + Return a list of deletions and insertions that will turn 'a' into 'b'. This + is done by traversing an implicit edit graph and searching for the shortest + route. The basic idea is as follows: + + - Matching a character is free as long as there was no + deletion/insertion before. Then, matching will be seen as delete + + insert [1]. + - Deleting one character has the same cost everywhere. Each additional + character costs only have of the first deletion. + - Insertion is cheaper the earlier it happens. The first character is + more expensive that any later [2]. + + [1] This is that world -> aolsa will be "D" world + "I" aolsa instead of + "D" w , "D" rld, "I" a, "I" lsa + [2] This is that "hello\n\n" -> "hello\n\n\n" will insert a newline after + hello and not after \n + """ + d = defaultdict(list) # pylint:disable=invalid-name + seen = defaultdict(lambda: sys.maxsize) + + d[0] = [(0, 0, sline, 0, ())] + cost = 0 + deletion_cost = len(a) + len(b) + insertion_cost = len(a) + len(b) + while True: + while len(d[cost]): + x, y, line, col, what = d[cost].pop() + + if a[x:] == b[y:]: + return what + + if x < len(a) and y < len(b) and a[x] == b[y]: + ncol = col + 1 + nline = line + if a[x] == '\n': + ncol = 0 + nline += 1 + lcost = cost + 1 + if (what and what[-1][0] == 'D' and what[-1][1] == line and + what[-1][2] == col and a[x] != '\n'): + # Matching directly after a deletion should be as costly as + # DELETE + INSERT + a bit + lcost = (deletion_cost + insertion_cost) * 1.5 + if seen[x + 1, y + 1] > lcost: + d[lcost].append((x + 1, y + 1, nline, ncol, what)) + seen[x + 1, y + 1] = lcost + if y < len(b): # INSERT + ncol = col + 1 + nline = line + if b[y] == '\n': + ncol = 0 + nline += 1 + if (what and what[-1][0] == 'I' and what[-1][1] == nline and + what[-1][2] + len(what[-1][-1]) == col and b[y] != '\n' and + seen[x, y + 1] > cost + (insertion_cost + ncol) // 2 + ): + seen[x, y + 1] = cost + (insertion_cost + ncol) // 2 + d[cost + (insertion_cost + ncol) // 2].append( + (x, y + 1, line, ncol, what[:-1] + ( + ('I', what[-1][1], what[-1][2], + what[-1][-1] + b[y]),) + ) + ) + elif seen[x, y + 1] > cost + insertion_cost + ncol: + seen[x, y + 1] = cost + insertion_cost + ncol + d[cost + ncol + insertion_cost].append((x, y + 1, nline, ncol, + what + (('I', line, col, b[y]),)) + ) + if x < len(a): # DELETE + if (what and what[-1][0] == 'D' and what[-1][1] == line and + what[-1][2] == col and a[x] != '\n' and + what[-1][-1] != '\n' and + seen[x + 1, y] > cost + deletion_cost // 2 + ): + seen[x + 1, y] = cost + deletion_cost // 2 + d[cost + deletion_cost // 2].append( + (x + 1, y, line, col, what[:-1] + ( + ('D', line, col, what[-1][-1] + a[x]),)) + ) + elif seen[x + 1, y] > cost + deletion_cost: + seen[x + 1, y] = cost + deletion_cost + d[cost + deletion_cost].append((x + 1, y, line, col, what + + (('D', line, col, a[x]),)) + ) + cost += 1 diff --git a/pythonx/UltiSnips/_vim.py b/pythonx/UltiSnips/_vim.py new file mode 100644 index 0000000..568da3a --- /dev/null +++ b/pythonx/UltiSnips/_vim.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Wrapper functionality around the functions we need from Vim.""" + +import re + +import vim # pylint:disable=import-error +from vim import error # pylint:disable=import-error,unused-import + +from UltiSnips.compatibility import col2byte, byte2col, \ + as_unicode, as_vimencoding +from UltiSnips.position import Position + +from contextlib import contextmanager + + +class VimBuffer(object): + + """Wrapper around the current Vim buffer.""" + + def __getitem__(self, idx): + if isinstance(idx, slice): # Py3 + return self.__getslice__(idx.start, idx.stop) + rv = vim.current.buffer[idx] + return as_unicode(rv) + + def __getslice__(self, i, j): # pylint:disable=no-self-use + rv = vim.current.buffer[i:j] + return [as_unicode(l) for l in rv] + + def __setitem__(self, idx, text): + if isinstance(idx, slice): # Py3 + return self.__setslice__(idx.start, idx.stop, text) + vim.current.buffer[idx] = as_vimencoding(text) + + def __setslice__(self, i, j, text): # pylint:disable=no-self-use + vim.current.buffer[i:j] = [as_vimencoding(l) for l in text] + + def __len__(self): + return len(vim.current.buffer) + + @property + def line_till_cursor(self): # pylint:disable=no-self-use + """Returns the text before the cursor.""" + _, col = self.cursor + return as_unicode(vim.current.line)[:col] + + @property + def number(self): # pylint:disable=no-self-use + """The bufnr() of the current buffer.""" + return vim.current.buffer.number + + @property + def cursor(self): # pylint:disable=no-self-use + """The current windows cursor. + + Note that this is 0 based in col and 0 based in line which is + different from Vim's cursor. + + """ + line, nbyte = vim.current.window.cursor + col = byte2col(line, nbyte) + return Position(line - 1, col) + + @cursor.setter + def cursor(self, pos): # pylint:disable=no-self-use + """See getter.""" + nbyte = col2byte(pos.line + 1, pos.col) + vim.current.window.cursor = pos.line + 1, nbyte +buf = VimBuffer() # pylint:disable=invalid-name + +@contextmanager +def toggle_opt(name, new_value): + old_value = eval('&' + name) + command('set {0}={1}'.format(name, new_value)) + try: + yield + finally: + command('set {0}={1}'.format(name, old_value)) + +@contextmanager +def save_mark(name): + old_pos = get_mark_pos(name) + try: + yield + finally: + if _is_pos_zero(old_pos): + delete_mark(name) + else: + set_mark_from_pos(name, old_pos) + +def escape(inp): + """Creates a vim-friendly string from a group of + dicts, lists and strings.""" + def conv(obj): + """Convert obj.""" + if isinstance(obj, list): + rv = as_unicode('[' + ','.join(conv(o) for o in obj) + ']') + elif isinstance(obj, dict): + rv = as_unicode('{' + ','.join([ + '%s:%s' % (conv(key), conv(value)) + for key, value in obj.iteritems()]) + '}') + else: + rv = as_unicode('"%s"') % as_unicode(obj).replace('"', '\\"') + return rv + return conv(inp) + + +def command(cmd): + """Wraps vim.command.""" + return as_unicode(vim.command(as_vimencoding(cmd))) + + +def eval(text): + """Wraps vim.eval.""" + rv = vim.eval(as_vimencoding(text)) + if not isinstance(rv, (dict, list)): + return as_unicode(rv) + return rv + + +def feedkeys(keys, mode='n'): + """Wrapper around vim's feedkeys function. + + Mainly for convenience. + + """ + if eval('mode()') == 'n': + if keys == 'a': + cursor_pos = get_cursor_pos() + cursor_pos[2] = int(cursor_pos[2]) + 1 + set_cursor_from_pos(cursor_pos) + if keys in 'ai': + keys = 'startinsert' + + if keys == 'startinsert': + command('startinsert') + else: + command(as_unicode(r'call feedkeys("%s", "%s")') % (keys, mode)) + + +def new_scratch_buffer(text): + """Create a new scratch buffer with the text given.""" + vim.command('botright new') + vim.command('set ft=') + vim.command('set buftype=nofile') + + vim.current.buffer[:] = text.splitlines() + + feedkeys(r"\") + + +def virtual_position(line, col): + """Runs the position through virtcol() and returns the result.""" + nbytes = col2byte(line, col) + return line, int(eval('virtcol([%d, %d])' % (line, nbytes))) + + +def select(start, end): + """Select the span in Select mode.""" + _unmap_select_mode_mapping() + + selection = eval('&selection') + + col = col2byte(start.line + 1, start.col) + vim.current.window.cursor = start.line + 1, col + + mode = eval('mode()') + + move_cmd = '' + if mode != 'n': + move_cmd += r"\" + + if start == end: + # Zero Length Tabstops, use 'i' or 'a'. + if col == 0 or mode not in 'i' and \ + col < len(buf[start.line]): + move_cmd += 'i' + else: + move_cmd += 'a' + else: + # Non zero length, use Visual selection. + move_cmd += 'v' + if 'inclusive' in selection: + if end.col == 0: + move_cmd += '%iG$' % end.line + else: + move_cmd += '%iG%i|' % virtual_position(end.line + 1, end.col) + elif 'old' in selection: + move_cmd += '%iG%i|' % virtual_position(end.line + 1, end.col) + else: + move_cmd += '%iG%i|' % virtual_position(end.line + 1, end.col + 1) + move_cmd += 'o%iG%i|o\\' % virtual_position( + start.line + 1, start.col + 1) + feedkeys(move_cmd) + +def set_mark_from_pos(name, pos): + return _set_pos("'" + name, pos) + +def get_mark_pos(name): + return _get_pos("'" + name) + +def set_cursor_from_pos(pos): + return _set_pos('.', pos) + +def get_cursor_pos(): + return _get_pos('.') + +def delete_mark(name): + try: + return command('delma ' + name) + except: + return False + +def _set_pos(name, pos): + return eval("setpos(\"{0}\", {1})".format(name, pos)) + +def _get_pos(name): + return eval("getpos(\"{0}\")".format(name)) + +def _is_pos_zero(pos): + return ['0'] * 4 == pos or [0] == pos + +def _unmap_select_mode_mapping(): + """This function unmaps select mode mappings if so wished by the user. + + Removes select mode mappings that can actually be typed by the user + (ie, ignores things like ). + + """ + if int(eval('g:UltiSnipsRemoveSelectModeMappings')): + ignores = eval('g:UltiSnipsMappingsToIgnore') + ['UltiSnips'] + + for option in ('', ''): + # Put all smaps into a var, and then read the var + command(r"redir => _tmp_smaps | silent smap %s " % option + + '| redir END') + + # Check if any mappings where found + all_maps = list(filter(len, eval(r"_tmp_smaps").splitlines())) + if len(all_maps) == 1 and all_maps[0][0] not in ' sv': + # "No maps found". String could be localized. Hopefully + # it doesn't start with any of these letters in any + # language + continue + + # Only keep mappings that should not be ignored + maps = [m for m in all_maps if + not any(i in m for i in ignores) and len(m.strip())] + + for map in maps: + # The first three chars are the modes, that might be listed. + # We are not interested in them here. + trig = map[3:].split()[0] if len( + map[3:].split()) != 0 else None + + if trig is None: + continue + + # The bar separates commands + if trig[-1] == '|': + trig = trig[:-1] + '' + + # Special ones + if trig[0] == '<': + add = False + # Only allow these + for valid in ['Tab', 'NL', 'CR', 'C-Tab', 'BS']: + if trig == '<%s>' % valid: + add = True + if not add: + continue + + # UltiSnips remaps . Keep this around. + if trig == '': + continue + + # Actually unmap it + try: + command('silent! sunmap %s %s' % (option, trig)) + except: # pylint:disable=bare-except + # Bug 908139: ignore unmaps that fail because of + # unprintable characters. This is not ideal because we + # will not be able to unmap lhs with any unprintable + # character. If the lhs stats with a printable + # character this will leak to the user when he tries to + # type this character as a first in a selected tabstop. + # This case should be rare enough to not bother us + # though. + pass diff --git a/pythonx/UltiSnips/buffer_proxy.py b/pythonx/UltiSnips/buffer_proxy.py new file mode 100644 index 0000000..6c5bb4a --- /dev/null +++ b/pythonx/UltiSnips/buffer_proxy.py @@ -0,0 +1,224 @@ +# coding=utf8 + +import vim +import UltiSnips._vim +from UltiSnips.compatibility import as_unicode, as_vimencoding +from UltiSnips.position import Position +from UltiSnips._diff import diff +from UltiSnips import _vim + +from contextlib import contextmanager + + +@contextmanager +def use_proxy_buffer(snippets_stack, vstate): + """ + Forward all changes made in the buffer to the current snippet stack while + function call. + """ + buffer_proxy = VimBufferProxy(snippets_stack, vstate) + old_buffer = _vim.buf + try: + _vim.buf = buffer_proxy + yield + finally: + _vim.buf = old_buffer + buffer_proxy.validate_buffer() + + +@contextmanager +def suspend_proxy_edits(): + """ + Prevents changes being applied to the snippet stack while function call. + """ + if not isinstance(_vim.buf, VimBufferProxy): + yield + else: + try: + _vim.buf._disable_edits() + yield + finally: + _vim.buf._enable_edits() + + +class VimBufferProxy(_vim.VimBuffer): + """ + Proxy object used for tracking changes that made from snippet actions. + + Unfortunately, vim by itself lacks of the API for changing text in + trackable maner. + + Vim marks offers limited functionality for tracking line additions and + deletions, but nothing offered for tracking changes withing single line. + + Instance of this class is passed to all snippet actions and behaves as + internal vim.current.window.buffer. + + All changes that are made by user passed to diff algorithm, and resulting + diff applied to internal snippet structures to ensure they are in sync with + actual buffer contents. + """ + + def __init__(self, snippets_stack, vstate): + """ + Instantiate new object. + + snippets_stack is a slice of currently active snippets. + """ + self._snippets_stack = snippets_stack + self._buffer = vim.current.buffer + self._change_tick = int(vim.eval("b:changedtick")) + self._forward_edits = True + self._vstate = vstate + + def is_buffer_changed_outside(self): + """ + Returns true, if buffer was changed without using proxy object, like + with vim.command() or through internal vim.current.window.buffer. + """ + return self._change_tick < int(vim.eval("b:changedtick")) + + def validate_buffer(self): + """ + Raises exception if buffer is changes beyound proxy object. + """ + if self.is_buffer_changed_outside(): + raise RuntimeError('buffer was modified using vim.command or ' + + 'vim.current.buffer; that changes are untrackable and leads to ' + + 'errors in snippet expansion; use special variable `snip.buffer` ' + 'for buffer modifications.\n\n' + + 'See :help UltiSnips-buffer-proxy for more info.') + + def __setitem__(self, key, value): + """ + Behaves as vim.current.window.buffer.__setitem__ except it tracks + changes and applies them to the current snippet stack. + """ + if isinstance(key, slice): + value = [as_vimencoding(line) for line in value] + changes = list(self._get_diff(key.start, key.stop, value)) + self._buffer[key.start:key.stop] = [ + line.strip('\n') for line in value + ] + else: + value = as_vimencoding(value) + changes = list(self._get_line_diff(key, self._buffer[key], value)) + self._buffer[key] = value + + self._change_tick += 1 + + if self._forward_edits: + for change in changes: + self._apply_change(change) + if self._snippets_stack: + self._vstate.remember_buffer(self._snippets_stack[0]) + + def __setslice__(self, i, j, text): + """ + Same as __setitem__. + """ + self.__setitem__(slice(i, j), text) + + def __getitem__(self, key): + """ + Just passing call to the vim.current.window.buffer.__getitem__. + """ + if isinstance(key, slice): + return [as_unicode(l) for l in self._buffer[key.start:key.stop]] + else: + return as_unicode(self._buffer[key]) + + def __getslice__(self, i, j): + """ + Same as __getitem__. + """ + return self.__getitem__(slice(i, j)) + + def __len__(self): + """ + Same as len(vim.current.window.buffer). + """ + return len(self._buffer) + + def append(self, line, line_number=-1): + """ + Same as vim.current.window.buffer.append(), but with tracking changes. + """ + if line_number < 0: + line_number = len(self) + if not isinstance(line, list): + line = [line] + self[line_number:line_number] = [as_vimencoding(l) for l in line] + + def __delitem__(self, key): + if isinstance(key, slice): + self.__setitem__(key, []) + else: + self.__setitem__(slice(key, key+1), []) + + def _get_diff(self, start, end, new_value): + """ + Very fast diffing algorithm when changes are across many lines. + """ + for line_number in range(start, end): + if line_number < 0: + line_number = len(self._buffer) + line_number + yield ('D', line_number, 0, self._buffer[line_number]) + + if start < 0: + start = len(self._buffer) + start + for line_number in range(0, len(new_value)): + yield ('I', start+line_number, 0, new_value[line_number]) + + def _get_line_diff(self, line_number, before, after): + """ + Use precise diffing for tracking changes in single line. + """ + if before == '': + for change in self._get_diff(line_number, line_number+1, [after]): + yield change + else: + for change in diff(before, after): + yield (change[0], line_number, change[2], change[3]) + + def _apply_change(self, change): + """ + Apply changeset to current snippets stack, correctly moving around + snippet itself or its child. + """ + if not self._snippets_stack: + return + + line_number = change[1] + column_number = change[2] + line_before = line_number <= self._snippets_stack[0]._start.line + column_before = column_number <= self._snippets_stack[0]._start.col + if line_before and column_before: + direction = 1 + if change[0] == 'D': + direction = -1 + + self._snippets_stack[0]._move( + Position(line_number, 0), + Position(direction, 0) + ) + else: + if line_number > self._snippets_stack[0]._end.line: + return + if column_number >= self._snippets_stack[0]._end.col: + return + self._snippets_stack[0]._do_edit(change) + + def _disable_edits(self): + """ + Temporary disable applying changes to snippets stack. Should be done + while expanding anonymous snippet in the middle of jump to prevent + double tracking. + """ + self._forward_edits = False + + def _enable_edits(self): + """ + Enables changes forwarding back. + """ + self._forward_edits = True diff --git a/pythonx/UltiSnips/compatibility.py b/pythonx/UltiSnips/compatibility.py new file mode 100644 index 0000000..b8781bb --- /dev/null +++ b/pythonx/UltiSnips/compatibility.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""This file contains compatibility code to stay compatible with as many python +versions as possible.""" + +import sys + +import vim # pylint:disable=import-error + +if sys.version_info >= (3, 0): + def _vim_dec(string): + """Decode 'string' using &encoding.""" + # We don't have the luxury here of failing, everything + # falls apart if we don't return a bytearray from the + # passed in string + return string.decode(vim.eval('&encoding'), 'replace') + + def _vim_enc(bytearray): + """Encode 'string' using &encoding.""" + # We don't have the luxury here of failing, everything + # falls apart if we don't return a string from the passed + # in bytearray + return bytearray.encode(vim.eval('&encoding'), 'replace') + + def open_ascii_file(filename, mode): + """Opens a file in "r" mode.""" + return open(filename, mode, encoding='utf-8') + + def col2byte(line, col): + """Convert a valid column index into a byte index inside of vims + buffer.""" + # We pad the line so that selecting the +1 st column still works. + pre_chars = (vim.current.buffer[line - 1] + ' ')[:col] + return len(_vim_enc(pre_chars)) + + def byte2col(line, nbyte): + """Convert a column into a byteidx suitable for a mark or cursor + position inside of vim.""" + line = vim.current.buffer[line - 1] + raw_bytes = _vim_enc(line)[:nbyte] + return len(_vim_dec(raw_bytes)) + + def as_unicode(string): + """Return 'string' as unicode instance.""" + if isinstance(string, bytes): + return _vim_dec(string) + return str(string) + + def as_vimencoding(string): + """Return 'string' as Vim internal encoding.""" + return string +else: + import warnings + warnings.filterwarnings('ignore', category=DeprecationWarning) + + def _vim_dec(string): + """Decode 'string' using &encoding.""" + try: + return string.decode(vim.eval('&encoding')) + except UnicodeDecodeError: + # At least we tried. There might be some problems down the road now + return string + + def _vim_enc(string): + """Encode 'string' using &encoding.""" + try: + return string.encode(vim.eval('&encoding')) + except UnicodeDecodeError: + return string + except UnicodeEncodeError: + return string + + def open_ascii_file(filename, mode): + """Opens a file in "r" mode.""" + return open(filename, mode) + + def col2byte(line, col): + """Convert a valid column index into a byte index inside of vims + buffer.""" + # We pad the line so that selecting the +1 st column still works. + pre_chars = _vim_dec(vim.current.buffer[line - 1] + ' ')[:col] + return len(_vim_enc(pre_chars)) + + def byte2col(line, nbyte): + """Convert a column into a byteidx suitable for a mark or cursor + position inside of vim.""" + line = vim.current.buffer[line - 1] + if nbyte >= len(line): # This is beyond end of line + return nbyte + return len(_vim_dec(line[:nbyte])) + + def as_unicode(string): + """Return 'string' as unicode instance.""" + if isinstance(string, str): + return _vim_dec(string) + return unicode(string) + + def as_vimencoding(string): + """Return 'string' as unicode instance.""" + return _vim_enc(string) diff --git a/pythonx/UltiSnips/debug.py b/pythonx/UltiSnips/debug.py new file mode 100644 index 0000000..e37f54e --- /dev/null +++ b/pythonx/UltiSnips/debug.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Convenience methods that help with debugging. + +They should never be used in production code. + +""" + +import sys + +from UltiSnips.compatibility import as_unicode + +DUMP_FILENAME = '/tmp/file.txt' if not sys.platform.lower().startswith('win') \ + else 'C:/windows/temp/ultisnips.txt' +with open(DUMP_FILENAME, 'w'): + pass # clears the file + + +def echo_to_hierarchy(text_object): + """Outputs the given 'text_object' and its children hierarchically.""" + # pylint:disable=protected-access + parent = text_object + while parent._parent: + parent = parent._parent + + def _do_print(text_object, indent=''): + """prints recursively.""" + debug(indent + as_unicode(text_object)) + try: + for child in text_object._children: + _do_print(child, indent=indent + ' ') + except AttributeError: + pass + _do_print(parent) + + +def debug(msg): + """Dumb 'msg' into the debug file.""" + msg = as_unicode(msg) + with open(DUMP_FILENAME, 'ab') as dump_file: + dump_file.write((msg + '\n').encode('utf-8')) + + +def print_stack(): + """Dump a stack trace into the debug file.""" + import traceback + with open(DUMP_FILENAME, 'ab') as dump_file: + traceback.print_stack(file=dump_file) diff --git a/pythonx/UltiSnips/indent_util.py b/pythonx/UltiSnips/indent_util.py new file mode 100644 index 0000000..55525d6 --- /dev/null +++ b/pythonx/UltiSnips/indent_util.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""See module doc.""" + +from UltiSnips import _vim + + +class IndentUtil(object): + + """Utility class for dealing properly with indentation.""" + + def __init__(self): + self.reset() + + def reset(self): + """Gets the spacing properties from Vim.""" + self.shiftwidth = int( + _vim.eval("exists('*shiftwidth') ? shiftwidth() : &shiftwidth")) + self._expandtab = (_vim.eval('&expandtab') == '1') + self._tabstop = int(_vim.eval('&tabstop')) + + def ntabs_to_proper_indent(self, ntabs): + """Convert 'ntabs' number of tabs to the proper indent prefix.""" + line_ind = ntabs * self.shiftwidth * ' ' + line_ind = self.indent_to_spaces(line_ind) + line_ind = self.spaces_to_indent(line_ind) + return line_ind + + def indent_to_spaces(self, indent): + """Converts indentation to spaces respecting Vim settings.""" + indent = indent.expandtabs(self._tabstop) + right = (len(indent) - len(indent.rstrip(' '))) * ' ' + indent = indent.replace(' ', '') + indent = indent.replace('\t', ' ' * self._tabstop) + return indent + right + + def spaces_to_indent(self, indent): + """Converts spaces to proper indentation respecting Vim settings.""" + if not self._expandtab: + indent = indent.replace(' ' * self._tabstop, '\t') + return indent diff --git a/pythonx/UltiSnips/position.py b/pythonx/UltiSnips/position.py new file mode 100644 index 0000000..3c9d2a5 --- /dev/null +++ b/pythonx/UltiSnips/position.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Represents a Position in a text file: (0 based line index, 0 based column +index) and provides methods for moving them around.""" + + +class Position(object): + + """See module docstring.""" + + def __init__(self, line, col): + self.line = line + self.col = col + + def move(self, pivot, delta): + """'pivot' is the position of the first changed character, 'delta' is + how text after it moved.""" + if self < pivot: + return + if delta.line == 0: + if self.line == pivot.line: + self.col += delta.col + elif delta.line > 0: + if self.line == pivot.line: + self.col += delta.col - pivot.col + self.line += delta.line + else: + self.line += delta.line + if self.line == pivot.line: + self.col += - delta.col + pivot.col + + def delta(self, pos): + """Returns the difference that the cursor must move to come from 'pos' + to us.""" + assert isinstance(pos, Position) + if self.line == pos.line: + return Position(0, self.col - pos.col) + else: + if self > pos: + return Position(self.line - pos.line, self.col) + else: + return Position(self.line - pos.line, pos.col) + return Position(self.line - pos.line, self.col - pos.col) + + def __add__(self, pos): + assert isinstance(pos, Position) + return Position(self.line + pos.line, self.col + pos.col) + + def __sub__(self, pos): + assert isinstance(pos, Position) + return Position(self.line - pos.line, self.col - pos.col) + + def __eq__(self, other): + return (self.line, self.col) == (other.line, other.col) + + def __ne__(self, other): + return (self.line, self.col) != (other.line, other.col) + + def __lt__(self, other): + return (self.line, self.col) < (other.line, other.col) + + def __le__(self, other): + return (self.line, self.col) <= (other.line, other.col) + + def __repr__(self): + return '(%i,%i)' % (self.line, self.col) + + def __getitem__(self, index): + if index > 1: + raise IndexError( + 'position can be indexed only 0 (line) and 1 (column)' + ) + if index == 0: + return self.line + else: + return self.col diff --git a/pythonx/UltiSnips/snippet/__init__.py b/pythonx/UltiSnips/snippet/__init__.py new file mode 100644 index 0000000..0128940 --- /dev/null +++ b/pythonx/UltiSnips/snippet/__init__.py @@ -0,0 +1 @@ +"""Code related to snippets.""" diff --git a/pythonx/UltiSnips/snippet/definition/__init__.py b/pythonx/UltiSnips/snippet/definition/__init__.py new file mode 100644 index 0000000..fb08801 --- /dev/null +++ b/pythonx/UltiSnips/snippet/definition/__init__.py @@ -0,0 +1,4 @@ +"""In memory representation of snippet definitions.""" + +from UltiSnips.snippet.definition.ultisnips import UltiSnipsSnippetDefinition +from UltiSnips.snippet.definition.snipmate import SnipMateSnippetDefinition diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py new file mode 100644 index 0000000..77c1f63 --- /dev/null +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Snippet representation after parsing.""" + +import re + +import vim +import textwrap + +from UltiSnips import _vim +from UltiSnips.compatibility import as_unicode +from UltiSnips.indent_util import IndentUtil +from UltiSnips.position import Position +from UltiSnips.text import escape +from UltiSnips.text_objects import SnippetInstance +from UltiSnips.text_objects._python_code import SnippetUtilCursor, SnippetUtilForAction + +__WHITESPACE_SPLIT = re.compile(r"\s") +def split_at_whitespace(string): + """Like string.split(), but keeps empty words as empty words.""" + return re.split(__WHITESPACE_SPLIT, string) + +def _words_for_line(trigger, before, num_words=None): + """Gets the final 'num_words' words from 'before'. + + If num_words is None, then use the number of words in 'trigger'. + + """ + if num_words is None: + num_words = len(split_at_whitespace(trigger)) + + word_list = split_at_whitespace(before) + if len(word_list) <= num_words: + return before.strip() + else: + before_words = before + for i in range(-1, -(num_words + 1), -1): + left = before_words.rfind(word_list[i]) + before_words = before_words[:left] + return before[len(before_words):].strip() + + +class SnippetDefinition(object): + + """Represents a snippet as parsed from a file.""" + + _INDENT = re.compile(r"^[ \t]*") + _TABS = re.compile(r"^\t*") + + def __init__(self, priority, trigger, value, description, + options, globals, location, context, actions): + self._priority = int(priority) + self._trigger = as_unicode(trigger) + self._value = as_unicode(value) + self._description = as_unicode(description) + self._opts = options + self._matched = '' + self._last_re = None + self._globals = globals + self._location = location + self._context_code = context + self._context = None + self._actions = actions + + # Make sure that we actually match our trigger in case we are + # immediately expanded. + self.matches(self._trigger) + + def __repr__(self): + return '_SnippetDefinition(%r,%s,%s,%s)' % ( + self._priority, self._trigger, self._description, self._opts) + + def _re_match(self, trigger): + """Test if a the current regex trigger matches `trigger`. + + If so, set _last_re and _matched. + + """ + for match in re.finditer(self._trigger, trigger): + if match.end() != len(trigger): + continue + else: + self._matched = trigger[match.start():match.end()] + + self._last_re = match + return match + return False + + def _context_match(self): + # skip on empty buffer + if len(vim.current.buffer) == 1 and vim.current.buffer[0] == "": + return + + return self._eval_code('snip.context = ' + self._context_code, { + 'context': None + }).context + + def _eval_code(self, code, additional_locals={}): + code = "\n".join([ + 'import re, os, vim, string, random', + '\n'.join(self._globals.get('!p', [])).replace('\r\n', '\n'), + code + ]) + + current = vim.current + + locals = { + 'window': current.window, + 'buffer': current.buffer, + 'line': current.window.cursor[0]-1, + 'column': current.window.cursor[1]-1, + 'cursor': SnippetUtilCursor(current.window.cursor) + } + + locals.update(additional_locals) + + snip = SnippetUtilForAction(locals) + + try: + exec(code, {'snip': snip}) + except Exception as e: + e.snippet_info = textwrap.dedent(""" + Defined in: {} + Trigger: {} + Description: {} + Context: {} + Pre-expand: {} + Post-expand: {} + """).format( + self._location, + self._trigger, + self._description, + self._context_code if self._context_code else '', + self._actions['pre_expand'] if 'pre_expand' in self._actions + else '', + self._actions['post_expand'] if 'post_expand' in self._actions + else '', + code, + ) + + e.snippet_code = code + + raise + + return snip + + def _execute_action( + self, + action, + context, + additional_locals={} + ): + mark_to_use = '`' + with _vim.save_mark(mark_to_use): + _vim.set_mark_from_pos(mark_to_use, _vim.get_cursor_pos()) + + cursor_line_before = _vim.buf.line_till_cursor + + locals = { + 'context': context, + } + + locals.update(additional_locals) + + snip = self._eval_code(action, locals) + + if snip.cursor.is_set(): + vim.current.window.cursor = snip.cursor.to_vim_cursor() + else: + new_mark_pos = _vim.get_mark_pos(mark_to_use) + + cursor_invalid = False + + if _vim._is_pos_zero(new_mark_pos): + cursor_invalid = True + else: + _vim.set_cursor_from_pos(new_mark_pos) + if cursor_line_before != _vim.buf.line_till_cursor: + cursor_invalid = True + + if cursor_invalid: + raise RuntimeError( + 'line under the cursor was modified, but ' + + '"snip.cursor" variable is not set; either set set ' + + '"snip.cursor" to new cursor position, or do not ' + + 'modify cursor line' + ) + + return snip + + def has_option(self, opt): + """Check if the named option is set.""" + return opt in self._opts + + @property + def description(self): + """Descriptive text for this snippet.""" + return ('(%s) %s' % (self._trigger, self._description)).strip() + + @property + def priority(self): + """The snippets priority, which defines which snippet will be preferred + over others with the same trigger.""" + return self._priority + + @property + def trigger(self): + """The trigger text for the snippet.""" + return self._trigger + + @property + def matched(self): + """The last text that matched this snippet in match() or + could_match().""" + return self._matched + + @property + def location(self): + """Where this snippet was defined.""" + return self._location + + @property + def context(self): + """The matched context.""" + return self._context + + def matches(self, before): + """Returns True if this snippet matches 'before'.""" + # If user supplies both "w" and "i", it should perhaps be an + # error, but if permitted it seems that "w" should take precedence + # (since matching at word boundary and within a word == matching at word + # boundary). + self._matched = '' + + words = _words_for_line(self._trigger, before) + + if 'r' in self._opts: + match = self._re_match(before) + elif 'w' in self._opts: + words_len = len(self._trigger) + words_prefix = words[:-words_len] + words_suffix = words[-words_len:] + match = (words_suffix == self._trigger) + if match and words_prefix: + # Require a word boundary between prefix and suffix. + boundary_chars = escape(words_prefix[-1:] + + words_suffix[:1], r'\"') + match = _vim.eval( + '"%s" =~# "\\\\v.<."' % + boundary_chars) != '0' + elif 'i' in self._opts: + match = words.endswith(self._trigger) + else: + match = (words == self._trigger) + + # By default, we match the whole trigger + if match and not self._matched: + self._matched = self._trigger + + # Ensure the match was on a word boundry if needed + if 'b' in self._opts and match: + text_before = before.rstrip()[:-len(self._matched)] + if text_before.strip(' \t') != '': + self._matched = '' + return False + + self._context = None + if match and self._context_code: + self._context = self._context_match() + if not self.context: + match = False + + return match + + def could_match(self, before): + """Return True if this snippet could match the (partial) 'before'.""" + self._matched = '' + + # List all on whitespace. + if before and before[-1] in (' ', '\t'): + before = '' + if before and before.rstrip() is not before: + return False + + words = _words_for_line(self._trigger, before) + + if 'r' in self._opts: + # Test for full match only + match = self._re_match(before) + elif 'w' in self._opts: + # Trim non-empty prefix up to word boundary, if present. + qwords = escape(words, r'\"') + words_suffix = _vim.eval( + 'substitute("%s", "\\\\v^.+<(.+)", "\\\\1", "")' % qwords) + match = self._trigger.startswith(words_suffix) + self._matched = words_suffix + + # TODO: list_snippets() function cannot handle partial-trigger + # matches yet, so for now fail if we trimmed the prefix. + if words_suffix != words: + match = False + elif 'i' in self._opts: + # TODO: It is hard to define when a inword snippet could match, + # therefore we check only for full-word trigger. + match = self._trigger.startswith(words) + else: + match = self._trigger.startswith(words) + + # By default, we match the words from the trigger + if match and not self._matched: + self._matched = words + + # Ensure the match was on a word boundry if needed + if 'b' in self._opts and match: + text_before = before.rstrip()[:-len(self._matched)] + if text_before.strip(' \t') != '': + self._matched = '' + return False + + return match + + def instantiate(self, snippet_instance, initial_text, indent): + """Parses the content of this snippet and brings the corresponding text + objects alive inside of Vim.""" + raise NotImplementedError() + + def do_pre_expand(self, visual_content, snippets_stack): + if 'pre_expand' in self._actions: + locals = {'buffer': _vim.buf, 'visual_content': visual_content} + + snip = self._execute_action( + self._actions['pre_expand'], self._context, locals + ) + + self._context = snip.context + + return snip.cursor.is_set() + else: + return False + + def do_post_expand(self, start, end, snippets_stack): + if 'post_expand' in self._actions: + locals = { + 'snippet_start': start, + 'snippet_end': end, + 'buffer': _vim.buf + } + + snip = self._execute_action( + self._actions['post_expand'], snippets_stack[-1].context, locals + ) + + snippets_stack[-1].context = snip.context + + return snip.cursor.is_set() + else: + return False + + def do_post_jump( + self, tabstop_number, jump_direction, snippets_stack, current_snippet + ): + if 'post_jump' in self._actions: + start = current_snippet.start + end = current_snippet.end + + locals = { + 'tabstop': tabstop_number, + 'jump_direction': jump_direction, + 'tabstops': current_snippet.get_tabstops(), + 'snippet_start': start, + 'snippet_end': end, + 'buffer': _vim.buf + } + + snip = self._execute_action( + self._actions['post_jump'], current_snippet.context, locals + ) + + current_snippet.context = snip.context + + return snip.cursor.is_set() + else: + return False + + + def launch(self, text_before, visual_content, parent, start, end): + """Launch this snippet, overwriting the text 'start' to 'end' and + keeping the 'text_before' on the launch line. + + 'Parent' is the parent snippet instance if any. + + """ + indent = self._INDENT.match(text_before).group(0) + lines = (self._value + '\n').splitlines() + ind_util = IndentUtil() + + # Replace leading tabs in the snippet definition via proper indenting + initial_text = [] + for line_num, line in enumerate(lines): + if 't' in self._opts: + tabs = 0 + else: + tabs = len(self._TABS.match(line).group(0)) + line_ind = ind_util.ntabs_to_proper_indent(tabs) + if line_num != 0: + line_ind = indent + line_ind + + result_line = line_ind + line[tabs:] + if 'm' in self._opts: + result_line = result_line.rstrip() + initial_text.append(result_line) + initial_text = '\n'.join(initial_text) + + snippet_instance = SnippetInstance( + self, parent, initial_text, start, end, visual_content, + last_re=self._last_re, globals=self._globals, + context=self._context) + self.instantiate(snippet_instance, initial_text, indent) + + snippet_instance.update_textobjects() + return snippet_instance diff --git a/pythonx/UltiSnips/snippet/definition/snipmate.py b/pythonx/UltiSnips/snippet/definition/snipmate.py new file mode 100644 index 0000000..8ab1e2a --- /dev/null +++ b/pythonx/UltiSnips/snippet/definition/snipmate.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""A snipMate snippet after parsing.""" + +from UltiSnips.snippet.definition._base import SnippetDefinition +from UltiSnips.snippet.parsing.snipmate import parse_and_instantiate + + +class SnipMateSnippetDefinition(SnippetDefinition): + + """See module doc.""" + + SNIPMATE_SNIPPET_PRIORITY = -1000 + + def __init__(self, trigger, value, description, location): + SnippetDefinition.__init__(self, self.SNIPMATE_SNIPPET_PRIORITY, + trigger, value, description, '', {}, location, + None, {}) + + def instantiate(self, snippet_instance, initial_text, indent): + parse_and_instantiate(snippet_instance, initial_text, indent) diff --git a/pythonx/UltiSnips/snippet/definition/ultisnips.py b/pythonx/UltiSnips/snippet/definition/ultisnips.py new file mode 100644 index 0000000..5338a6a --- /dev/null +++ b/pythonx/UltiSnips/snippet/definition/ultisnips.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""A UltiSnips snippet after parsing.""" + +from UltiSnips.snippet.definition._base import SnippetDefinition +from UltiSnips.snippet.parsing.ultisnips import parse_and_instantiate + + +class UltiSnipsSnippetDefinition(SnippetDefinition): + + """See module doc.""" + + def instantiate(self, snippet_instance, initial_text, indent): + return parse_and_instantiate(snippet_instance, initial_text, indent) diff --git a/pythonx/UltiSnips/snippet/parsing/__init__.py b/pythonx/UltiSnips/snippet/parsing/__init__.py new file mode 100644 index 0000000..ea46beb --- /dev/null +++ b/pythonx/UltiSnips/snippet/parsing/__init__.py @@ -0,0 +1 @@ +"""Code related to turning text into snippets.""" diff --git a/pythonx/UltiSnips/snippet/parsing/_base.py b/pythonx/UltiSnips/snippet/parsing/_base.py new file mode 100644 index 0000000..8c7f342 --- /dev/null +++ b/pythonx/UltiSnips/snippet/parsing/_base.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Common functionality of the snippet parsing codes.""" + +from UltiSnips.position import Position +from UltiSnips.snippet.parsing._lexer import tokenize, TabStopToken +from UltiSnips.text_objects import TabStop + +from UltiSnips.text_objects import Mirror +from UltiSnips.snippet.parsing._lexer import MirrorToken + + +def resolve_ambiguity(all_tokens, seen_ts): + """$1 could be a Mirror or a TabStop. + + This figures this out. + + """ + for parent, token in all_tokens: + if isinstance(token, MirrorToken): + if token.number not in seen_ts: + seen_ts[token.number] = TabStop(parent, token) + else: + Mirror(parent, seen_ts[token.number], token) + + +def tokenize_snippet_text(snippet_instance, text, indent, + allowed_tokens_in_text, allowed_tokens_in_tabstops, + token_to_textobject): + """Turns 'text' into a stream of tokens and creates the text objects from + those tokens that are mentioned in 'token_to_textobject' assuming the + current 'indent'. + + The 'allowed_tokens_in_text' define which tokens will be recognized + in 'text' while 'allowed_tokens_in_tabstops' are the tokens that + will be recognized in TabStop placeholder text. + + """ + seen_ts = {} + all_tokens = [] + + def _do_parse(parent, text, allowed_tokens): + """Recursive function that actually creates the objects.""" + tokens = list(tokenize(text, indent, parent.start, allowed_tokens)) + for token in tokens: + all_tokens.append((parent, token)) + if isinstance(token, TabStopToken): + ts = TabStop(parent, token) + seen_ts[token.number] = ts + _do_parse(ts, token.initial_text, + allowed_tokens_in_tabstops) + else: + klass = token_to_textobject.get(token.__class__, None) + if klass is not None: + klass(parent, token) + _do_parse(snippet_instance, text, allowed_tokens_in_text) + return all_tokens, seen_ts + + +def finalize(all_tokens, seen_ts, snippet_instance): + """Adds a tabstop 0 if non is in 'seen_ts' and brings the text of the + snippet instance into Vim.""" + if 0 not in seen_ts: + mark = all_tokens[-1][1].end # Last token is always EndOfText + m1 = Position(mark.line, mark.col) + TabStop(snippet_instance, 0, mark, m1) + snippet_instance.replace_initial_text() diff --git a/pythonx/UltiSnips/snippet/parsing/_lexer.py b/pythonx/UltiSnips/snippet/parsing/_lexer.py new file mode 100644 index 0000000..60859fe --- /dev/null +++ b/pythonx/UltiSnips/snippet/parsing/_lexer.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Not really a lexer in the classical sense, but code to convert snippet +definitions into logical units called Tokens.""" + +import string +import re + +from UltiSnips.compatibility import as_unicode +from UltiSnips.position import Position +from UltiSnips.text import unescape + + +class _TextIterator(object): + + """Helper class to make iterating over text easier.""" + + def __init__(self, text, offset): + self._text = as_unicode(text) + self._line = offset.line + self._col = offset.col + + self._idx = 0 + + def __iter__(self): + """Iterator interface.""" + return self + + def __next__(self): + """Returns the next character.""" + if self._idx >= len(self._text): + raise StopIteration + + rv = self._text[self._idx] + if self._text[self._idx] in ('\n', '\r\n'): + self._line += 1 + self._col = 0 + else: + self._col += 1 + self._idx += 1 + return rv + next = __next__ # for python2 + + def peek(self, count=1): + """Returns the next 'count' characters without advancing the stream.""" + if count > 1: # This might return '' if nothing is found + return self._text[self._idx:self._idx + count] + try: + return self._text[self._idx] + except IndexError: + return None + + @property + def pos(self): + """Current position in the text.""" + return Position(self._line, self._col) + + +def _parse_number(stream): + """Expects the stream to contain a number next, returns the number without + consuming any more bytes.""" + rv = '' + while stream.peek() and stream.peek() in string.digits: + rv += next(stream) + + return int(rv) + + +def _parse_till_closing_brace(stream): + """ + Returns all chars till a non-escaped } is found. Other + non escaped { are taken into account and skipped over. + + Will also consume the closing }, but not return it + """ + rv = '' + in_braces = 1 + while True: + if EscapeCharToken.starts_here(stream, '{}'): + rv += next(stream) + next(stream) + else: + char = next(stream) + if char == '{': + in_braces += 1 + elif char == '}': + in_braces -= 1 + if in_braces == 0: + break + rv += char + return rv + + +def _parse_till_unescaped_char(stream, chars): + """ + Returns all chars till a non-escaped char is found. + + Will also consume the closing char, but and return it as second + return value + """ + rv = '' + while True: + escaped = False + for char in chars: + if EscapeCharToken.starts_here(stream, char): + rv += next(stream) + next(stream) + escaped = True + if not escaped: + char = next(stream) + if char in chars: + break + rv += char + return rv, char + + +class Token(object): + + """Represents a Token as parsed from a snippet definition.""" + + def __init__(self, gen, indent): + self.initial_text = as_unicode('') + self.start = gen.pos + self._parse(gen, indent) + self.end = gen.pos + + def _parse(self, stream, indent): + """Parses the token from 'stream' with the current 'indent'.""" + pass # Does nothing + + +class TabStopToken(Token): + + """${1:blub}""" + CHECK = re.compile(r'^\${\d+[:}]') + + @classmethod + def starts_here(cls, stream): + """Returns true if this token starts at the current position in + 'stream'.""" + return cls.CHECK.match(stream.peek(10)) is not None + + def _parse(self, stream, indent): + next(stream) # $ + next(stream) # { + + self.number = _parse_number(stream) + + if stream.peek() == ':': + next(stream) + self.initial_text = _parse_till_closing_brace(stream) + + def __repr__(self): + return 'TabStopToken(%r,%r,%r,%r)' % ( + self.start, self.end, self.number, self.initial_text + ) + + +class VisualToken(Token): + + """${VISUAL}""" + CHECK = re.compile(r"^\${VISUAL[:}/]") + + @classmethod + def starts_here(cls, stream): + """Returns true if this token starts at the current position in + 'stream'.""" + return cls.CHECK.match(stream.peek(10)) is not None + + def _parse(self, stream, indent): + for _ in range(8): # ${VISUAL + next(stream) + + if stream.peek() == ':': + next(stream) + self.alternative_text, char = _parse_till_unescaped_char(stream, '/}') + self.alternative_text = unescape(self.alternative_text) + + if char == '/': # Transformation going on + try: + self.search = _parse_till_unescaped_char(stream, '/')[0] + self.replace = _parse_till_unescaped_char(stream, '/')[0] + self.options = _parse_till_closing_brace(stream) + except StopIteration: + raise RuntimeError( + "Invalid ${VISUAL} transformation! Forgot to escape a '/'?") + else: + self.search = None + self.replace = None + self.options = None + + def __repr__(self): + return 'VisualToken(%r,%r)' % ( + self.start, self.end + ) + + +class TransformationToken(Token): + + """${1/match/replace/options}""" + + CHECK = re.compile(r'^\${\d+\/') + + @classmethod + def starts_here(cls, stream): + """Returns true if this token starts at the current position in + 'stream'.""" + return cls.CHECK.match(stream.peek(10)) is not None + + def _parse(self, stream, indent): + next(stream) # $ + next(stream) # { + + self.number = _parse_number(stream) + + next(stream) # / + + self.search = _parse_till_unescaped_char(stream, '/')[0] + self.replace = _parse_till_unescaped_char(stream, '/')[0] + self.options = _parse_till_closing_brace(stream) + + def __repr__(self): + return 'TransformationToken(%r,%r,%r,%r,%r)' % ( + self.start, self.end, self.number, self.search, self.replace + ) + + +class MirrorToken(Token): + + """$1.""" + CHECK = re.compile(r'^\$\d+') + + @classmethod + def starts_here(cls, stream): + """Returns true if this token starts at the current position in + 'stream'.""" + return cls.CHECK.match(stream.peek(10)) is not None + + def _parse(self, stream, indent): + next(stream) # $ + self.number = _parse_number(stream) + + def __repr__(self): + return 'MirrorToken(%r,%r,%r)' % ( + self.start, self.end, self.number + ) + + +class EscapeCharToken(Token): + + """\\n.""" + @classmethod + def starts_here(cls, stream, chars=r'{}\$`'): + """Returns true if this token starts at the current position in + 'stream'.""" + cs = stream.peek(2) + if len(cs) == 2 and cs[0] == '\\' and cs[1] in chars: + return True + + def _parse(self, stream, indent): + next(stream) # \ + self.initial_text = next(stream) + + def __repr__(self): + return 'EscapeCharToken(%r,%r,%r)' % ( + self.start, self.end, self.initial_text + ) + + +class ShellCodeToken(Token): + + """`echo "hi"`""" + @classmethod + def starts_here(cls, stream): + """Returns true if this token starts at the current position in + 'stream'.""" + return stream.peek(1) == '`' + + def _parse(self, stream, indent): + next(stream) # ` + self.code = _parse_till_unescaped_char(stream, '`')[0] + + def __repr__(self): + return 'ShellCodeToken(%r,%r,%r)' % ( + self.start, self.end, self.code + ) + + +class PythonCodeToken(Token): + + """`!p snip.rv = "Hi"`""" + CHECK = re.compile(r'^`!p\s') + + @classmethod + def starts_here(cls, stream): + """Returns true if this token starts at the current position in + 'stream'.""" + return cls.CHECK.match(stream.peek(4)) is not None + + def _parse(self, stream, indent): + for _ in range(3): + next(stream) # `!p + if stream.peek() in '\t ': + next(stream) + + code = _parse_till_unescaped_char(stream, '`')[0] + + # Strip the indent if any + if len(indent): + lines = code.splitlines() + self.code = lines[0] + '\n' + self.code += '\n'.join([l[len(indent):] + for l in lines[1:]]) + else: + self.code = code + self.indent = indent + + def __repr__(self): + return 'PythonCodeToken(%r,%r,%r)' % ( + self.start, self.end, self.code + ) + + +class VimLCodeToken(Token): + + """`!v g:hi`""" + CHECK = re.compile(r'^`!v\s') + + @classmethod + def starts_here(cls, stream): + """Returns true if this token starts at the current position in + 'stream'.""" + return cls.CHECK.match(stream.peek(4)) is not None + + def _parse(self, stream, indent): + for _ in range(4): + next(stream) # `!v + self.code = _parse_till_unescaped_char(stream, '`')[0] + + def __repr__(self): + return 'VimLCodeToken(%r,%r,%r)' % ( + self.start, self.end, self.code + ) + + +class EndOfTextToken(Token): + + """Appears at the end of the text.""" + + def __repr__(self): + return 'EndOfText(%r)' % self.end + + +def tokenize(text, indent, offset, allowed_tokens): + """Returns an iterator of tokens of 'text'['offset':] which is assumed to + have 'indent' as the whitespace of the begging of the lines. Only + 'allowed_tokens' are considered to be valid tokens.""" + stream = _TextIterator(text, offset) + try: + while True: + done_something = False + for token in allowed_tokens: + if token.starts_here(stream): + yield token(stream, indent) + done_something = True + break + if not done_something: + next(stream) + except StopIteration: + yield EndOfTextToken(stream, indent) diff --git a/pythonx/UltiSnips/snippet/parsing/snipmate.py b/pythonx/UltiSnips/snippet/parsing/snipmate.py new file mode 100644 index 0000000..8aaed53 --- /dev/null +++ b/pythonx/UltiSnips/snippet/parsing/snipmate.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Parses a snipMate snippet definition and launches it into Vim.""" + +from UltiSnips.snippet.parsing._base import tokenize_snippet_text, finalize, resolve_ambiguity +from UltiSnips.snippet.parsing._lexer import EscapeCharToken, \ + VisualToken, TabStopToken, MirrorToken, ShellCodeToken +from UltiSnips.text_objects import EscapedChar, Mirror, VimLCode, Visual + +_TOKEN_TO_TEXTOBJECT = { + EscapeCharToken: EscapedChar, + VisualToken: Visual, + ShellCodeToken: VimLCode, # `` is VimL in snipMate +} + +__ALLOWED_TOKENS = [ + EscapeCharToken, VisualToken, TabStopToken, MirrorToken, ShellCodeToken +] + +__ALLOWED_TOKENS_IN_TABSTOPS = [ + EscapeCharToken, VisualToken, MirrorToken, ShellCodeToken +] + + +def parse_and_instantiate(parent_to, text, indent): + """Parses a snippet definition in snipMate format from 'text' assuming the + current 'indent'. + + Will instantiate all the objects and link them as children to + parent_to. Will also put the initial text into Vim. + + """ + all_tokens, seen_ts = tokenize_snippet_text(parent_to, text, indent, + __ALLOWED_TOKENS, __ALLOWED_TOKENS_IN_TABSTOPS, + _TOKEN_TO_TEXTOBJECT) + resolve_ambiguity(all_tokens, seen_ts) + finalize(all_tokens, seen_ts, parent_to) diff --git a/pythonx/UltiSnips/snippet/parsing/ultisnips.py b/pythonx/UltiSnips/snippet/parsing/ultisnips.py new file mode 100644 index 0000000..9bb6068 --- /dev/null +++ b/pythonx/UltiSnips/snippet/parsing/ultisnips.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Parses a UltiSnips snippet definition and launches it into Vim.""" + +from UltiSnips.snippet.parsing._base import tokenize_snippet_text, finalize, resolve_ambiguity +from UltiSnips.snippet.parsing._lexer import EscapeCharToken, \ + VisualToken, TransformationToken, TabStopToken, MirrorToken, \ + PythonCodeToken, VimLCodeToken, ShellCodeToken +from UltiSnips.text_objects import EscapedChar, Mirror, PythonCode, \ + ShellCode, TabStop, Transformation, VimLCode, Visual + +_TOKEN_TO_TEXTOBJECT = { + EscapeCharToken: EscapedChar, + VisualToken: Visual, + ShellCodeToken: ShellCode, + PythonCodeToken: PythonCode, + VimLCodeToken: VimLCode, +} + +__ALLOWED_TOKENS = [ + EscapeCharToken, VisualToken, TransformationToken, TabStopToken, + MirrorToken, PythonCodeToken, VimLCodeToken, ShellCodeToken +] + + +def _create_transformations(all_tokens, seen_ts): + """Create the objects that need to know about tabstops.""" + for parent, token in all_tokens: + if isinstance(token, TransformationToken): + if token.number not in seen_ts: + raise RuntimeError( + 'Tabstop %i is not known but is used by a Transformation' + % token.number) + Transformation(parent, seen_ts[token.number], token) + + +def parse_and_instantiate(parent_to, text, indent): + """Parses a snippet definition in UltiSnips format from 'text' assuming the + current 'indent'. + + Will instantiate all the objects and link them as children to + parent_to. Will also put the initial text into Vim. + + """ + all_tokens, seen_ts = tokenize_snippet_text(parent_to, text, indent, + __ALLOWED_TOKENS, __ALLOWED_TOKENS, _TOKEN_TO_TEXTOBJECT) + resolve_ambiguity(all_tokens, seen_ts) + _create_transformations(all_tokens, seen_ts) + finalize(all_tokens, seen_ts, parent_to) diff --git a/pythonx/UltiSnips/snippet/source/__init__.py b/pythonx/UltiSnips/snippet/source/__init__.py new file mode 100644 index 0000000..08c20ac --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Sources of snippet definitions.""" + +from UltiSnips.snippet.source._base import SnippetSource +from UltiSnips.snippet.source.added import AddedSnippetsSource +from UltiSnips.snippet.source.file.snipmate import SnipMateFileSource +from UltiSnips.snippet.source.file.ultisnips import UltiSnipsFileSource, \ + find_all_snippet_files, find_snippet_files diff --git a/pythonx/UltiSnips/snippet/source/_base.py b/pythonx/UltiSnips/snippet/source/_base.py new file mode 100644 index 0000000..aa38af3 --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/_base.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Base class for snippet sources.""" + +from collections import defaultdict + +from UltiSnips.snippet.source._snippet_dictionary import SnippetDictionary + + +class SnippetSource(object): + + """See module docstring.""" + + def __init__(self): + self._snippets = defaultdict(SnippetDictionary) + self._extends = defaultdict(set) + + def ensure(self, filetypes, cached): + """Update/reload the snippets in the source when needed. + + It makes sure that the snippets are not outdated. + + """ + + def loaded(self, filetypes): + return len(self._snippets) > 0 + + def _get_existing_deep_extends(self, base_filetypes): + """Helper for get all existing filetypes extended by base filetypes.""" + deep_extends = self.get_deep_extends(base_filetypes) + return [ft for ft in deep_extends if ft in self._snippets] + + def get_snippets(self, filetypes, before, possible, autotrigger_only): + """Returns the snippets for all 'filetypes' (in order) and their + parents matching the text 'before'. If 'possible' is true, a partial + match is enough. Base classes can override this method to provide means + of creating snippets on the fly. + + Returns a list of SnippetDefinition s. + + """ + result = [] + for ft in self._get_existing_deep_extends(filetypes): + snips = self._snippets[ft] + result.extend(snips.get_matching_snippets(before, possible, + autotrigger_only)) + return result + + def get_clear_priority(self, filetypes): + """Get maximum clearsnippets priority without arguments for specified + filetypes, if any. + + It returns None if there are no clearsnippets. + + """ + pri = None + for ft in self._get_existing_deep_extends(filetypes): + snippets = self._snippets[ft] + if pri is None or snippets._clear_priority > pri: + pri = snippets._clear_priority + return pri + + def get_cleared(self, filetypes): + """Get a set of cleared snippets marked by clearsnippets with arguments + for specified filetypes.""" + cleared = {} + for ft in self._get_existing_deep_extends(filetypes): + snippets = self._snippets[ft] + for key, value in snippets._cleared.items(): + if key not in cleared or value > cleared[key]: + cleared[key] = value + return cleared + + def update_extends(self, child_ft, parent_fts): + """Update the extending relation by given child filetype and its parent + filetypes.""" + self._extends[child_ft].update(parent_fts) + + def get_deep_extends(self, base_filetypes): + """Get a list of filetypes that is either directed or indirected + extended by given base filetypes. + + Note that the returned list include the root filetype itself. + + """ + seen = set(base_filetypes) + todo_fts = list(set(base_filetypes)) + while todo_fts: + todo_ft = todo_fts.pop() + unseen_extends = set( + ft for ft in self._extends[todo_ft] if ft not in seen) + seen.update(unseen_extends) + todo_fts.extend(unseen_extends) + return seen diff --git a/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py b/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py new file mode 100644 index 0000000..b5b9a5b --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Implements a container for parsed snippets.""" + +class SnippetDictionary(object): + + """See module docstring.""" + + def __init__(self): + self._snippets = [] + self._cleared = {} + self._clear_priority = float("-inf") + + def add_snippet(self, snippet): + """Add 'snippet' to this dictionary.""" + self._snippets.append(snippet) + + def get_matching_snippets(self, trigger, potentially, autotrigger_only): + """Returns all snippets matching the given trigger. + + If 'potentially' is true, returns all that could_match(). + + If 'autotrigger_only' is true, function will return only snippets which + are marked with flag 'A' (should be automatically expanded without + trigger key press). + It's handled specially to avoid walking down the list of all snippets, + which can be very slow, because function will be called on each change + made in insert mode. + + """ + all_snippets = self._snippets + if autotrigger_only: + all_snippets = [s for s in all_snippets if s.has_option('A')] + + if not potentially: + return [s for s in all_snippets if s.matches(trigger)] + else: + return [s for s in all_snippets if s.could_match(trigger)] + + def clear_snippets(self, priority, triggers): + """Clear the snippets by mark them as cleared. + + If trigger is None, it updates the value of clear priority + instead. + + """ + if not triggers: + if self._clear_priority is None or priority > self._clear_priority: + self._clear_priority = priority + else: + for trigger in triggers: + if (trigger not in self._cleared or + priority > self._cleared[trigger]): + self._cleared[trigger] = priority + + def __len__(self): + return len(self._snippets) diff --git a/pythonx/UltiSnips/snippet/source/added.py b/pythonx/UltiSnips/snippet/source/added.py new file mode 100644 index 0000000..c410946 --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/added.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Handles manually added snippets UltiSnips_Manager.add_snippet().""" + +from UltiSnips.snippet.source._base import SnippetSource + + +class AddedSnippetsSource(SnippetSource): + + """See module docstring.""" + + def add_snippet(self, ft, snippet): + """Adds the given 'snippet' for 'ft'.""" + self._snippets[ft].add_snippet(snippet) diff --git a/pythonx/UltiSnips/snippet/source/file/__init__.py b/pythonx/UltiSnips/snippet/source/file/__init__.py new file mode 100644 index 0000000..6abf472 --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/file/__init__.py @@ -0,0 +1 @@ +"""Snippet sources that are file based.""" diff --git a/pythonx/UltiSnips/snippet/source/file/_base.py b/pythonx/UltiSnips/snippet/source/file/_base.py new file mode 100644 index 0000000..daa3f1d --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/file/_base.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Code to provide access to UltiSnips files from disk.""" + +from collections import defaultdict +import hashlib +import os + +from UltiSnips import _vim +from UltiSnips import compatibility +from UltiSnips.snippet.source._base import SnippetSource + + +def _hash_file(path): + """Returns a hashdigest of 'path'.""" + if not os.path.isfile(path): + return False + return hashlib.sha1(open(path, 'rb').read()).hexdigest() + + +class SnippetSyntaxError(RuntimeError): + + """Thrown when a syntax error is found in a file.""" + + def __init__(self, filename, line_index, msg): + RuntimeError.__init__(self, '%s in %s:%d' % ( + msg, filename, line_index)) + + +class SnippetFileSource(SnippetSource): + + """Base class that abstracts away 'extends' info and file hashes.""" + + def __init__(self): + SnippetSource.__init__(self) + self._files_for_ft = defaultdict(set) + self._file_hashes = defaultdict(lambda: None) + self._ensure_cached = False + + def ensure(self, filetypes, cached): + if cached and self._ensure_cached: + return + + for ft in self.get_deep_extends(filetypes): + if self._needs_update(ft): + self._load_snippets_for(ft) + + self._ensure_cached = True + + def _get_all_snippet_files_for(self, ft): + """Returns a set of all files that define snippets for 'ft'.""" + raise NotImplementedError() + + def _parse_snippet_file(self, filedata, filename): + """Parses 'filedata' as a snippet file and yields events.""" + raise NotImplementedError() + + def _needs_update(self, ft): + """Returns true if any files for 'ft' have changed and must be + reloaded.""" + existing_files = self._get_all_snippet_files_for(ft) + if existing_files != self._files_for_ft[ft]: + self._files_for_ft[ft] = existing_files + return True + + for filename in self._files_for_ft[ft]: + if _hash_file(filename) != self._file_hashes[filename]: + return True + + return False + + def _load_snippets_for(self, ft): + """Load all snippets for the given 'ft'.""" + if ft in self._snippets: + del self._snippets[ft] + del self._extends[ft] + try: + for fn in self._files_for_ft[ft]: + self._parse_snippets(ft, fn) + except: + del self._files_for_ft[ft] + raise + # Now load for the parents + for parent_ft in self.get_deep_extends([ft]): + if parent_ft != ft and self._needs_update(parent_ft): + self._load_snippets_for(parent_ft) + + def _parse_snippets(self, ft, filename): + """Parse the 'filename' for the given 'ft' and watch it for changes in + the future.""" + self._file_hashes[filename] = _hash_file(filename) + file_data = compatibility.open_ascii_file(filename, 'r').read() + for event, data in self._parse_snippet_file(file_data, filename): + if event == 'error': + msg, line_index = data + filename = _vim.eval("""fnamemodify(%s, ":~:.")""" % + _vim.escape(filename)) + raise SnippetSyntaxError(filename, line_index, msg) + elif event == 'clearsnippets': + priority, triggers = data + self._snippets[ft].clear_snippets(priority, triggers) + elif event == 'extends': + # TODO(sirver): extends information is more global + # than one snippet source. + filetypes, = data + self.update_extends(ft, filetypes) + elif event == 'snippet': + snippet, = data + self._snippets[ft].add_snippet(snippet) + else: + assert False, 'Unhandled %s: %r' % (event, data) diff --git a/pythonx/UltiSnips/snippet/source/file/_common.py b/pythonx/UltiSnips/snippet/source/file/_common.py new file mode 100644 index 0000000..ec4be60 --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/file/_common.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Common code for snipMate and UltiSnips snippet files.""" + + +def handle_extends(tail, line_index): + """Handles an extends line in a snippet.""" + if tail: + return 'extends', ([p.strip() for p in tail.split(',')],) + else: + return 'error', ("'extends' without file types", line_index) + + +def handle_action(head, tail, line_index): + if tail: + action = tail.strip('"').replace(r'\"', '"').replace(r'\\\\', r'\\') + return head, (action,) + else: + return 'error', ("'{}' without specified action".format(head), + line_index) diff --git a/pythonx/UltiSnips/snippet/source/file/snipmate.py b/pythonx/UltiSnips/snippet/source/file/snipmate.py new file mode 100644 index 0000000..1791243 --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/file/snipmate.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Parses snipMate files.""" + +import os +import glob + +from UltiSnips import _vim +from UltiSnips.snippet.definition import SnipMateSnippetDefinition +from UltiSnips.snippet.source.file._base import SnippetFileSource +from UltiSnips.snippet.source.file._common import handle_extends +from UltiSnips.text import LineIterator, head_tail + + +def _splitall(path): + """Split 'path' into all its components.""" + # From http://my.safaribooksonline.com/book/programming/ + # python/0596001673/files/pythoncook-chp-4-sect-16 + allparts = [] + while True: + parts = os.path.split(path) + if parts[0] == path: # sentinel for absolute paths + allparts.insert(0, parts[0]) + break + elif parts[1] == path: # sentinel for relative paths + allparts.insert(0, parts[1]) + break + else: + path = parts[0] + allparts.insert(0, parts[1]) + return allparts + + +def snipmate_files_for(ft): + """Returns all snipMate files we need to look at for 'ft'.""" + if ft == 'all': + ft = '_' + patterns = [ + '%s.snippets' % ft, + os.path.join(ft, '*.snippets'), + os.path.join(ft, '*.snippet'), + os.path.join(ft, '*/*.snippet'), + ] + ret = set() + for rtp in _vim.eval('&runtimepath').split(','): + path = os.path.realpath(os.path.expanduser( + os.path.join(rtp, 'snippets'))) + for pattern in patterns: + for fn in glob.glob(os.path.join(path, pattern)): + ret.add(fn) + return ret + + +def _parse_snippet_file(content, full_filename): + """Parses 'content' assuming it is a .snippet file and yields events.""" + filename = full_filename[:-len('.snippet')] # strip extension + segments = _splitall(filename) + segments = segments[segments.index('snippets') + 1:] + assert len(segments) in (2, 3) + + trigger = segments[1] + description = segments[2] if 2 < len(segments) else '' + + # Chomp \n if any. + if content and content.endswith(os.linesep): + content = content[:-len(os.linesep)] + yield 'snippet', (SnipMateSnippetDefinition(trigger, content, + description, full_filename),) + + +def _parse_snippet(line, lines, filename): + """Parse a snippet defintions.""" + start_line_index = lines.line_index + trigger, description = head_tail(line[len('snippet'):].lstrip()) + content = '' + while True: + next_line = lines.peek() + if next_line is None: + break + if next_line.strip() and not next_line.startswith('\t'): + break + line = next(lines) + if line[0] == '\t': + line = line[1:] + content += line + content = content[:-1] # Chomp the last newline + return 'snippet', (SnipMateSnippetDefinition( + trigger, content, description, '%s:%i' % (filename, start_line_index)),) + + +def _parse_snippets_file(data, filename): + """Parse 'data' assuming it is a .snippets file. + + Yields events in the file. + + """ + lines = LineIterator(data) + for line in lines: + if not line.strip(): + continue + + head, tail = head_tail(line) + if head == 'extends': + yield handle_extends(tail, lines.line_index) + elif head in 'snippet': + snippet = _parse_snippet(line, lines, filename) + if snippet is not None: + yield snippet + elif head and not head.startswith('#'): + yield 'error', ('Invalid line %r' % line.rstrip(), lines.line_index) + + +class SnipMateFileSource(SnippetFileSource): + + """Manages all snipMate snippet definitions found in rtp.""" + + def _get_all_snippet_files_for(self, ft): + return snipmate_files_for(ft) + + def _parse_snippet_file(self, filedata, filename): + if filename.lower().endswith('snippet'): + for event, data in _parse_snippet_file(filedata, filename): + yield event, data + else: + for event, data in _parse_snippets_file(filedata, filename): + yield event, data diff --git a/pythonx/UltiSnips/snippet/source/file/ultisnips.py b/pythonx/UltiSnips/snippet/source/file/ultisnips.py new file mode 100644 index 0000000..2005ebd --- /dev/null +++ b/pythonx/UltiSnips/snippet/source/file/ultisnips.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Parsing of snippet files.""" + +from collections import defaultdict +import glob +import os + +from UltiSnips import _vim +from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition +from UltiSnips.snippet.source.file._base import SnippetFileSource +from UltiSnips.snippet.source.file._common import handle_extends, \ + handle_action +from UltiSnips.text import LineIterator, head_tail + + +def find_snippet_files(ft, directory): + """Returns all matching snippet files for 'ft' in 'directory'.""" + patterns = ['%s.snippets', '%s_*.snippets', os.path.join('%s', '*')] + ret = set() + directory = os.path.expanduser(directory) + for pattern in patterns: + for fn in glob.glob(os.path.join(directory, pattern % ft)): + ret.add(os.path.realpath(fn)) + return ret + + +def find_all_snippet_files(ft): + """Returns all snippet files matching 'ft' in the given runtime path + directory.""" + if _vim.eval("exists('b:UltiSnipsSnippetDirectories')") == '1': + snippet_dirs = _vim.eval('b:UltiSnipsSnippetDirectories') + else: + snippet_dirs = _vim.eval('g:UltiSnipsSnippetDirectories') + if len(snippet_dirs) == 1 and os.path.isabs(snippet_dirs[0]): + check_dirs = [''] + else: + check_dirs = _vim.eval('&runtimepath').split(',') + patterns = ['%s.snippets', '%s_*.snippets', os.path.join('%s', '*')] + ret = set() + for rtp in check_dirs: + for snippet_dir in snippet_dirs: + if snippet_dir == 'snippets': + raise RuntimeError( + "You have 'snippets' in UltiSnipsSnippetDirectories. This " + 'directory is reserved for snipMate snippets. Use another ' + 'directory for UltiSnips snippets.') + pth = os.path.realpath(os.path.expanduser( + os.path.join(rtp, snippet_dir))) + for pattern in patterns: + for fn in glob.glob(os.path.join(pth, pattern % ft)): + ret.add(fn) + return ret + + +def _handle_snippet_or_global( + filename, line, lines, python_globals, priority, pre_expand +): + """Parses the snippet that begins at the current line.""" + start_line_index = lines.line_index + descr = '' + opts = '' + + # Ensure this is a snippet + snip = line.split()[0] + + # Get and strip options if they exist + remain = line[len(snip):].strip() + words = remain.split() + + if len(words) > 2: + # second to last word ends with a quote + if '"' not in words[-1] and words[-2][-1] == '"': + opts = words[-1] + remain = remain[:-len(opts) - 1].rstrip() + + context = None + if 'e' in opts: + left = remain[:-1].rfind('"') + if left != -1 and left != 0: + context, remain = remain[left:].strip('"'), remain[:left] + + # Get and strip description if it exists + remain = remain.strip() + if len(remain.split()) > 1 and remain[-1] == '"': + left = remain[:-1].rfind('"') + if left != -1 and left != 0: + descr, remain = remain[left:], remain[:left] + + # The rest is the trigger + trig = remain.strip() + if len(trig.split()) > 1 or 'r' in opts: + if trig[0] != trig[-1]: + return 'error', ("Invalid multiword trigger: '%s'" % trig, + lines.line_index) + trig = trig[1:-1] + end = 'end' + snip + content = '' + + found_end = False + for line in lines: + if line.rstrip() == end: + content = content[:-1] # Chomp the last newline + found_end = True + break + content += line + + if not found_end: + return 'error', ("Missing 'endsnippet' for %r" % + trig, lines.line_index) + + if snip == 'global': + python_globals[trig].append(content) + elif snip == 'snippet': + definition = UltiSnipsSnippetDefinition( + priority, trig, content, + descr, opts, python_globals, + '%s:%i' % (filename, start_line_index), + context, pre_expand) + return 'snippet', (definition,) + else: + return 'error', ("Invalid snippet type: '%s'" % snip, lines.line_index) + + +def _parse_snippets_file(data, filename): + """Parse 'data' assuming it is a snippet file. + + Yields events in the file. + + """ + + python_globals = defaultdict(list) + lines = LineIterator(data) + current_priority = 0 + actions = {} + for line in lines: + if not line.strip(): + continue + + head, tail = head_tail(line) + if head in ('snippet', 'global'): + snippet = _handle_snippet_or_global( + filename, line, lines, + python_globals, + current_priority, + actions + ) + + actions = {} + if snippet is not None: + yield snippet + elif head == 'extends': + yield handle_extends(tail, lines.line_index) + elif head == 'clearsnippets': + yield 'clearsnippets', (current_priority, tail.split()) + elif head == 'priority': + try: + current_priority = int(tail.split()[0]) + except (ValueError, IndexError): + yield 'error', ('Invalid priority %r' % tail, lines.line_index) + elif head in ['pre_expand', 'post_expand', 'post_jump']: + head, tail = handle_action(head, tail, lines.line_index) + if head == 'error': + yield (head, tail) + else: + actions[head], = tail + elif head and not head.startswith('#'): + yield 'error', ('Invalid line %r' % line.rstrip(), lines.line_index) + + +class UltiSnipsFileSource(SnippetFileSource): + + """Manages all snippets definitions found in rtp for ultisnips.""" + + def _get_all_snippet_files_for(self, ft): + return find_all_snippet_files(ft) + + def _parse_snippet_file(self, filedata, filename): + for event, data in _parse_snippets_file(filedata, filename): + yield event, data diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py new file mode 100644 index 0000000..d9377e3 --- /dev/null +++ b/pythonx/UltiSnips/snippet_manager.py @@ -0,0 +1,818 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Contains the SnippetManager facade used by all Vim Functions.""" + +from collections import defaultdict +from functools import wraps +import os +import platform +import traceback +import sys +import vim +import re +from contextlib import contextmanager + +from UltiSnips import _vim +from UltiSnips._diff import diff, guess_edit +from UltiSnips.compatibility import as_unicode +from UltiSnips.position import Position +from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition +from UltiSnips.snippet.source import UltiSnipsFileSource, SnipMateFileSource, \ + find_all_snippet_files, find_snippet_files, AddedSnippetsSource +from UltiSnips.text import escape +from UltiSnips.vim_state import VimState, VisualContentPreserver +from UltiSnips.buffer_proxy import use_proxy_buffer, suspend_proxy_edits + + +def _ask_user(a, formatted): + """Asks the user using inputlist() and returns the selected element or + None.""" + try: + rv = _vim.eval('inputlist(%s)' % _vim.escape(formatted)) + if rv is None or rv == '0': + return None + rv = int(rv) + if rv > len(a): + rv = len(a) + return a[rv - 1] + except _vim.error: + # Likely "invalid expression", but might be translated. We have no way + # of knowing the exact error, therefore, we ignore all errors silently. + return None + except KeyboardInterrupt: + return None + + +def _ask_snippets(snippets): + """Given a list of snippets, ask the user which one they want to use, and + return it.""" + display = [as_unicode('%i: %s (%s)') % (i + 1, escape(s.description, '\\'), + escape(s.location, '\\')) for i, s in enumerate(snippets)] + return _ask_user(snippets, display) + + +def err_to_scratch_buffer(func): + """Decorator that will catch any Exception that 'func' throws and displays + it in a new Vim scratch buffer.""" + @wraps(func) + def wrapper(self, *args, **kwds): + try: + return func(self, *args, **kwds) + except Exception as e: # pylint: disable=bare-except + msg = \ + """An error occured. This is either a bug in UltiSnips or a bug in a +snippet definition. If you think this is a bug, please report it to +https://github.com/SirVer/ultisnips/issues/new. + +Following is the full stack trace: +""" + + msg += traceback.format_exc() + if hasattr(e, 'snippet_info'): + msg += "\nSnippet, caused error:\n" + msg += re.sub( + '^(?=\S)', ' ', e.snippet_info, flags=re.MULTILINE + ) + # snippet_code comes from _python_code.py, it's set manually for + # providing error message with stacktrace of failed python code + # inside of the snippet. + if hasattr(e, 'snippet_code'): + _, _, tb = sys.exc_info() + tb_top = traceback.extract_tb(tb)[-1] + msg += "\nExecuted snippet code:\n" + lines = e.snippet_code.split("\n") + for number, line in enumerate(lines, 1): + msg += str(number).rjust(3) + prefix = " " if line else "" + if tb_top[1] == number: + prefix = " > " + msg += prefix + line + "\n" + + # Vim sends no WinLeave msg here. + self._leaving_buffer() # pylint:disable=protected-access + _vim.new_scratch_buffer(msg) + return wrapper + + +# TODO(sirver): This class is still too long. It should only contain public +# facing methods, most of the private methods should be moved outside of it. +class SnippetManager(object): + + """The main entry point for all UltiSnips functionality. + + All Vim functions call methods in this class. + + """ + + def __init__(self, expand_trigger, forward_trigger, backward_trigger): + self.expand_trigger = expand_trigger + self.forward_trigger = forward_trigger + self.backward_trigger = backward_trigger + self._inner_state_up = False + self._supertab_keys = None + + self._csnippets = [] + self._buffer_filetypes = defaultdict(lambda: ['all']) + + self._vstate = VimState() + self._visual_content = VisualContentPreserver() + + self._snippet_sources = [] + + self._snip_expanded_in_action = False + self._inside_action = False + + self._last_inserted_char = '' + + self._added_snippets_source = AddedSnippetsSource() + self.register_snippet_source('ultisnips_files', UltiSnipsFileSource()) + self.register_snippet_source('added', self._added_snippets_source) + + enable_snipmate = '1' + if _vim.eval("exists('g:UltiSnipsEnableSnipMate')") == '1': + enable_snipmate = _vim.eval('g:UltiSnipsEnableSnipMate') + if enable_snipmate == '1': + self.register_snippet_source('snipmate_files', + SnipMateFileSource()) + + self._reinit() + + @err_to_scratch_buffer + def jump_forwards(self): + """Jumps to the next tabstop.""" + _vim.command('let g:ulti_jump_forwards_res = 1') + _vim.command('let &undolevels = &undolevels') + if not self._jump(): + _vim.command('let g:ulti_jump_forwards_res = 0') + return self._handle_failure(self.forward_trigger) + + @err_to_scratch_buffer + def jump_backwards(self): + """Jumps to the previous tabstop.""" + _vim.command('let g:ulti_jump_backwards_res = 1') + _vim.command('let &undolevels = &undolevels') + if not self._jump(True): + _vim.command('let g:ulti_jump_backwards_res = 0') + return self._handle_failure(self.backward_trigger) + + @err_to_scratch_buffer + def expand(self): + """Try to expand a snippet at the current position.""" + _vim.command('let g:ulti_expand_res = 1') + if not self._try_expand(): + _vim.command('let g:ulti_expand_res = 0') + self._handle_failure(self.expand_trigger) + + @err_to_scratch_buffer + def expand_or_jump(self): + """This function is used for people who wants to have the same trigger + for expansion and forward jumping. + + It first tries to expand a snippet, if this fails, it tries to + jump forward. + + """ + _vim.command('let g:ulti_expand_or_jump_res = 1') + rv = self._try_expand() + if not rv: + _vim.command('let g:ulti_expand_or_jump_res = 2') + rv = self._jump() + if not rv: + _vim.command('let g:ulti_expand_or_jump_res = 0') + self._handle_failure(self.expand_trigger) + + @err_to_scratch_buffer + def snippets_in_current_scope(self): + """Returns the snippets that could be expanded to Vim as a global + variable.""" + before = _vim.buf.line_till_cursor + snippets = self._snips(before, True) + + # Sort snippets alphabetically + snippets.sort(key=lambda x: x.trigger) + for snip in snippets: + description = snip.description[snip.description.find(snip.trigger) + + len(snip.trigger) + 2:] + + key = as_unicode(snip.trigger) + description = as_unicode(description) + + # remove surrounding "" or '' in snippet description if it exists + if len(description) > 2: + if (description[0] == description[-1] and + description[0] in "'\""): + description = description[1:-1] + + _vim.command(as_unicode( + "let g:current_ulti_dict['{key}'] = '{val}'").format( + key=key.replace("'", "''"), + val=description.replace("'", "''"))) + + @err_to_scratch_buffer + def list_snippets(self): + """Shows the snippets that could be expanded to the User and let her + select one.""" + before = _vim.buf.line_till_cursor + snippets = self._snips(before, True) + + if len(snippets) == 0: + self._handle_failure(self.backward_trigger) + return True + + # Sort snippets alphabetically + snippets.sort(key=lambda x: x.trigger) + + if not snippets: + return True + + snippet = _ask_snippets(snippets) + if not snippet: + return True + + self._do_snippet(snippet, before) + + return True + + @err_to_scratch_buffer + def add_snippet(self, trigger, value, description, + options, ft='all', priority=0, context=None, actions={}): + """Add a snippet to the list of known snippets of the given 'ft'.""" + self._added_snippets_source.add_snippet(ft, + UltiSnipsSnippetDefinition(priority, trigger, value, + description, options, {}, 'added', + context, actions)) + + @err_to_scratch_buffer + def expand_anon( + self, value, trigger='', description='', options='', + context=None, actions={} + ): + """Expand an anonymous snippet right here.""" + before = _vim.buf.line_till_cursor + snip = UltiSnipsSnippetDefinition(0, trigger, value, description, + options, {}, '', context, actions) + + if not trigger or snip.matches(before): + self._do_snippet(snip, before) + return True + else: + return False + + def register_snippet_source(self, name, snippet_source): + """Registers a new 'snippet_source' with the given 'name'. + + The given class must be an instance of SnippetSource. This + source will be queried for snippets. + + """ + self._snippet_sources.append((name, snippet_source)) + + def unregister_snippet_source(self, name): + """Unregister the source with the given 'name'. + + Does nothing if it is not registered. + + """ + for index, (source_name, _) in enumerate(self._snippet_sources): + if name == source_name: + self._snippet_sources = self._snippet_sources[:index] + \ + self._snippet_sources[index + 1:] + break + + def reset_buffer_filetypes(self): + """Reset the filetypes for the current buffer.""" + if _vim.buf.number in self._buffer_filetypes: + del self._buffer_filetypes[_vim.buf.number] + + def add_buffer_filetypes(self, ft): + """Checks for changes in the list of snippet files or the contents of + the snippet files and reloads them if necessary.""" + buf_fts = self._buffer_filetypes[_vim.buf.number] + idx = -1 + for ft in ft.split('.'): + ft = ft.strip() + if not ft: + continue + try: + idx = buf_fts.index(ft) + except ValueError: + self._buffer_filetypes[_vim.buf.number].insert(idx + 1, ft) + idx += 1 + + @err_to_scratch_buffer + def _cursor_moved(self): + """Called whenever the cursor moved.""" + if not self._csnippets and self._inner_state_up: + self._teardown_inner_state() + self._vstate.remember_position() + if _vim.eval('mode()') not in 'in': + return + + + if self._ignore_movements: + self._ignore_movements = False + return + + if self._csnippets: + cstart = self._csnippets[0].start.line + cend = self._csnippets[0].end.line + \ + self._vstate.diff_in_buffer_length + ct = _vim.buf[cstart:cend + 1] + lt = self._vstate.remembered_buffer + pos = _vim.buf.cursor + + lt_span = [0, len(lt)] + ct_span = [0, len(ct)] + initial_line = cstart + + # Cut down on lines searched for changes. Start from behind and + # remove all equal lines. Then do the same from the front. + if lt and ct: + while (lt[lt_span[1] - 1] == ct[ct_span[1] - 1] and + self._vstate.ppos.line < initial_line + lt_span[1] - 1 and + pos.line < initial_line + ct_span[1] - 1 and + (lt_span[0] < lt_span[1]) and + (ct_span[0] < ct_span[1])): + ct_span[1] -= 1 + lt_span[1] -= 1 + while (lt_span[0] < lt_span[1] and + ct_span[0] < ct_span[1] and + lt[lt_span[0]] == ct[ct_span[0]] and + self._vstate.ppos.line >= initial_line and + pos.line >= initial_line): + ct_span[0] += 1 + lt_span[0] += 1 + initial_line += 1 + ct_span[0] = max(0, ct_span[0] - 1) + lt_span[0] = max(0, lt_span[0] - 1) + initial_line = max(cstart, initial_line - 1) + + lt = lt[lt_span[0]:lt_span[1]] + ct = ct[ct_span[0]:ct_span[1]] + + try: + rv, es = guess_edit(initial_line, lt, ct, self._vstate) + if not rv: + lt = '\n'.join(lt) + ct = '\n'.join(ct) + es = diff(lt, ct, initial_line) + self._csnippets[0].replay_user_edits(es, self._ctab) + except IndexError: + # Rather do nothing than throwing an error. It will be correct + # most of the time + pass + + self._check_if_still_inside_snippet() + if self._csnippets: + self._csnippets[0].update_textobjects() + self._vstate.remember_buffer(self._csnippets[0]) + + def _setup_inner_state(self): + """Map keys and create autocommands that should only be defined when a + snippet is active.""" + if self._inner_state_up: + return + if self.expand_trigger != self.forward_trigger: + _vim.command('inoremap ' + self.forward_trigger + + ' =UltiSnips#JumpForwards()') + _vim.command('snoremap ' + self.forward_trigger + + ' :call UltiSnips#JumpForwards()') + _vim.command('inoremap ' + self.backward_trigger + + ' =UltiSnips#JumpBackwards()') + _vim.command('snoremap ' + self.backward_trigger + + ' :call UltiSnips#JumpBackwards()') + + # Setup the autogroups. + _vim.command('augroup UltiSnips') + _vim.command('autocmd!') + _vim.command('autocmd CursorMovedI * call UltiSnips#CursorMoved()') + _vim.command('autocmd CursorMoved * call UltiSnips#CursorMoved()') + + _vim.command( + 'autocmd InsertLeave * call UltiSnips#LeavingInsertMode()') + + _vim.command('autocmd BufLeave * call UltiSnips#LeavingBuffer()') + _vim.command( + 'autocmd CmdwinEnter * call UltiSnips#LeavingBuffer()') + _vim.command( + 'autocmd CmdwinLeave * call UltiSnips#LeavingBuffer()') + + # Also exit the snippet when we enter a unite complete buffer. + _vim.command('autocmd Filetype unite call UltiSnips#LeavingBuffer()') + + _vim.command('augroup END') + + _vim.command('silent doautocmd User UltiSnipsEnterFirstSnippet') + self._inner_state_up = True + + def _teardown_inner_state(self): + """Reverse _setup_inner_state.""" + if not self._inner_state_up: + return + try: + _vim.command('silent doautocmd User UltiSnipsExitLastSnippet') + if self.expand_trigger != self.forward_trigger: + _vim.command('iunmap %s' % self.forward_trigger) + _vim.command('sunmap %s' % self.forward_trigger) + _vim.command('iunmap %s' % self.backward_trigger) + _vim.command('sunmap %s' % self.backward_trigger) + _vim.command('augroup UltiSnips') + _vim.command('autocmd!') + _vim.command('augroup END') + self._inner_state_up = False + except _vim.error: + # This happens when a preview window was opened. This issues + # CursorMoved, but not BufLeave. We have no way to unmap, until we + # are back in our buffer + pass + + @err_to_scratch_buffer + def _save_last_visual_selection(self): + """This is called when the expand trigger is pressed in visual mode. + Our job is to remember everything between '< and '> and pass it on to. + + ${VISUAL} in case it will be needed. + + """ + self._visual_content.conserve() + + def _leaving_buffer(self): + """Called when the user switches tabs/windows/buffers. + + It basically means that all snippets must be properly + terminated. + + """ + while len(self._csnippets): + self._current_snippet_is_done() + self._reinit() + + def _reinit(self): + """Resets transient state.""" + self._ctab = None + self._ignore_movements = False + + def _check_if_still_inside_snippet(self): + """Checks if the cursor is outside of the current snippet.""" + if self._cs and ( + not self._cs.start <= _vim.buf.cursor <= self._cs.end + ): + self._current_snippet_is_done() + self._reinit() + self._check_if_still_inside_snippet() + + def _current_snippet_is_done(self): + """The current snippet should be terminated.""" + self._csnippets.pop() + if not self._csnippets: + self._teardown_inner_state() + + def _jump(self, backwards=False): + # we need to set 'onemore' there, because of limitations of the vim + # API regarding cursor movements; without that test + # 'CanExpandAnonSnippetInJumpActionWhileSelected' will fail + with _vim.toggle_opt('ve', 'onemore'): + """Helper method that does the actual jump.""" + jumped = False + + # We need to remember current snippets stack here because of + # post-jump action on the last tabstop should be able to access + # snippet instance which is ended just now. + stack_for_post_jump = self._csnippets[:] + + # If next tab has length 1 and the distance between itself and + # self._ctab is 1 then there is 1 less CursorMove events. We + # cannot ignore next movement in such case. + ntab_short_and_near = False + + if self._cs: + snippet_for_action = self._cs + elif stack_for_post_jump: + snippet_for_action = stack_for_post_jump[-1] + else: + snippet_for_action = None + + if self._cs: + ntab = self._cs.select_next_tab(backwards) + if ntab: + if self._cs.snippet.has_option('s'): + lineno = _vim.buf.cursor.line + _vim.buf[lineno] = _vim.buf[lineno].rstrip() + _vim.select(ntab.start, ntab.end) + jumped = True + if (self._ctab is not None + and ntab.start - self._ctab.end == Position(0, 1) + and ntab.end - ntab.start == Position(0, 1)): + ntab_short_and_near = True + if ntab.number == 0: + self._current_snippet_is_done() + self._ctab = ntab + else: + # This really shouldn't happen, because a snippet should + # have been popped when its final tabstop was used. + # Cleanup by removing current snippet and recursing. + self._current_snippet_is_done() + jumped = self._jump(backwards) + if jumped: + self._vstate.remember_position() + self._vstate.remember_unnamed_register(self._ctab.current_text) + if not ntab_short_and_near: + self._ignore_movements = True + + if len(stack_for_post_jump) > 0 and ntab is not None: + with use_proxy_buffer(stack_for_post_jump, self._vstate): + snippet_for_action.snippet.do_post_jump( + ntab.number, + -1 if backwards else 1, + stack_for_post_jump, + snippet_for_action + ) + + return jumped + + def _leaving_insert_mode(self): + """Called whenever we leave the insert mode.""" + self._vstate.restore_unnamed_register() + + def _handle_failure(self, trigger): + """Mainly make sure that we play well with SuperTab.""" + if trigger.lower() == '': + feedkey = '\\' + trigger + elif trigger.lower() == '': + feedkey = '\\' + trigger + else: + feedkey = None + mode = 'n' + if not self._supertab_keys: + if _vim.eval("exists('g:SuperTabMappingForward')") != '0': + self._supertab_keys = ( + _vim.eval('g:SuperTabMappingForward'), + _vim.eval('g:SuperTabMappingBackward'), + ) + else: + self._supertab_keys = ['', ''] + + for idx, sttrig in enumerate(self._supertab_keys): + if trigger.lower() == sttrig.lower(): + if idx == 0: + feedkey = r"\SuperTabForward" + mode = 'n' + elif idx == 1: + feedkey = r"\SuperTabBackward" + mode = 'p' + # Use remap mode so SuperTab mappings will be invoked. + break + + if (feedkey == r"\SuperTabForward" or + feedkey == r"\SuperTabBackward"): + _vim.command('return SuperTab(%s)' % _vim.escape(mode)) + elif feedkey: + _vim.command('return %s' % _vim.escape(feedkey)) + + def _snips(self, before, partial, autotrigger_only=False): + """Returns all the snippets for the given text before the cursor. + + If partial is True, then get also return partial matches. + + """ + filetypes = self._buffer_filetypes[_vim.buf.number][::-1] + matching_snippets = defaultdict(list) + clear_priority = None + cleared = {} + for _, source in self._snippet_sources: + source.ensure(filetypes, cached=autotrigger_only) + + # Collect cleared information from sources. + for _, source in self._snippet_sources: + sclear_priority = source.get_clear_priority(filetypes) + if sclear_priority is not None and (clear_priority is None + or sclear_priority > clear_priority): + clear_priority = sclear_priority + for key, value in source.get_cleared(filetypes).items(): + if key not in cleared or value > cleared[key]: + cleared[key] = value + + for _, source in self._snippet_sources: + possible_snippets = source.get_snippets( + filetypes, + before, + partial, + autotrigger_only + ) + + for snippet in possible_snippets: + if ((clear_priority is None or snippet.priority > clear_priority) + and (snippet.trigger not in cleared or + snippet.priority > cleared[snippet.trigger])): + matching_snippets[snippet.trigger].append(snippet) + if not matching_snippets: + return [] + + # Now filter duplicates and only keep the one with the highest + # priority. + snippets = [] + for snippets_with_trigger in matching_snippets.values(): + highest_priority = max(s.priority for s in snippets_with_trigger) + snippets.extend(s for s in snippets_with_trigger + if s.priority == highest_priority) + + # For partial matches we are done, but if we want to expand a snippet, + # we have to go over them again and only keep those with the maximum + # priority. + if partial: + return snippets + + highest_priority = max(s.priority for s in snippets) + return [s for s in snippets if s.priority == highest_priority] + + def _do_snippet(self, snippet, before): + """Expands the given snippet, and handles everything that needs to be + done with it.""" + self._setup_inner_state() + + self._snip_expanded_in_action = False + + # Adjust before, maybe the trigger is not the complete word + text_before = before + if snippet.matched: + text_before = before[:-len(snippet.matched)] + + with use_proxy_buffer(self._csnippets, self._vstate): + with self._action_context(): + cursor_set_in_action = snippet.do_pre_expand( + self._visual_content.text, + self._csnippets + ) + + if cursor_set_in_action: + text_before = _vim.buf.line_till_cursor + before = _vim.buf.line_till_cursor + + with suspend_proxy_edits(): + if self._cs: + start = Position(_vim.buf.cursor.line, len(text_before)) + end = Position(_vim.buf.cursor.line, len(before)) + + # If cursor is set in pre-action, then action was modified + # cursor line, in that case we do not need to do any edits, it + # can break snippet + if not cursor_set_in_action: + # It could be that our trigger contains the content of + # TextObjects in our containing snippet. If this is indeed + # the case, we have to make sure that those are properly + # killed. We do this by pretending that the user deleted + # and retyped the text that our trigger matched. + edit_actions = [ + ('D', start.line, start.col, snippet.matched), + ('I', start.line, start.col, snippet.matched), + ] + self._csnippets[0].replay_user_edits(edit_actions) + + si = snippet.launch(text_before, self._visual_content, + self._cs.find_parent_for_new_to(start), + start, end + ) + else: + start = Position(_vim.buf.cursor.line, len(text_before)) + end = Position(_vim.buf.cursor.line, len(before)) + si = snippet.launch(text_before, self._visual_content, + None, start, end) + + self._visual_content.reset() + self._csnippets.append(si) + + si.update_textobjects() + + with use_proxy_buffer(self._csnippets, self._vstate): + with self._action_context(): + snippet.do_post_expand( + si._start, si._end, self._csnippets + ) + + self._vstate.remember_buffer(self._csnippets[0]) + + if not self._snip_expanded_in_action: + self._jump() + elif self._cs.current_text != '': + self._jump() + else: + self._current_snippet_is_done() + + if self._inside_action: + self._snip_expanded_in_action = True + + + def _try_expand(self, autotrigger_only=False): + """Try to expand a snippet in the current place.""" + before = _vim.buf.line_till_cursor + snippets = self._snips(before, False, autotrigger_only) + if snippets: + # prefer snippets with context if any + snippets_with_context = [s for s in snippets if s.context] + if snippets_with_context: + snippets = snippets_with_context + if not snippets: + # No snippet found + return False + _vim.command('let &undolevels = &undolevels') + if len(snippets) == 1: + snippet = snippets[0] + else: + snippet = _ask_snippets(snippets) + if not snippet: + return True + self._do_snippet(snippet, before) + _vim.command('let &undolevels = &undolevels') + return True + + @property + def _cs(self): + """The current snippet or None.""" + if not len(self._csnippets): + return None + return self._csnippets[-1] + + def _file_to_edit(self, requested_ft, bang): # pylint: disable=no-self-use + """Returns a file to be edited for the given requested_ft. + + If 'bang' is + empty only private files in g:UltiSnipsSnippetsDir are considered, + otherwise all files are considered and the user gets to choose. + + """ + # This method is not using self, but is called by UltiSnips.vim and is + # therefore in this class because it is the facade to Vim. + potentials = set() + + if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == '1': + snippet_dir = _vim.eval('g:UltiSnipsSnippetsDir') + else: + home = _vim.eval('$HOME') + if platform.system() == 'Windows': + snippet_dir = os.path.join(home, 'vimfiles', 'UltiSnips') + elif _vim.eval("has('nvim')") == '1': + xdg_home_config = _vim.eval('$XDG_CONFIG_HOME') or os.path.join(home, ".config") + snippet_dir = os.path.join(xdg_home_config, 'nvim', 'UltiSnips') + else: + snippet_dir = os.path.join(home, '.vim', 'UltiSnips') + + filetypes = [] + if requested_ft: + filetypes.append(requested_ft) + else: + if bang: + filetypes.extend(self._buffer_filetypes[_vim.buf.number]) + else: + filetypes.append(self._buffer_filetypes[_vim.buf.number][0]) + + for ft in filetypes: + potentials.update(find_snippet_files(ft, snippet_dir)) + potentials.add(os.path.join(snippet_dir, + ft + '.snippets')) + if bang: + potentials.update(find_all_snippet_files(ft)) + + potentials = set(os.path.realpath(os.path.expanduser(p)) + for p in potentials) + + if len(potentials) > 1: + files = sorted(potentials) + formatted = [as_unicode('%i: %s') % (i, escape(fn, '\\')) for + i, fn in enumerate(files, 1)] + file_to_edit = _ask_user(files, formatted) + if file_to_edit is None: + return '' + else: + file_to_edit = potentials.pop() + + dirname = os.path.dirname(file_to_edit) + if not os.path.exists(dirname): + os.makedirs(dirname) + return file_to_edit + + @contextmanager + def _action_context(self): + try: + old_flag = self._inside_action + self._inside_action = True + yield + finally: + self._inside_action = old_flag + + @err_to_scratch_buffer + def _track_change(self): + inserted_char = _vim.eval('v:char') + try: + if inserted_char == '': + before = _vim.buf.line_till_cursor + if before and before[-1] == self._last_inserted_char: + self._try_expand(autotrigger_only=True) + finally: + self._last_inserted_char = inserted_char + + +UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name + vim.eval('g:UltiSnipsExpandTrigger'), + vim.eval('g:UltiSnipsJumpForwardTrigger'), + vim.eval('g:UltiSnipsJumpBackwardTrigger')) diff --git a/pythonx/UltiSnips/test_diff.py b/pythonx/UltiSnips/test_diff.py new file mode 100644 index 0000000..6d1d4f4 --- /dev/null +++ b/pythonx/UltiSnips/test_diff.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# pylint: skip-file + +import unittest + +from _diff import diff, guess_edit +from position import Position + + +def transform(a, cmds): + buf = a.split('\n') + + for cmd in cmds: + ctype, line, col, char = cmd + if ctype == 'D': + if char != '\n': + buf[line] = buf[line][:col] + buf[line][col + len(char):] + else: + buf[line] = buf[line] + buf[line + 1] + del buf[line + 1] + elif ctype == 'I': + buf[line] = buf[line][:col] + char + buf[line][col:] + buf = '\n'.join(buf).split('\n') + return '\n'.join(buf) + + +import unittest + +# Test Guessing {{{ + + +class _BaseGuessing(object): + + def runTest(self): + rv, es = guess_edit( + self.initial_line, self.a, self.b, Position(*self.ppos), Position(*self.pos)) + self.assertEqual(rv, True) + self.assertEqual(self.wanted, es) + + +class TestGuessing_Noop0(_BaseGuessing, unittest.TestCase): + a, b = [], [] + initial_line = 0 + ppos, pos = (0, 6), (0, 7) + wanted = () + + +class TestGuessing_InsertOneChar(_BaseGuessing, unittest.TestCase): + a, b = ['Hello World'], ['Hello World'] + initial_line = 0 + ppos, pos = (0, 6), (0, 7) + wanted = ( + ('I', 0, 6, ' '), + ) + + +class TestGuessing_InsertOneChar1(_BaseGuessing, unittest.TestCase): + a, b = ['Hello World'], ['Hello World'] + initial_line = 0 + ppos, pos = (0, 7), (0, 8) + wanted = ( + ('I', 0, 7, ' '), + ) + + +class TestGuessing_BackspaceOneChar(_BaseGuessing, unittest.TestCase): + a, b = ['Hello World'], ['Hello World'] + initial_line = 0 + ppos, pos = (0, 7), (0, 6) + wanted = ( + ('D', 0, 6, ' '), + ) + + +class TestGuessing_DeleteOneChar(_BaseGuessing, unittest.TestCase): + a, b = ['Hello World'], ['Hello World'] + initial_line = 0 + ppos, pos = (0, 5), (0, 5) + wanted = ( + ('D', 0, 5, ' '), + ) + +# End: Test Guessing }}} + + +class _Base(object): + + def runTest(self): + es = diff(self.a, self.b) + tr = transform(self.a, es) + self.assertEqual(self.b, tr) + self.assertEqual(self.wanted, es) + + +class TestEmptyString(_Base, unittest.TestCase): + a, b = '', '' + wanted = () + + +class TestAllMatch(_Base, unittest.TestCase): + a, b = 'abcdef', 'abcdef' + wanted = () + + +class TestLotsaNewlines(_Base, unittest.TestCase): + a, b = 'Hello', 'Hello\nWorld\nWorld\nWorld' + wanted = ( + ('I', 0, 5, '\n'), + ('I', 1, 0, 'World'), + ('I', 1, 5, '\n'), + ('I', 2, 0, 'World'), + ('I', 2, 5, '\n'), + ('I', 3, 0, 'World'), + ) + + +class TestCrash(_Base, unittest.TestCase): + a = 'hallo Blah mitte=sdfdsfsd\nhallo kjsdhfjksdhfkjhsdfkh mittekjshdkfhkhsdfdsf' + b = 'hallo Blah mitte=sdfdsfsd\nhallo b mittekjshdkfhkhsdfdsf' + wanted = ( + ('D', 1, 6, 'kjsdhfjksdhfkjhsdfkh'), + ('I', 1, 6, 'b'), + ) + + +class TestRealLife(_Base, unittest.TestCase): + a = 'hallo End Beginning' + b = 'hallo End t' + wanted = ( + ('D', 0, 10, 'Beginning'), + ('I', 0, 10, 't'), + ) + + +class TestRealLife1(_Base, unittest.TestCase): + a = 'Vorne hallo Hinten' + b = 'Vorne hallo Hinten' + wanted = ( + ('I', 0, 11, ' '), + ) + + +class TestWithNewline(_Base, unittest.TestCase): + a = 'First Line\nSecond Line' + b = 'n' + wanted = ( + ('D', 0, 0, 'First Line'), + ('D', 0, 0, '\n'), + ('D', 0, 0, 'Second Line'), + ('I', 0, 0, 'n'), + ) + + +class TestCheapDelete(_Base, unittest.TestCase): + a = 'Vorne hallo Hinten' + b = 'Vorne Hinten' + wanted = ( + ('D', 0, 5, ' hallo'), + ) + + +class TestNoSubstring(_Base, unittest.TestCase): + a, b = 'abc', 'def' + wanted = ( + ('D', 0, 0, 'abc'), + ('I', 0, 0, 'def'), + ) + + +class TestCommonCharacters(_Base, unittest.TestCase): + a, b = 'hasomelongertextbl', 'hol' + wanted = ( + ('D', 0, 1, 'asomelongertextb'), + ('I', 0, 1, 'o'), + ) + + +class TestUltiSnipsProblem(_Base, unittest.TestCase): + a = 'this is it this is it this is it' + b = 'this is it a this is it' + wanted = ( + ('D', 0, 11, 'this is it'), + ('I', 0, 11, 'a'), + ) + + +class MatchIsTooCheap(_Base, unittest.TestCase): + a = 'stdin.h' + b = 's' + wanted = ( + ('D', 0, 1, 'tdin.h'), + ) + + +class MultiLine(_Base, unittest.TestCase): + a = 'hi first line\nsecond line first line\nsecond line world' + b = 'hi first line\nsecond line k world' + + wanted = ( + ('D', 1, 12, 'first line'), + ('D', 1, 12, '\n'), + ('D', 1, 12, 'second line'), + ('I', 1, 12, 'k'), + ) + + +if __name__ == '__main__': + unittest.main() + # k = TestEditScript() + # unittest.TextTestRunner().run(k) diff --git a/pythonx/UltiSnips/test_position.py b/pythonx/UltiSnips/test_position.py new file mode 100644 index 0000000..f980f4b --- /dev/null +++ b/pythonx/UltiSnips/test_position.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# pylint: skip-file + +import unittest + +from position import Position + + +class _MPBase(object): + + def runTest(self): + obj = Position(*self.obj) + for pivot, delta, wanted in self.steps: + obj.move(Position(*pivot), Position(*delta)) + self.assertEqual(Position(*wanted), obj) + + +class MovePosition_DelSameLine(_MPBase, unittest.TestCase): + # hello wor*ld -> h*ld -> hl*ld + obj = (0, 9) + steps = ( + ((0, 1), (0, -8), (0, 1)), + ((0, 1), (0, 1), (0, 2)), + ) + + +class MovePosition_DelSameLine1(_MPBase, unittest.TestCase): + # hel*lo world -> hel*world -> hel*worl + obj = (0, 3) + steps = ( + ((0, 4), (0, -3), (0, 3)), + ((0, 8), (0, -1), (0, 3)), + ) + + +class MovePosition_InsSameLine1(_MPBase, unittest.TestCase): + # hel*lo world -> hel*woresld + obj = (0, 3) + steps = ( + ((0, 4), (0, -3), (0, 3)), + ((0, 6), (0, 2), (0, 3)), + ((0, 8), (0, -1), (0, 3)) + ) + + +class MovePosition_InsSameLine2(_MPBase, unittest.TestCase): + # hello wor*ld -> helesdlo wor*ld + obj = (0, 9) + steps = ( + ((0, 3), (0, 3), (0, 12)), + ) + + +class MovePosition_DelSecondLine(_MPBase, unittest.TestCase): + # hello world. sup hello world.*a, was + # *a, was ach nix + # ach nix + obj = (1, 0) + steps = ( + ((0, 12), (0, -4), (1, 0)), + ((0, 12), (-1, 0), (0, 12)), + ) + + +class MovePosition_DelSecondLine1(_MPBase, unittest.TestCase): + # hello world. sup + # a, *was + # ach nix + # hello world.a*was + # ach nix + obj = (1, 3) + steps = ( + ((0, 12), (0, -4), (1, 3)), + ((0, 12), (-1, 0), (0, 15)), + ((0, 12), (0, -3), (0, 12)), + ((0, 12), (0, 1), (0, 13)), + ) + +if __name__ == '__main__': + unittest.main() diff --git a/pythonx/UltiSnips/text.py b/pythonx/UltiSnips/text.py new file mode 100644 index 0000000..165cddd --- /dev/null +++ b/pythonx/UltiSnips/text.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Utilities to deal with text.""" + + +def unescape(text): + """Removes '\\' escaping from 'text'.""" + rv = '' + i = 0 + while i < len(text): + if i + 1 < len(text) and text[i] == '\\': + rv += text[i + 1] + i += 1 + else: + rv += text[i] + i += 1 + return rv + + +def escape(text, chars): + """Escapes all characters in 'chars' in text using backspaces.""" + rv = '' + for char in text: + if char in chars: + rv += '\\' + rv += char + return rv + + +def fill_in_whitespace(text): + """Returns 'text' with escaped whitespace replaced through whitespaces.""" + text = text.replace(r"\n", '\n') + text = text.replace(r"\t", '\t') + text = text.replace(r"\r", '\r') + text = text.replace(r"\a", '\a') + text = text.replace(r"\b", '\b') + return text + + +def head_tail(line): + """Returns the first word in 'line' and the rest of 'line' or None if the + line is too short.""" + generator = (t.strip() for t in line.split(None, 1)) + head = next(generator).strip() + tail = '' + try: + tail = next(generator).strip() + except StopIteration: + pass + return head, tail + + +class LineIterator(object): + + """Convenience class that keeps track of line numbers in files.""" + + def __init__(self, text): + self._line_index = -1 + self._lines = list(text.splitlines(True)) + + def __iter__(self): + return self + + def __next__(self): + """Returns the next line.""" + if self._line_index + 1 < len(self._lines): + self._line_index += 1 + return self._lines[self._line_index] + raise StopIteration() + next = __next__ # for python2 + + @property + def line_index(self): + """The 1 based line index in the current file.""" + return self._line_index + 1 + + def peek(self): + """Returns the next line (if there is any, otherwise None) without + advancing the iterator.""" + try: + return self._lines[self._line_index + 1] + except IndexError: + return None diff --git a/pythonx/UltiSnips/text_objects/__init__.py b/pythonx/UltiSnips/text_objects/__init__.py new file mode 100644 index 0000000..97e2252 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Public facing classes for TextObjects.""" + +from UltiSnips.text_objects._escaped_char import EscapedChar +from UltiSnips.text_objects._mirror import Mirror +from UltiSnips.text_objects._python_code import PythonCode +from UltiSnips.text_objects._shell_code import ShellCode +from UltiSnips.text_objects._snippet_instance import SnippetInstance +from UltiSnips.text_objects._tabstop import TabStop +from UltiSnips.text_objects._transformation import Transformation +from UltiSnips.text_objects._viml_code import VimLCode +from UltiSnips.text_objects._visual import Visual diff --git a/pythonx/UltiSnips/text_objects/_base.py b/pythonx/UltiSnips/text_objects/_base.py new file mode 100644 index 0000000..6ee1745 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_base.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Base classes for all text objects.""" + +from UltiSnips import _vim +from UltiSnips.position import Position + + +def _calc_end(text, start): + """Calculate the end position of the 'text' starting at 'start.""" + if len(text) == 1: + new_end = start + Position(0, len(text[0])) + else: + new_end = Position(start.line + len(text) - 1, len(text[-1])) + return new_end + + +def _text_to_vim(start, end, text): + """Copy the given text to the current buffer, overwriting the span 'start' + to 'end'.""" + lines = text.split('\n') + + new_end = _calc_end(lines, start) + + before = _vim.buf[start.line][:start.col] + after = _vim.buf[end.line][end.col:] + + new_lines = [] + if len(lines): + new_lines.append(before + lines[0]) + new_lines.extend(lines[1:]) + new_lines[-1] += after + _vim.buf[start.line:end.line + 1] = new_lines + + # Open any folds this might have created + _vim.buf.cursor = start + _vim.command('normal! zv') + + return new_end + +# These classes use their subclasses a lot and we really do not want to expose +# their functions more globally. +# pylint: disable=protected-access + + +class TextObject(object): + + """Represents any object in the text that has a span in any ways.""" + + def __init__(self, parent, token_or_start, end=None, + initial_text='', tiebreaker=None): + self._parent = parent + + if end is not None: # Took 4 arguments + self._start = token_or_start + self._end = end + self._initial_text = initial_text + else: # Initialize from token + self._start = token_or_start.start + self._end = token_or_start.end + self._initial_text = token_or_start.initial_text + self._tiebreaker = tiebreaker or Position( + self._start.line, self._end.line) + if parent is not None: + parent._add_child(self) + + def _move(self, pivot, diff): + """Move this object by 'diff' while 'pivot' is the point of change.""" + self._start.move(pivot, diff) + self._end.move(pivot, diff) + + def __lt__(self, other): + me_tuple = (self.start.line, self.start.col, + self._tiebreaker.line, self._tiebreaker.col) + other_tuple = (other._start.line, other._start.col, + other._tiebreaker.line, other._tiebreaker.col) + return me_tuple < other_tuple + + def __le__(self, other): + me_tuple = (self._start.line, self._start.col, + self._tiebreaker.line, self._tiebreaker.col) + other_tuple = (other._start.line, other._start.col, + other._tiebreaker.line, other._tiebreaker.col) + return me_tuple <= other_tuple + + def __repr__(self): + ct = '' + try: + ct = self.current_text + except IndexError: + ct = '' + + return '%s(%r->%r,%r)' % (self.__class__.__name__, + self._start, self._end, ct) + + @property + def current_text(self): + """The current text of this object.""" + if self._start.line == self._end.line: + return _vim.buf[self._start.line][self._start.col:self._end.col] + else: + lines = [_vim.buf[self._start.line][self._start.col:]] + lines.extend(_vim.buf[self._start.line + 1:self._end.line]) + lines.append(_vim.buf[self._end.line][:self._end.col]) + return '\n'.join(lines) + + @property + def start(self): + """The start position.""" + return self._start + + @property + def end(self): + """The end position.""" + return self._end + + def overwrite(self, gtext=None): + """Overwrite the text of this object in the Vim Buffer and update its + length information. + + If 'gtext' is None use the initial text of this object. + + """ + # We explicitly do not want to move our children around here as we + # either have non or we are replacing text initially which means we do + # not want to mess with their positions + if self.current_text == gtext: + return + old_end = self._end + self._end = _text_to_vim( + self._start, self._end, gtext or self._initial_text) + if self._parent: + self._parent._child_has_moved( + self._parent._children.index(self), min(old_end, self._end), + self._end.delta(old_end) + ) + + def _update(self, done): + """Update this object inside the Vim Buffer. + + Return False if you need to be called again for this edit cycle. + Otherwise return True. + + """ + raise NotImplementedError('Must be implemented by subclasses.') + + +class EditableTextObject(TextObject): + + """This base class represents any object in the text that can be changed by + the user.""" + + def __init__(self, *args, **kwargs): + TextObject.__init__(self, *args, **kwargs) + self._children = [] + self._tabstops = {} + + ############## + # Properties # + ############## + @property + def children(self): + """List of all children.""" + return self._children + + @property + def _editable_children(self): + """List of all children that are EditableTextObjects.""" + return [child for child in self._children if + isinstance(child, EditableTextObject)] + + #################### + # Public Functions # + #################### + def find_parent_for_new_to(self, pos): + """Figure out the parent object for something at 'pos'.""" + for children in self._editable_children: + if children._start <= pos < children._end: + return children.find_parent_for_new_to(pos) + if children._start == pos and pos == children._end: + return children.find_parent_for_new_to(pos) + return self + + ############################### + # Private/Protected functions # + ############################### + def _do_edit(self, cmd, ctab=None): + """Apply the edit 'cmd' to this object.""" + ctype, line, col, text = cmd + assert ('\n' not in text) or (text == '\n') + pos = Position(line, col) + + to_kill = set() + new_cmds = [] + for child in self._children: + if ctype == 'I': # Insertion + if (child._start < pos < + Position(child._end.line, child._end.col) and + isinstance(child, NoneditableTextObject)): + to_kill.add(child) + new_cmds.append(cmd) + break + elif ((child._start <= pos <= child._end) and + isinstance(child, EditableTextObject)): + if pos == child.end and not child.children: + try: + if ctab.number != child.number: + continue + except AttributeError: + pass + child._do_edit(cmd, ctab) + return + else: # Deletion + delend = pos + Position(0, len(text)) if text != '\n' \ + else Position(line + 1, 0) + if ((child._start <= pos < child._end) and + (child._start < delend <= child._end)): + # this edit command is completely for the child + if isinstance(child, NoneditableTextObject): + to_kill.add(child) + new_cmds.append(cmd) + break + else: + child._do_edit(cmd, ctab) + return + elif ((pos < child._start and child._end <= delend and + child.start < delend) or + (pos <= child._start and child._end < delend)): + # Case: this deletion removes the child + to_kill.add(child) + new_cmds.append(cmd) + break + elif (pos < child._start and + (child._start < delend <= child._end)): + # Case: partially for us, partially for the child + my_text = text[:(child._start - pos).col] + c_text = text[(child._start - pos).col:] + new_cmds.append((ctype, line, col, my_text)) + new_cmds.append((ctype, line, col, c_text)) + break + elif (delend >= child._end and ( + child._start <= pos < child._end)): + # Case: partially for us, partially for the child + c_text = text[(child._end - pos).col:] + my_text = text[:(child._end - pos).col] + new_cmds.append((ctype, line, col, c_text)) + new_cmds.append((ctype, line, col, my_text)) + break + + for child in to_kill: + self._del_child(child) + if len(new_cmds): + for child in new_cmds: + self._do_edit(child) + return + + # We have to handle this ourselves + delta = Position(1, 0) if text == '\n' else Position(0, len(text)) + if ctype == 'D': + # Makes no sense to delete in empty textobject + if self._start == self._end: + return + delta.line *= -1 + delta.col *= -1 + pivot = Position(line, col) + idx = -1 + for cidx, child in enumerate(self._children): + if child._start < pivot <= child._end: + idx = cidx + self._child_has_moved(idx, pivot, delta) + + def _move(self, pivot, diff): + TextObject._move(self, pivot, diff) + + for child in self._children: + child._move(pivot, diff) + + def _child_has_moved(self, idx, pivot, diff): + """Called when a the child with 'idx' has moved behind 'pivot' by + 'diff'.""" + self._end.move(pivot, diff) + + for child in self._children[idx + 1:]: + child._move(pivot, diff) + + if self._parent: + self._parent._child_has_moved( + self._parent._children.index(self), pivot, diff + ) + + def _get_next_tab(self, number): + """Returns the next tabstop after 'number'.""" + if not len(self._tabstops.keys()): + return + tno_max = max(self._tabstops.keys()) + + possible_sol = [] + i = number + 1 + while i <= tno_max: + if i in self._tabstops: + possible_sol.append((i, self._tabstops[i])) + break + i += 1 + + child = [c._get_next_tab(number) for c in self._editable_children] + child = [c for c in child if c] + + possible_sol += child + + if not len(possible_sol): + return None + + return min(possible_sol) + + def _get_prev_tab(self, number): + """Returns the previous tabstop before 'number'.""" + if not len(self._tabstops.keys()): + return + tno_min = min(self._tabstops.keys()) + + possible_sol = [] + i = number - 1 + while i >= tno_min and i > 0: + if i in self._tabstops: + possible_sol.append((i, self._tabstops[i])) + break + i -= 1 + + child = [c._get_prev_tab(number) for c in self._editable_children] + child = [c for c in child if c] + + possible_sol += child + + if not len(possible_sol): + return None + + return max(possible_sol) + + def _get_tabstop(self, requester, number): + """Returns the tabstop 'number'. + + 'requester' is the class that is interested in this. + + """ + if number in self._tabstops: + return self._tabstops[number] + for child in self._editable_children: + if child is requester: + continue + rv = child._get_tabstop(self, number) + if rv is not None: + return rv + if self._parent and requester is not self._parent: + return self._parent._get_tabstop(self, number) + + def _update(self, done): + if all((child in done) for child in self._children): + assert self not in done + done.add(self) + return True + + def _add_child(self, child): + """Add 'child' as a new child of this text object.""" + self._children.append(child) + self._children.sort() + + def _del_child(self, child): + """Delete this 'child'.""" + child._parent = None + self._children.remove(child) + + # If this is a tabstop, delete it. Might have been deleted already if + # it was nested. + try: + del self._tabstops[child.number] + except (AttributeError, KeyError): + pass + + +class NoneditableTextObject(TextObject): + + """All passive text objects that the user can't edit by hand.""" + + def _update(self, done): + return True diff --git a/pythonx/UltiSnips/text_objects/_escaped_char.py b/pythonx/UltiSnips/text_objects/_escaped_char.py new file mode 100644 index 0000000..3301a26 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_escaped_char.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""See module comment.""" + +from UltiSnips.text_objects._base import NoneditableTextObject + + +class EscapedChar(NoneditableTextObject): + + r""" + This class is a escape char like \$. It is handled in a text object to make + sure that siblings are correctly moved after replacing the text. + + This is a base class without functionality just to mark it in the code. + """ diff --git a/pythonx/UltiSnips/text_objects/_mirror.py b/pythonx/UltiSnips/text_objects/_mirror.py new file mode 100644 index 0000000..7f8c961 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_mirror.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""A Mirror object contains the same text as its related tabstop.""" + +from UltiSnips.text_objects._base import NoneditableTextObject + + +class Mirror(NoneditableTextObject): + + """See module docstring.""" + + def __init__(self, parent, tabstop, token): + NoneditableTextObject.__init__(self, parent, token) + self._ts = tabstop + + def _update(self, done): + if self._ts.is_killed: + self.overwrite('') + self._parent._del_child(self) # pylint:disable=protected-access + return True + + if self._ts not in done: + return False + + self.overwrite(self._get_text()) + return True + + def _get_text(self): + """Returns the text used for mirroring. + + Overwritten by base classes. + + """ + return self._ts.current_text diff --git a/pythonx/UltiSnips/text_objects/_python_code.py b/pythonx/UltiSnips/text_objects/_python_code.py new file mode 100644 index 0000000..dac036e --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_python_code.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Implements `!p ` interpolation.""" + +import os +from collections import namedtuple + +from UltiSnips import _vim +from UltiSnips.compatibility import as_unicode +from UltiSnips.indent_util import IndentUtil +from UltiSnips.text_objects._base import NoneditableTextObject +import UltiSnips.snippet_manager + + +class _Tabs(object): + + """Allows access to tabstop content via t[] inside of python code.""" + + def __init__(self, to): + self._to = to + + def __getitem__(self, no): + ts = self._to._get_tabstop( + self._to, + int(no)) # pylint:disable=protected-access + if ts is None: + return '' + return ts.current_text + +_VisualContent = namedtuple('_VisualContent', ['mode', 'text']) + + +class SnippetUtilForAction(dict): + def __init__(self, *args, **kwargs): + super(SnippetUtilForAction, self).__init__(*args, **kwargs) + self.__dict__ = self + + def expand_anon(self, *args, **kwargs): + UltiSnips.snippet_manager.UltiSnips_Manager.expand_anon( + *args, **kwargs + ) + self.cursor.preserve() + + +class SnippetUtilCursor(object): + def __init__(self, cursor): + self._cursor = [cursor[0] - 1, cursor[1]] + self._set = False + + def preserve(self): + self._set = True + self._cursor = [ + _vim.buf.cursor[0], + _vim.buf.cursor[1], + ] + + def is_set(self): + return self._set + + def set(self, line, column): + self.__setitem__(0, line) + self.__setitem__(1, column) + + def to_vim_cursor(self): + return (self._cursor[0] + 1, self._cursor[1]) + + def __getitem__(self, index): + return self._cursor[index] + + def __setitem__(self, index, value): + self._set = True + self._cursor[index] = value + + def __len__(self): + return 2 + + def __str__(self): + return str((self._cursor[0], self._cursor[1])) + + +class SnippetUtil(object): + + """Provides easy access to indentation, etc. + + This is the 'snip' object in python code. + + """ + + def __init__(self, initial_indent, vmode, vtext, context): + self._ind = IndentUtil() + self._visual = _VisualContent(vmode, vtext) + self._initial_indent = self._ind.indent_to_spaces(initial_indent) + self._reset('') + self._context = context + + def _reset(self, cur): + """Gets the snippet ready for another update. + + :cur: the new value for c. + + """ + self._ind.reset() + self._cur = cur + self._rv = '' + self._changed = False + self.reset_indent() + + def shift(self, amount=1): + """Shifts the indentation level. Note that this uses the shiftwidth + because thats what code formatters use. + + :amount: the amount by which to shift. + + """ + self.indent += ' ' * self._ind.shiftwidth * amount + + def unshift(self, amount=1): + """Unshift the indentation level. Note that this uses the shiftwidth + because thats what code formatters use. + + :amount: the amount by which to unshift. + + """ + by = -self._ind.shiftwidth * amount + try: + self.indent = self.indent[:by] + except IndexError: + self.indent = '' + + def mkline(self, line='', indent=None): + """Creates a properly set up line. + + :line: the text to add + :indent: the indentation to have at the beginning + if None, it uses the default amount + + """ + if indent is None: + indent = self.indent + # this deals with the fact that the first line is + # already properly indented + if '\n' not in self._rv: + try: + indent = indent[len(self._initial_indent):] + except IndexError: + indent = '' + indent = self._ind.spaces_to_indent(indent) + + return indent + line + + def reset_indent(self): + """Clears the indentation.""" + self.indent = self._initial_indent + + # Utility methods + @property + def fn(self): # pylint:disable=no-self-use,invalid-name + """The filename.""" + return _vim.eval('expand("%:t")') or '' + + @property + def basename(self): # pylint:disable=no-self-use + """The filename without extension.""" + return _vim.eval('expand("%:t:r")') or '' + + @property + def ft(self): # pylint:disable=invalid-name + """The filetype.""" + return self.opt('&filetype', '') + + @property + def rv(self): # pylint:disable=invalid-name + """The return value. + + The text to insert at the location of the placeholder. + + """ + return self._rv + + @rv.setter + def rv(self, value): # pylint:disable=invalid-name + """See getter.""" + self._changed = True + self._rv = value + + @property + def _rv_changed(self): + """True if rv has changed.""" + return self._changed + + @property + def c(self): # pylint:disable=invalid-name + """The current text of the placeholder.""" + return self._cur + + @property + def v(self): # pylint:disable=invalid-name + """Content of visual expansions.""" + return self._visual + + @property + def context(self): + return self._context + + def opt(self, option, default=None): # pylint:disable=no-self-use + """Gets a Vim variable.""" + if _vim.eval("exists('%s')" % option) == '1': + try: + return _vim.eval(option) + except _vim.error: + pass + return default + + def __add__(self, value): + """Appends the given line to rv using mkline.""" + self.rv += '\n' # pylint:disable=invalid-name + self.rv += self.mkline(value) + return self + + def __lshift__(self, other): + """Same as unshift.""" + self.unshift(other) + + def __rshift__(self, other): + """Same as shift.""" + self.shift(other) + + +class PythonCode(NoneditableTextObject): + + """See module docstring.""" + + def __init__(self, parent, token): + + # Find our containing snippet for snippet local data + snippet = parent + while snippet: + try: + self._locals = snippet.locals + text = snippet.visual_content.text + mode = snippet.visual_content.mode + context = snippet.context + break + except AttributeError: + snippet = snippet._parent # pylint:disable=protected-access + self._snip = SnippetUtil(token.indent, mode, text, context) + + self._codes = (( + 'import re, os, vim, string, random', + '\n'.join(snippet.globals.get('!p', [])).replace('\r\n', '\n'), + token.code.replace('\\`', '`') + )) + NoneditableTextObject.__init__(self, parent, token) + + def _update(self, done): + path = _vim.eval('expand("%")') or '' + ct = self.current_text + self._locals.update({ + 't': _Tabs(self._parent), + 'fn': os.path.basename(path), + 'path': path, + 'cur': ct, + 'res': ct, + 'snip': self._snip, + }) + self._snip._reset(ct) # pylint:disable=protected-access + + for code in self._codes: + try: + exec(code, self._locals) # pylint:disable=exec-used + except Exception as e: + e.snippet_code = code + raise + + rv = as_unicode( + self._snip.rv if self._snip._rv_changed # pylint:disable=protected-access + else as_unicode(self._locals['res']) + ) + + if ct != rv: + self.overwrite(rv) + return False + return True diff --git a/pythonx/UltiSnips/text_objects/_shell_code.py b/pythonx/UltiSnips/text_objects/_shell_code.py new file mode 100644 index 0000000..a7ad964 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_shell_code.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Implements `echo hi` shell code interpolation.""" + +import os +import platform +from subprocess import Popen, PIPE +import stat +import tempfile + +from UltiSnips.compatibility import as_unicode +from UltiSnips.text_objects._base import NoneditableTextObject + + +def _chomp(string): + """Rather than rstrip(), remove only the last newline and preserve + purposeful whitespace.""" + if len(string) and string[-1] == '\n': + string = string[:-1] + if len(string) and string[-1] == '\r': + string = string[:-1] + return string + + +def _run_shell_command(cmd, tmpdir): + """Write the code to a temporary file.""" + cmdsuf = '' + if platform.system() == 'Windows': + # suffix required to run command on windows + cmdsuf = '.bat' + # turn echo off + cmd = '@echo off\r\n' + cmd + handle, path = tempfile.mkstemp(text=True, dir=tmpdir, suffix=cmdsuf) + os.write(handle, cmd.encode('utf-8')) + os.close(handle) + os.chmod(path, stat.S_IRWXU) + + # Execute the file and read stdout + proc = Popen(path, shell=True, stdout=PIPE, stderr=PIPE) + proc.wait() + stdout, _ = proc.communicate() + os.unlink(path) + return _chomp(as_unicode(stdout)) + + +def _get_tmp(): + """Find an executable tmp directory.""" + userdir = os.path.expanduser('~') + for testdir in [tempfile.gettempdir(), os.path.join(userdir, '.cache'), + os.path.join(userdir, '.tmp'), userdir]: + if (not os.path.exists(testdir) or + not _run_shell_command('echo success', testdir) == 'success'): + continue + return testdir + return '' + + +class ShellCode(NoneditableTextObject): + + """See module docstring.""" + + def __init__(self, parent, token): + NoneditableTextObject.__init__(self, parent, token) + self._code = token.code.replace('\\`', '`') + self._tmpdir = _get_tmp() + + def _update(self, done): + if not self._tmpdir: + output = \ + 'Unable to find executable tmp directory, check noexec on /tmp' + else: + output = _run_shell_command(self._code, self._tmpdir) + self.overwrite(output) + self._parent._del_child(self) # pylint:disable=protected-access + return True diff --git a/pythonx/UltiSnips/text_objects/_snippet_instance.py b/pythonx/UltiSnips/text_objects/_snippet_instance.py new file mode 100644 index 0000000..e3e3158 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_snippet_instance.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""A Snippet instance is an instance of a Snippet Definition. + +That is, when the user expands a snippet, a SnippetInstance is created +to keep track of the corresponding TextObjects. The Snippet itself is +also a TextObject. + +""" + +from UltiSnips import _vim +from UltiSnips.position import Position +from UltiSnips.text_objects._base import EditableTextObject, \ + NoneditableTextObject +from UltiSnips.text_objects._tabstop import TabStop + + +class SnippetInstance(EditableTextObject): + + """See module docstring.""" + # pylint:disable=protected-access + + def __init__(self, snippet, parent, initial_text, + start, end, visual_content, last_re, globals, context): + if start is None: + start = Position(0, 0) + if end is None: + end = Position(0, 0) + self.snippet = snippet + self._cts = 0 + + self.context = context + self.locals = {'match': last_re, 'context': context} + self.globals = globals + self.visual_content = visual_content + + EditableTextObject.__init__(self, parent, start, end, initial_text) + + def replace_initial_text(self): + """Puts the initial text of all text elements into Vim.""" + def _place_initial_text(obj): + """recurses on the children to do the work.""" + obj.overwrite() + if isinstance(obj, EditableTextObject): + for child in obj._children: + _place_initial_text(child) + _place_initial_text(self) + + def replay_user_edits(self, cmds, ctab=None): + """Replay the edits the user has done to keep endings of our Text + objects in sync with reality.""" + for cmd in cmds: + self._do_edit(cmd, ctab) + + def update_textobjects(self): + """Update the text objects that should change automagically after the + users edits have been replayed. + + This might also move the Cursor + + """ + vc = _VimCursor(self) + done = set() + not_done = set() + + def _find_recursive(obj): + """Finds all text objects and puts them into 'not_done'.""" + if isinstance(obj, EditableTextObject): + for child in obj._children: + _find_recursive(child) + not_done.add(obj) + _find_recursive(self) + + counter = 10 + while (done != not_done) and counter: + # Order matters for python locals! + for obj in sorted(not_done - done): + if obj._update(done): + done.add(obj) + counter -= 1 + if not counter: + raise RuntimeError( + 'The snippets content did not converge: Check for Cyclic ' + 'dependencies or random strings in your snippet. You can use ' + "'if not snip.c' to make sure to only expand random output " + 'once.') + vc.to_vim() + self._del_child(vc) + + def select_next_tab(self, backwards=False): + """Selects the next tabstop or the previous if 'backwards' is True.""" + if self._cts is None: + return + + if backwards: + cts_bf = self._cts + + res = self._get_prev_tab(self._cts) + if res is None: + self._cts = cts_bf + return self._tabstops.get(self._cts, None) + self._cts, ts = res + return ts + else: + res = self._get_next_tab(self._cts) + if res is None: + self._cts = None + + ts = self._get_tabstop(self, 0) + if ts: + return ts + + # TabStop 0 was deleted. It was probably killed through some + # edit action. Recreate it at the end of us. + start = Position(self.end.line, self.end.col) + end = Position(self.end.line, self.end.col) + return TabStop(self, 0, start, end) + else: + self._cts, ts = res + return ts + + return self._tabstops[self._cts] + + def _get_tabstop(self, requester, no): + # SnippetInstances are completely self contained, therefore, we do not + # need to ask our parent for Tabstops + cached_parent = self._parent + self._parent = None + rv = EditableTextObject._get_tabstop(self, requester, no) + self._parent = cached_parent + return rv + + def get_tabstops(self): + return self._tabstops + + +class _VimCursor(NoneditableTextObject): + + """Helper class to keep track of the Vim Cursor when text objects expand + and move.""" + + def __init__(self, parent): + NoneditableTextObject.__init__( + self, parent, _vim.buf.cursor, _vim.buf.cursor, + tiebreaker=Position(-1, -1)) + + def to_vim(self): + """Moves the cursor in the Vim to our position.""" + assert self._start == self._end + _vim.buf.cursor = self._start diff --git a/pythonx/UltiSnips/text_objects/_tabstop.py b/pythonx/UltiSnips/text_objects/_tabstop.py new file mode 100644 index 0000000..f113f75 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_tabstop.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""This is the most important TextObject. + +A TabStop is were the cursor comes to rest when the user taps through +the Snippet. + +""" + +from UltiSnips.text_objects._base import EditableTextObject + + +class TabStop(EditableTextObject): + + """See module docstring.""" + + def __init__(self, parent, token, start=None, end=None): + if start is not None: + self._number = token + EditableTextObject.__init__(self, parent, start, end) + else: + self._number = token.number + EditableTextObject.__init__(self, parent, token) + parent._tabstops[ + self._number] = self # pylint:disable=protected-access + + @property + def number(self): + """The tabstop number.""" + return self._number + + @property + def is_killed(self): + """True if this tabstop has been typed over and the user therefore can + no longer jump to it.""" + return self._parent is None + + def __repr__(self): + try: + text = self.current_text + except IndexError: + text = '' + return 'TabStop(%s,%r->%r,%r)' % (self.number, self._start, + self._end, text) diff --git a/pythonx/UltiSnips/text_objects/_transformation.py b/pythonx/UltiSnips/text_objects/_transformation.py new file mode 100644 index 0000000..2099213 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_transformation.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Implements TabStop transformations.""" + +import re +import sys + +from UltiSnips.text import unescape, fill_in_whitespace +from UltiSnips.text_objects._mirror import Mirror + + +def _find_closing_brace(string, start_pos): + """Finds the corresponding closing brace after start_pos.""" + bracks_open = 1 + for idx, char in enumerate(string[start_pos:]): + if char == '(': + if string[idx + start_pos - 1] != '\\': + bracks_open += 1 + elif char == ')': + if string[idx + start_pos - 1] != '\\': + bracks_open -= 1 + if not bracks_open: + return start_pos + idx + 1 + + +def _split_conditional(string): + """Split the given conditional 'string' into its arguments.""" + bracks_open = 0 + args = [] + carg = '' + for idx, char in enumerate(string): + if char == '(': + if string[idx - 1] != '\\': + bracks_open += 1 + elif char == ')': + if string[idx - 1] != '\\': + bracks_open -= 1 + elif char == ':' and not bracks_open and not string[idx - 1] == '\\': + args.append(carg) + carg = '' + continue + carg += char + args.append(carg) + return args + + +def _replace_conditional(match, string): + """Replaces a conditional match in a transformation.""" + conditional_match = _CONDITIONAL.search(string) + while conditional_match: + start = conditional_match.start() + end = _find_closing_brace(string, start + 4) + args = _split_conditional(string[start + 4:end - 1]) + rv = '' + if match.group(int(conditional_match.group(1))): + rv = unescape(_replace_conditional(match, args[0])) + elif len(args) > 1: + rv = unescape(_replace_conditional(match, args[1])) + string = string[:start] + rv + string[end:] + conditional_match = _CONDITIONAL.search(string) + return string + +_ONE_CHAR_CASE_SWITCH = re.compile(r"\\([ul].)", re.DOTALL) +_LONG_CASEFOLDINGS = re.compile(r"\\([UL].*?)\\E", re.DOTALL) +_DOLLAR = re.compile(r"\$(\d+)", re.DOTALL) +_CONDITIONAL = re.compile(r"\(\?(\d+):", re.DOTALL) + + +class _CleverReplace(object): + + """Mimics TextMates replace syntax.""" + + def __init__(self, expression): + self._expression = expression + + def replace(self, match): + """Replaces 'match' through the correct replacement string.""" + transformed = self._expression + # Replace all $? with capture groups + transformed = _DOLLAR.subn( + lambda m: match.group(int(m.group(1))), transformed)[0] + + # Replace Case switches + def _one_char_case_change(match): + """Replaces one character case changes.""" + if match.group(1)[0] == 'u': + return match.group(1)[-1].upper() + else: + return match.group(1)[-1].lower() + transformed = _ONE_CHAR_CASE_SWITCH.subn( + _one_char_case_change, transformed)[0] + + def _multi_char_case_change(match): + """Replaces multi character case changes.""" + if match.group(1)[0] == 'U': + return match.group(1)[1:].upper() + else: + return match.group(1)[1:].lower() + transformed = _LONG_CASEFOLDINGS.subn( + _multi_char_case_change, transformed)[0] + transformed = _replace_conditional(match, transformed) + return unescape(fill_in_whitespace(transformed)) + +# flag used to display only one time the lack of unidecode +UNIDECODE_ALERT_RAISED = False + + +class TextObjectTransformation(object): + + """Base class for Transformations and ${VISUAL}.""" + + def __init__(self, token): + self._convert_to_ascii = False + + self._find = None + if token.search is None: + return + + flags = 0 + self._match_this_many = 1 + if token.options: + if 'g' in token.options: + self._match_this_many = 0 + if 'i' in token.options: + flags |= re.IGNORECASE + if 'a' in token.options: + self._convert_to_ascii = True + + self._find = re.compile(token.search, flags | re.DOTALL) + self._replace = _CleverReplace(token.replace) + + def _transform(self, text): + """Do the actual transform on the given text.""" + global UNIDECODE_ALERT_RAISED # pylint:disable=global-statement + if self._convert_to_ascii: + try: + import unidecode + text = unidecode.unidecode(text) + except Exception: # pylint:disable=broad-except + if UNIDECODE_ALERT_RAISED == False: + UNIDECODE_ALERT_RAISED = True + sys.stderr.write( + 'Please install unidecode python package in order to ' + 'be able to make ascii conversions.\n') + if self._find is None: + return text + return self._find.subn( + self._replace.replace, text, self._match_this_many)[0] + + +class Transformation(Mirror, TextObjectTransformation): + + """See module docstring.""" + + def __init__(self, parent, ts, token): + Mirror.__init__(self, parent, ts, token) + TextObjectTransformation.__init__(self, token) + + def _get_text(self): + return self._transform(self._ts.current_text) diff --git a/pythonx/UltiSnips/text_objects/_viml_code.py b/pythonx/UltiSnips/text_objects/_viml_code.py new file mode 100644 index 0000000..86329bf --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_viml_code.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Implements `!v ` VimL interpolation.""" + +from UltiSnips import _vim +from UltiSnips.text_objects._base import NoneditableTextObject + + +class VimLCode(NoneditableTextObject): + + """See module docstring.""" + + def __init__(self, parent, token): + self._code = token.code.replace('\\`', '`').strip() + + NoneditableTextObject.__init__(self, parent, token) + + def _update(self, done): + self.overwrite(_vim.eval(self._code)) + return True diff --git a/pythonx/UltiSnips/text_objects/_visual.py b/pythonx/UltiSnips/text_objects/_visual.py new file mode 100644 index 0000000..0be64b3 --- /dev/null +++ b/pythonx/UltiSnips/text_objects/_visual.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""A ${VISUAL} placeholder that will use the text that was last visually +selected and insert it here. + +If there was no text visually selected, this will be the empty string. + +""" + +import re +import textwrap + +from UltiSnips import _vim +from UltiSnips.indent_util import IndentUtil +from UltiSnips.text_objects._transformation import TextObjectTransformation +from UltiSnips.text_objects._base import NoneditableTextObject + +_REPLACE_NON_WS = re.compile(r"[^ \t]") + + +class Visual(NoneditableTextObject, TextObjectTransformation): + + """See module docstring.""" + + def __init__(self, parent, token): + # Find our containing snippet for visual_content + snippet = parent + while snippet: + try: + self._text = snippet.visual_content.text + self._mode = snippet.visual_content.mode + break + except AttributeError: + snippet = snippet._parent # pylint:disable=protected-access + if not self._text: + self._text = token.alternative_text + self._mode = 'v' + + NoneditableTextObject.__init__(self, parent, token) + TextObjectTransformation.__init__(self, token) + + def _update(self, done): + if self._mode == 'v': # Normal selection. + text = self._text + else: # Block selection or line selection. + text_before = _vim.buf[self.start.line][:self.start.col] + indent = _REPLACE_NON_WS.sub(' ', text_before) + iu = IndentUtil() + indent = iu.indent_to_spaces(indent) + indent = iu.spaces_to_indent(indent) + text = '' + for idx, line in enumerate(textwrap.dedent( + self._text).splitlines(True)): + if idx != 0: + text += indent + text += line + text = text[:-1] # Strip final '\n' + + text = self._transform(text) + self.overwrite(text) + self._parent._del_child(self) # pylint:disable=protected-access + + return True diff --git a/pythonx/UltiSnips/vim_state.py b/pythonx/UltiSnips/vim_state.py new file mode 100644 index 0000000..4d15842 --- /dev/null +++ b/pythonx/UltiSnips/vim_state.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Some classes to conserve Vim's state for comparing over time.""" + +from collections import deque + +from UltiSnips import _vim +from UltiSnips.compatibility import as_unicode, byte2col +from UltiSnips.position import Position + + +class VimPosition(Position): + + """Represents the current position in the buffer, together with some status + variables that might change our decisions down the line.""" + + def __init__(self): + pos = _vim.buf.cursor + self._mode = _vim.eval('mode()') + Position.__init__(self, pos.line, pos.col) + + @property + def mode(self): + """Returns the mode() this position was created.""" + return self._mode + + +class VimState(object): + + """Caches some state information from Vim to better guess what editing + tasks the user might have done in the last step.""" + + def __init__(self): + self._poss = deque(maxlen=5) + self._lvb = None + + self._text_to_expect = '' + self._unnamed_reg_cached = False + + # We store the cached value of the unnamed register in Vim directly to + # avoid any Unicode issues with saving and restoring the unnamed + # register across the Python bindings. The unnamed register can contain + # data that cannot be coerced to Unicode, and so a simple vim.eval('@"') + # fails badly. Keeping the cached value in Vim directly, sidesteps the + # problem. + _vim.command('let g:_ultisnips_unnamed_reg_cache = ""') + + def remember_unnamed_register(self, text_to_expect): + """Save the unnamed register. + + 'text_to_expect' is text that we expect + to be contained in the register the next time this method is called - + this could be text from the tabstop that was selected and might have + been overwritten. We will not cache that then. + + """ + self._unnamed_reg_cached = True + escaped_text = self._text_to_expect.replace("'", "''") + res = int(_vim.eval('@" != ' + "'" + escaped_text + "'")) + if res: + _vim.command('let g:_ultisnips_unnamed_reg_cache = @"') + self._text_to_expect = text_to_expect + + def restore_unnamed_register(self): + """Restores the unnamed register and forgets what we cached.""" + if not self._unnamed_reg_cached: + return + _vim.command('let @" = g:_ultisnips_unnamed_reg_cache') + self._unnamed_reg_cached = False + + def remember_position(self): + """Remember the current position as a previous pose.""" + self._poss.append(VimPosition()) + + def remember_buffer(self, to): + """Remember the content of the buffer and the position.""" + self._lvb = _vim.buf[to.start.line:to.end.line + 1] + self._lvb_len = len(_vim.buf) + self.remember_position() + + @property + def diff_in_buffer_length(self): + """Returns the difference in the length of the current buffer compared + to the remembered.""" + return len(_vim.buf) - self._lvb_len + + @property + def pos(self): + """The last remembered position.""" + return self._poss[-1] + + @property + def ppos(self): + """The second to last remembered position.""" + return self._poss[-2] + + @property + def remembered_buffer(self): + """The content of the remembered buffer.""" + return self._lvb[:] + + +class VisualContentPreserver(object): + + """Saves the current visual selection and the selection mode it was done in + (e.g. line selection, block selection or regular selection.)""" + + def __init__(self): + self.reset() + + def reset(self): + """Forget the preserved state.""" + self._mode = '' + self._text = as_unicode('') + + def conserve(self): + """Save the last visual selection ond the mode it was made in.""" + sl, sbyte = map(int, + (_vim.eval("""line("'<")"""), _vim.eval("""col("'<")"""))) + el, ebyte = map(int, + (_vim.eval("""line("'>")"""), _vim.eval("""col("'>")"""))) + sc = byte2col(sl, sbyte - 1) + ec = byte2col(el, ebyte - 1) + self._mode = _vim.eval('visualmode()') + + _vim_line_with_eol = lambda ln: _vim.buf[ln] + '\n' + + if sl == el: + text = _vim_line_with_eol(sl - 1)[sc:ec + 1] + else: + text = _vim_line_with_eol(sl - 1)[sc:] + for cl in range(sl, el - 1): + text += _vim_line_with_eol(cl) + text += _vim_line_with_eol(el - 1)[:ec + 1] + self._text = text + + @property + def text(self): + """The conserved text.""" + return self._text + + @property + def mode(self): + """The conserved visualmode().""" + return self._mode diff --git a/rplugin/python3/deoplete/sources/ultisnips.py b/rplugin/python3/deoplete/sources/ultisnips.py new file mode 100644 index 0000000..943b3aa --- /dev/null +++ b/rplugin/python3/deoplete/sources/ultisnips.py @@ -0,0 +1,20 @@ +from .base import Base + +class Source(Base): + def __init__(self, vim): + Base.__init__(self, vim) + + self.name = 'ultisnips' + self.mark = '[US]' + self.rank = 8 + + def gather_candidates(self, context): + suggestions = [] + snippets = self.vim.eval( + 'UltiSnips#SnippetsInCurrentScope()') + for trigger in snippets: + suggestions.append({ + 'word': trigger, + 'menu': self.mark + ' ' + snippets.get(trigger, '') + }) + return suggestions diff --git a/syntax/snippets.vim b/syntax/snippets.vim new file mode 100644 index 0000000..3a044ce --- /dev/null +++ b/syntax/snippets.vim @@ -0,0 +1,217 @@ +" Syntax highlighting for snippet files (used for UltiSnips.vim) +" Revision: 26/03/11 19:53:33 + +if exists("b:current_syntax") + finish +endif + +if expand("%:p:h:t") == "snippets" && search("^endsnippet", "nw") == 0 + \ && !exists("b:ultisnips_override_snipmate") + " this appears to be a snipmate file + " It's in a directory called snippets/ and there's no endsnippet keyword + " anywhere in the file. + source :h/snippets_snipmate.vim + finish +endif + +" Embedded Syntaxes {{{1 + +try + syntax include @Python syntax/python.vim + unlet b:current_syntax + syntax include @Viml syntax/vim.vim + unlet b:current_syntax + syntax include @Shell syntax/sh.vim + unlet b:current_syntax +catch /E403/ + " Ignore errors about syntax files that can't be loaded more than once +endtry + +" Syntax definitions {{{1 + +" Comments {{{2 + +syn match snipComment "^#.*" contains=snipTODO display +syn keyword snipTODO contained display FIXME NOTE NOTES TODO XXX + +" Errors {{{2 + +syn match snipLeadingSpaces "^\t* \+" contained + +" Extends {{{2 + +syn match snipExtends "^extends\%(\s.*\|$\)" contains=snipExtendsKeyword display +syn match snipExtendsKeyword "^extends" contained display + +" Definitions {{{2 + +" snippet {{{3 + +syn region snipSnippet start="^snippet\_s" end="^endsnippet\s*$" contains=snipSnippetHeader fold keepend +syn match snipSnippetHeader "^.*$" nextgroup=snipSnippetBody,snipSnippetFooter skipnl contained contains=snipSnippetHeaderKeyword +syn match snipSnippetHeaderKeyword "^snippet" contained nextgroup=snipSnippetTrigger skipwhite +syn region snipSnippetBody start="\_." end="^\zeendsnippet\s*$" contained nextgroup=snipSnippetFooter contains=snipLeadingSpaces,@snipTokens +syn match snipSnippetFooter "^endsnippet.*" contained contains=snipSnippetFooterKeyword +syn match snipSnippetFooterKeyword "^endsnippet" contained + +" The current parser is a bit lax about parsing. For example, given this: +" snippet foo"bar" +" it treats `foo"bar"` as the trigger. But with this: +" snippet foo"bar baz" +" it treats `foo` as the trigger and "bar baz" as the description. +" I think this is an accident. Instead, we'll assume the description must +" be surrounded by spaces. That means we'll treat +" snippet foo"bar" +" as a trigger `foo"bar"` and +" snippet foo"bar baz" +" as an attempted multiword snippet `foo"bar baz"` that is invalid. +" NB: UltiSnips parses right-to-left, which Vim doesn't support, so that makes +" the following patterns very complicated. +syn match snipSnippetTrigger "\S\+" contained nextgroup=snipSnippetDocString,snipSnippetTriggerInvalid skipwhite +" We want to match a trailing " as the start of a doc comment, but we also +" want to allow for using " as the delimiter in a multiword/pattern snippet. +" So we have to define this twice, once in the general case that matches a +" trailing " as the doc comment, and once for the case of the multiword +" delimiter using " that has more constraints +syn match snipSnippetTrigger ,\([^"[:space:]]\).\{-}\1\%(\s*$\)\@!\ze\%(\s\+"[^"]*\%("\s\+[^"[:space:]]\+\|"\)\=\)\=\s*$, contained nextgroup=snipSnippetDocString skipwhite +syn match snipSnippetTrigger ,".\{-}"\ze\%(\s\+"\%(\s*\S\)\@=[^"]*\%("\s\+[^"[:space:]]\+\|"\)\=\)\=\s*$, contained nextgroup=snipSnippetDocString skipwhite +syn match snipSnippetTriggerInvalid ,\S\@=.\{-}\S\ze\%(\s\+"[^"]*\%("\s\+[^"[:space:]]\+\s*\|"\s*\)\=\|\s*\)$, contained nextgroup=snipSnippetDocString skipwhite +syn match snipSnippetDocString ,"[^"]*\%("\ze\s*\%(\s[^"[:space:]]\+\s*\)\=\)\=$, contained nextgroup=snipSnippetOptions skipwhite +syn match snipSnippetOptions ,\S\+, contained contains=snipSnippetOptionFlag +syn match snipSnippetOptionFlag ,[biwrtsmx], contained + +" Command substitution {{{4 + +syn region snipCommand keepend matchgroup=snipCommandDelim start="`" skip="\\[{}\\$`]" end="`" contained contains=snipPythonCommand,snipVimLCommand,snipShellCommand,snipCommandSyntaxOverride +syn region snipShellCommand start="\ze\_." skip="\\[{}\\$`]" end="\ze`" contained contains=@Shell +syn region snipPythonCommand matchgroup=snipPythonCommandP start="`\@<=!p\_s" skip="\\[{}\\$`]" end="\ze`" contained contains=@Python +syn region snipVimLCommand matchgroup=snipVimLCommandV start="`\@<=!v\_s" skip="\\[{}\\$`]" end="\ze`" contained contains=@Viml +syn cluster snipTokens add=snipCommand +syn cluster snipTabStopTokens add=snipCommand + +" unfortunately due to the balanced braces parsing of commands, if a { occurs +" in the command, we need to prevent the embedded syntax highlighting. +" Otherwise, we can't track the balanced braces properly. + +syn region snipCommandSyntaxOverride start="\%(\\[{}\\$`]\|\_[^`"{]\)*\ze{" skip="\\[{}\\$`]" end="\ze`" contained contains=snipBalancedBraces transparent + +" Tab Stops {{{4 + +syn match snipEscape "\\[{}\\$`]" contained +syn cluster snipTokens add=snipEscape +syn cluster snipTabStopTokens add=snipEscape + +syn match snipMirror "\$\d\+" contained +syn cluster snipTokens add=snipMirror +syn cluster snipTabStopTokens add=snipMirror + +syn region snipTabStop matchgroup=snipTabStop start="\${\d\+[:}]\@=" end="}" contained contains=snipTabStopDefault extend +syn region snipTabStopDefault matchgroup=snipTabStop start=":" skip="\\[{}]" end="\ze}" contained contains=snipTabStopEscape,snipBalancedBraces,@snipTabStopTokens keepend +syn match snipTabStopEscape "\\[{}]" contained +syn region snipBalancedBraces start="{" end="}" contained transparent extend +syn cluster snipTokens add=snipTabStop +syn cluster snipTabStopTokens add=snipTabStop + +syn region snipVisual matchgroup=snipVisual start="\${VISUAL[:}/]\@=" end="}" contained contains=snipVisualDefault,snipTransformationPattern +syn region snipVisualDefault matchgroup=snipVisual start=":" end="\ze[}/]" contained contains=snipTabStopEscape nextgroup=snipTransformationPattern +syn cluster snipTokens add=snipVisual +syn cluster snipTabStopTokens add=snipVisual + +syn region snipTransformation matchgroup=snipTransformation start="\${\d\/\@=" end="}" contained contains=snipTransformationPattern +syn region snipTransformationPattern matchgroup=snipTransformationPatternDelim start="/" end="\ze/" contained contains=snipTransformationEscape nextgroup=snipTransformationReplace skipnl +syn region snipTransformationReplace matchgroup=snipTransformationPatternDelim start="/" end="/" contained contains=snipTransformationEscape nextgroup=snipTransformationOptions skipnl +syn region snipTransformationOptions start="\ze[^}]" end="\ze}" contained contains=snipTabStopEscape +syn match snipTransformationEscape "\\/" contained +syn cluster snipTokens add=snipTransformation +syn cluster snipTabStopTokens add=snipTransformation + +" global {{{3 + +" Generic (non-Python) {{{4 + +syn region snipGlobal start="^global\_s" end="^\zeendglobal\s*$" contains=snipGlobalHeader nextgroup=snipGlobalFooter fold keepend +syn match snipGlobalHeader "^.*$" nextgroup=snipGlobalBody,snipGlobalFooter skipnl contained contains=snipGlobalHeaderKeyword +syn region snipGlobalBody start="\_." end="^\zeendglobal\s*$" contained contains=snipLeadingSpaces + +" Python (!p) {{{4 + +syn region snipGlobal start=,^global\s\+!p\%(\s\+"[^"]*\%("\s\+[^"[:space:]]\+\|"\)\=\)\=\s*$, end=,^\zeendglobal\s*$, contains=snipGlobalPHeader nextgroup=snipGlobalFooter fold keepend +syn match snipGlobalPHeader "^.*$" nextgroup=snipGlobalPBody,snipGlobalFooter skipnl contained contains=snipGlobalHeaderKeyword +syn match snipGlobalHeaderKeyword "^global" contained nextgroup=snipSnippetTrigger skipwhite +syn region snipGlobalPBody start="\_." end="^\zeendglobal\s*$" contained contains=@Python + +" Common {{{4 + +syn match snipGlobalFooter "^endglobal.*" contained contains=snipGlobalFooterKeyword +syn match snipGlobalFooterKeyword "^endglobal" contained + +" priority {{{3 + +syn match snipPriority "^priority\%(\s.*\|$\)" contains=snipPriorityKeyword display +syn match snipPriorityKeyword "^priority" contained nextgroup=snipPriorityValue skipwhite display +syn match snipPriorityValue "-\?\d\+" contained display + +" Actions {{{3 + +syn match snipAction "^\(pre_expand\|post_expand\|post_jump\)\%(\s.*\|$\)" contains=snipActionKeyword display +syn match snipActionKeyword "^\(pre_expand\|post_expand\|post_jump\)" contained nextgroup=snipActionValue skipwhite display +syn match snipActionValue '".*"' contained display + +" Snippt Clearing {{{2 + +syn match snipClear "^clearsnippets\%(\s.*\|$\)" contains=snipClearKeyword display +syn match snipClearKeyword "^clearsnippets" contained display + +" Highlight groups {{{1 + +hi def link snipComment Comment +hi def link snipTODO Todo +hi def snipLeadingSpaces term=reverse ctermfg=15 ctermbg=4 gui=reverse guifg=#dc322f + +hi def link snipKeyword Keyword + +hi def link snipExtendsKeyword snipKeyword + +hi def link snipSnippetHeaderKeyword snipKeyword +hi def link snipSnippetFooterKeyword snipKeyword + +hi def link snipSnippetTrigger Identifier +hi def link snipSnippetTriggerInvalid Error +hi def link snipSnippetDocString String +hi def link snipSnippetOptionFlag Special + +hi def link snipGlobalHeaderKeyword snipKeyword +hi def link snipGlobalFooterKeyword snipKeyword + +hi def link snipCommand Special +hi def link snipCommandDelim snipCommand +hi def link snipShellCommand snipCommand +hi def link snipVimLCommand snipCommand +hi def link snipPythonCommandP PreProc +hi def link snipVimLCommandV PreProc + +hi def link snipEscape Special +hi def link snipMirror StorageClass +hi def link snipTabStop Define +hi def link snipTabStopDefault String +hi def link snipTabStopEscape Special +hi def link snipVisual snipTabStop +hi def link snipVisualDefault snipTabStopDefault +hi def link snipTransformation snipTabStop +hi def link snipTransformationPattern String +hi def link snipTransformationPatternDelim Operator +hi def link snipTransformationReplace String +hi def link snipTransformationEscape snipEscape +hi def link snipTransformationOptions Operator + +hi def link snipPriorityKeyword Keyword +hi def link snipPriorityValue Number + +hi def link snipActionKeyword Keyword +hi def link snipActionValue String + +hi def link snipClearKeyword Keyword + +" }}}1 + +let b:current_syntax = "snippets" diff --git a/syntax/snippets_snipmate.vim b/syntax/snippets_snipmate.vim new file mode 100644 index 0000000..aa55c1f --- /dev/null +++ b/syntax/snippets_snipmate.vim @@ -0,0 +1,47 @@ +" Syntax highlighting variant used for snipmate snippets files +" The snippets.vim file sources this if it wants snipmate mode + +if exists("b:current_syntax") + finish +endif + +" Embedded syntaxes {{{1 + +" Re-include the original file so we can share some of its definitions +let b:ultisnips_override_snipmate = 1 +syn include :h/snippets.vim +unlet b:current_syntax +unlet b:ultisnips_override_snipmate + +syn cluster snipTokens contains=snipEscape,snipVisual,snipTabStop,snipMirror,snipmateCommand +syn cluster snipTabStopTokens contains=snipVisual,snipMirror,snipEscape,snipmateCommand + +" Syntax definitions {{{1 + +syn match snipmateComment "^#.*" + +syn match snipmateExtends "^extends\%(\s.*\|$\)" contains=snipExtendsKeyword display + +syn region snipmateSnippet start="^snippet\ze\%(\s\|$\)" end="^\ze[^[:tab:]]" contains=snipmateSnippetHeader keepend +syn match snipmateSnippetHeader "^.*" contained contains=snipmateKeyword nextgroup=snipmateSnippetBody skipnl skipempty +syn match snipmateKeyword "^snippet\ze\%(\s\|$\)" contained nextgroup=snipmateTrigger skipwhite +syn match snipmateTrigger "\S\+" contained nextgroup=snipmateDescription skipwhite +syn match snipmateDescription "\S.*" contained +syn region snipmateSnippetBody start="^\t" end="^\ze[^[:tab:]]" contained contains=@snipTokens + +syn region snipmateCommand keepend matchgroup=snipCommandDelim start="`" skip="\\[{}\\$`]" end="`" contained contains=snipCommandSyntaxOverride,@Viml + +" Highlight groups {{{1 + +hi def link snipmateComment snipComment + +hi def link snipmateSnippet snipSnippet +hi def link snipmateKeyword snipKeyword +hi def link snipmateTrigger snipSnippetTrigger +hi def link snipmateDescription snipSnippetDocString + +hi def link snipmateCommand snipCommand + +" }}}1 + +let b:current_syntax = "snippets" diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/constant.py b/test/constant.py new file mode 100644 index 0000000..0e795e0 --- /dev/null +++ b/test/constant.py @@ -0,0 +1,24 @@ +import sys + +# Some constants for better reading +BS = '\x7f' +ESC = '\x1b' +ARR_L = '\x1bOD' +ARR_R = '\x1bOC' +ARR_U = '\x1bOA' +ARR_D = '\x1bOB' + +# multi-key sequences generating a single key press +SEQUENCES = [ARR_L, ARR_R, ARR_U, ARR_D] + +# Defined Constants +JF = '?' # Jump forwards +JB = '+' # Jump backwards +LS = '@' # List snippets +EX = '\t' # EXPAND +EA = '#' # Expand anonymous + +COMPL_KW = chr(24) + chr(14) +COMPL_ACCEPT = chr(25) + +PYTHON3 = sys.version_info >= (3, 0) diff --git a/test/test_AnonymousExpansion.py b/test/test_AnonymousExpansion.py new file mode 100644 index 0000000..f74d3ff --- /dev/null +++ b/test/test_AnonymousExpansion.py @@ -0,0 +1,67 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Anonymous Expansion {{{# + + +class _AnonBase(_VimTest): + args = '' + + def _extra_vim_config(self, vim_config): + vim_config.append('inoremap %s =UltiSnips#Anon(%s)' + % (EA, self.args)) + + +class Anon_NoTrigger_Simple(_AnonBase): + args = '"simple expand"' + keys = 'abc' + EA + wanted = 'abcsimple expand' + + +class Anon_NoTrigger_AfterSpace(_AnonBase): + args = '"simple expand"' + keys = 'abc ' + EA + wanted = 'abc simple expand' + + +class Anon_NoTrigger_BeginningOfLine(_AnonBase): + args = r"':latex:\`$1\`$0'" + keys = EA + 'Hello' + JF + 'World' + wanted = ':latex:`Hello`World' + + +class Anon_NoTrigger_FirstCharOfLine(_AnonBase): + args = r"':latex:\`$1\`$0'" + keys = ' ' + EA + 'Hello' + JF + 'World' + wanted = ' :latex:`Hello`World' + + +class Anon_NoTrigger_Multi(_AnonBase): + args = '"simple $1 expand $1 $0"' + keys = 'abc' + EA + '123' + JF + '456' + wanted = 'abcsimple 123 expand 123 456' + + +class Anon_Trigger_Multi(_AnonBase): + args = '"simple $1 expand $1 $0", "abc"' + keys = '123 abc' + EA + '123' + JF + '456' + wanted = '123 simple 123 expand 123 456' + + +class Anon_Trigger_Simple(_AnonBase): + args = '"simple expand", "abc"' + keys = 'abc' + EA + wanted = 'simple expand' + + +class Anon_Trigger_Twice(_AnonBase): + args = '"simple expand", "abc"' + keys = 'abc' + EA + '\nabc' + EX + wanted = 'simple expand\nabc' + EX + + +class Anon_Trigger_Opts(_AnonBase): + args = '"simple expand", ".*abc", "desc", "r"' + keys = 'blah blah abc' + EA + wanted = 'simple expand' +# End: Anonymous Expansion #}}} diff --git a/test/test_Autocommands.py b/test/test_Autocommands.py new file mode 100644 index 0000000..bcb6a56 --- /dev/null +++ b/test/test_Autocommands.py @@ -0,0 +1,31 @@ +# encoding: utf-8 +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Autocommands {{{# + +class Autocommands(_VimTest): + snippets = ('test', '[ ${1:foo} ]') + args = '' + keys = 'test' + EX + 'test' + EX + 'bar' + JF + JF + ' done ' + ESC + \ + ':execute "normal aM" . g:mapper_call_count . "\"' + "\n" + \ + ':execute "normal aU" . g:unmapper_call_count . "\"' + "\n" + wanted = '[ [ bar ] ] done M1U1' + + def _extra_vim_config(self, vim_config): + vim_config.append('let g:mapper_call_count = 0') + vim_config.append('function! CustomMapper()') + vim_config.append(' let g:mapper_call_count += 1') + vim_config.append('endfunction') + + vim_config.append('let g:unmapper_call_count = 0') + vim_config.append('function! CustomUnmapper()') + vim_config.append(' let g:unmapper_call_count += 1') + vim_config.append('endfunction') + + vim_config.append('autocmd! User UltiSnipsEnterFirstSnippet') + vim_config.append('autocmd User UltiSnipsEnterFirstSnippet call CustomMapper()') + vim_config.append('autocmd! User UltiSnipsExitLastSnippet') + vim_config.append('autocmd User UltiSnipsExitLastSnippet call CustomUnmapper()') + +# end: Autocommands #}}} diff --git a/test/test_Autotrigger.py b/test/test_Autotrigger.py new file mode 100644 index 0000000..86a1e2b --- /dev/null +++ b/test/test_Autotrigger.py @@ -0,0 +1,69 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +import subprocess + + +def has_patch(version, executable): + output = subprocess.check_output([executable, "--version"]) + patch = 1 + for line in output.decode('utf-8').split("\n"): + if line.startswith("Included patches:"): + patch = line.split('-')[1] + + return int(patch) >= version + + +def check_required_vim_version(test): + if test.vim_flavor == 'neovim': + return None + if not has_patch(214, test.vim._vim_executable): + return 'Vim newer than 7.4.214 is required' + else: + return None + + +class Autotrigger_CanMatchSimpleTrigger(_VimTest): + skip_if = check_required_vim_version + files = { 'us/all.snippets': r""" + snippet a "desc" A + autotriggered + endsnippet + """} + keys = 'a' + wanted = 'autotriggered' + + +class Autotrigger_CanMatchContext(_VimTest): + skip_if = check_required_vim_version + files = { 'us/all.snippets': r""" + snippet a "desc" "snip.line == 2" Ae + autotriggered + endsnippet + """} + keys = 'a\na' + wanted = 'autotriggered\na' + + +class Autotrigger_CanExpandOnTriggerWithLengthMoreThanOne(_VimTest): + skip_if = check_required_vim_version + files = { 'us/all.snippets': r""" + snippet abc "desc" A + autotriggered + endsnippet + """} + keys = 'abc' + wanted = 'autotriggered' + + +class Autotrigger_WillProduceNoExceptionWithVimLowerThan214(_VimTest): + skip_if = lambda self: 'Vim older than 7.4.214 is required' \ + if has_patch(214, self.vim._vim_executable) else None + + files = { 'us/all.snippets': r""" + snippet abc "desc" A + autotriggered + endsnippet + """} + keys = 'abc' + wanted = 'abc' diff --git a/test/test_Chars.py b/test/test_Chars.py new file mode 100644 index 0000000..93d81ba --- /dev/null +++ b/test/test_Chars.py @@ -0,0 +1,254 @@ +# encoding: utf-8 +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * +from test.util import running_on_windows + +# Quotes in Snippets {{{# +# Test for Bug #774917 + + +def _snip_quote(qt): + return ( + ('te' + qt + 'st', 'Expand me' + qt + '!', 'test: ' + qt), + ('te', 'Bad', ''), + ) + + +class Snippet_With_SingleQuote(_VimTest): + snippets = _snip_quote("'") + keys = "te'st" + EX + wanted = "Expand me'!" + + +class Snippet_With_SingleQuote_List(_VimTest): + snippets = _snip_quote("'") + keys = 'te' + LS + '2\n' + wanted = "Expand me'!" + + +class Snippet_With_DoubleQuote(_VimTest): + snippets = _snip_quote('"') + keys = 'te"st' + EX + wanted = "Expand me\"!" + + +class Snippet_With_DoubleQuote_List(_VimTest): + snippets = _snip_quote('"') + keys = 'te' + LS + '2\n' + wanted = "Expand me\"!" + +# End: Quotes in Snippets #}}} + +# Trailing whitespace {{{# + + +class RemoveTrailingWhitespace(_VimTest): + snippets = ('test', """Hello\t ${1:default}\n$2""", '', 's') + wanted = """Hello\nGoodbye""" + keys = 'test' + EX + BS + JF + 'Goodbye' + +class TrimSpacesAtEndOfLines(_VimTest): + snippets = ('test', """next line\n\nshould be empty""", '', 'm') + wanted = """\tnext line\n\n\tshould be empty""" + keys = '\ttest' + EX + +class DoNotTrimSpacesAtEndOfLinesByDefault(_VimTest): + snippets = ('test', """next line\n\nshould be empty""", '', '') + wanted = """\tnext line\n\t\n\tshould be empty""" + keys = '\ttest' + EX + + +class LeaveTrailingWhitespace(_VimTest): + snippets = ('test', """Hello \t ${1:default}\n$2""") + wanted = """Hello \t \nGoodbye""" + keys = 'test' + EX + BS + JF + 'Goodbye' +# End: Trailing whitespace #}}} + +# Newline in default text {{{# +# Tests for bug 616315 # + + +class TrailingNewline_TabStop_NLInsideStuffBehind(_VimTest): + snippets = ('test', r""" +x${1: +}<-behind1 +$2<-behind2""") + keys = 'test' + EX + 'j' + JF + 'k' + wanted = """ +xj<-behind1 +k<-behind2""" + + +class TrailingNewline_TabStop_JustNL(_VimTest): + snippets = ('test', r""" +x${1: +} +$2""") + keys = 'test' + EX + 'j' + JF + 'k' + wanted = """ +xj +k""" + + +class TrailingNewline_TabStop_EndNL(_VimTest): + snippets = ('test', r""" +x${1:a +} +$2""") + keys = 'test' + EX + 'j' + JF + 'k' + wanted = """ +xj +k""" + + +class TrailingNewline_TabStop_StartNL(_VimTest): + snippets = ('test', r""" +x${1: +a} +$2""") + keys = 'test' + EX + 'j' + JF + 'k' + wanted = """ +xj +k""" + + +class TrailingNewline_TabStop_EndStartNL(_VimTest): + snippets = ('test', r""" +x${1: +a +} +$2""") + keys = 'test' + EX + 'j' + JF + 'k' + wanted = """ +xj +k""" + + +class TrailingNewline_TabStop_NotEndStartNL(_VimTest): + snippets = ('test', r""" +x${1:a +a} +$2""") + keys = 'test' + EX + 'j' + JF + 'k' + wanted = """ +xj +k""" + + +class TrailingNewline_TabStop_ExtraNL_ECR(_VimTest): + snippets = ('test', r""" +x${1:a +a} +$2 +""") + keys = 'test' + EX + 'j' + JF + 'k' + wanted = """ +xj +k +""" + + +class _MultiLineDefault(_VimTest): + snippets = ('test', r""" +x${1:a +b +c +d +e +f} +$2""") + + +class MultiLineDefault_Jump(_MultiLineDefault): + keys = 'test' + EX + JF + 'y' + wanted = """ +xa +b +c +d +e +f +y""" + + +class MultiLineDefault_Type(_MultiLineDefault): + keys = 'test' + EX + 'z' + JF + 'y' + wanted = """ +xz +y""" + + +class MultiLineDefault_BS(_MultiLineDefault): + keys = 'test' + EX + BS + JF + 'y' + wanted = """ +x +y""" + +# End: Newline in default text #}}} + +# Umlauts and Special Chars {{{# + + +class _UmlautsBase(_VimTest): + # SendKeys can't send UTF characters + skip_if = lambda self: running_on_windows() + + +class Snippet_With_Umlauts_List(_UmlautsBase): + snippets = _snip_quote('ü') + keys = 'te' + LS + '2\n' + wanted = 'Expand meü!' + + +class Snippet_With_Umlauts(_UmlautsBase): + snippets = _snip_quote('ü') + keys = 'teüst' + EX + wanted = 'Expand meü!' + + +class Snippet_With_Umlauts_TypeOn(_UmlautsBase): + snippets = ('ül', 'üüüüüßßßß') + keys = 'te ül' + EX + 'more text' + wanted = 'te üüüüüßßßßmore text' + + +class Snippet_With_Umlauts_OverwriteFirst(_UmlautsBase): + snippets = ('ül', 'üü ${1:world} üü ${2:hello}ßß\nüüüü') + keys = 'te ül' + EX + 'more text' + JF + JF + 'end' + wanted = 'te üü more text üü helloßß\nüüüüend' + + +class Snippet_With_Umlauts_OverwriteSecond(_UmlautsBase): + snippets = ('ül', 'üü ${1:world} üü ${2:hello}ßß\nüüüü') + keys = 'te ül' + EX + JF + 'more text' + JF + 'end' + wanted = 'te üü world üü more textßß\nüüüüend' + + +class Snippet_With_Umlauts_OverwriteNone(_UmlautsBase): + snippets = ('ül', 'üü ${1:world} üü ${2:hello}ßß\nüüüü') + keys = 'te ül' + EX + JF + JF + 'end' + wanted = 'te üü world üü helloßß\nüüüüend' + + +class Snippet_With_Umlauts_Mirrors(_UmlautsBase): + snippets = ('ül', 'üü ${1:world} üü $1') + keys = 'te ül' + EX + 'hello' + wanted = 'te üü hello üü hello' + + +class Snippet_With_Umlauts_Python(_UmlautsBase): + snippets = ('ül', 'üü ${1:world} üü `!p snip.rv = len(t[1])*"a"`') + keys = 'te ül' + EX + 'hüüll' + wanted = 'te üü hüüll üü aaaaa' + +class UmlautsBeforeTriggerAndCharsAfter(_UmlautsBase): + snippets = ('trig', 'success') + keys = 'ööuu trig b' + 2 * ARR_L + EX + wanted = 'ööuu success b' + +class NoUmlautsBeforeTriggerAndCharsAfter(_UmlautsBase): + snippets = ('trig', 'success') + keys = 'oouu trig b' + 2 * ARR_L + EX + wanted = 'oouu success b' + +# End: Umlauts and Special Chars #}}} diff --git a/test/test_Completion.py b/test/test_Completion.py new file mode 100644 index 0000000..0d95c61 --- /dev/null +++ b/test/test_Completion.py @@ -0,0 +1,34 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Tab Completion of Words {{{# + + +class Completion_SimpleExample_ECR(_VimTest): + snippets = ('test', '$1 ${1:blah}') + keys = 'superkallifragilistik\ntest' + EX + 'sup' + COMPL_KW + \ + COMPL_ACCEPT + ' some more' + wanted = 'superkallifragilistik\nsuperkallifragilistik some more ' \ + 'superkallifragilistik some more' + +# We need >2 different words with identical starts to create the +# popup-menu: +COMPLETION_OPTIONS = 'completion1\ncompletion2\n' + + +class Completion_ForwardsJumpWithoutCOMPL_ACCEPT(_VimTest): + # completions should not be truncated when JF is activated without having + # pressed COMPL_ACCEPT (Bug #598903) + snippets = ('test', '$1 $2') + keys = COMPLETION_OPTIONS + 'test' + EX + 'com' + COMPL_KW + JF + 'foo' + wanted = COMPLETION_OPTIONS + 'completion1 foo' + + +class Completion_BackwardsJumpWithoutCOMPL_ACCEPT(_VimTest): + # completions should not be truncated when JB is activated without having + # pressed COMPL_ACCEPT (Bug #598903) + snippets = ('test', '$1 $2') + keys = COMPLETION_OPTIONS + 'test' + EX + 'foo' + JF + 'com' + COMPL_KW + \ + JB + 'foo' + wanted = COMPLETION_OPTIONS + 'foo completion1' +# End: Tab Completion of Words #}}} diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py new file mode 100644 index 0000000..f178b40 --- /dev/null +++ b/test/test_ContextSnippets.py @@ -0,0 +1,151 @@ +from test.constant import * +from test.vim_test_case import VimTestCase as _VimTest + + +class ContextSnippets_SimpleSnippet(_VimTest): + files = { 'us/all.snippets': r""" + snippet a "desc" "True" e + abc + endsnippet + """} + keys = 'a' + EX + wanted = 'abc' + + +class ContextSnippets_ExpandOnTrue(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def check_context(): + return True + endglobal + + snippet a "desc" "check_context()" e + abc + endsnippet + """} + keys = 'a' + EX + wanted = 'abc' + + +class ContextSnippets_DoNotExpandOnFalse(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def check_context(): + return False + endglobal + + snippet a "desc" "check_context()" e + abc + endsnippet + """} + keys = 'a' + EX + wanted = keys + + +class ContextSnippets_UseContext(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def wrap(ins): + return "< " + ins + " >" + endglobal + + snippet a "desc" "wrap(snip.buffer[snip.line])" e + { `!p snip.rv = context` } + endsnippet + """} + keys = 'a' + EX + wanted = '{ < a > }' + + +class ContextSnippets_SnippetPriority(_VimTest): + files = { 'us/all.snippets': r""" + snippet i "desc" "re.search('err :=', snip.buffer[snip.line-1])" e + if err != nil { + ${1:// pass} + } + endsnippet + + snippet i + if ${1:true} { + ${2:// pass} + } + endsnippet + """} + + keys = r""" + err := some_call() + i""" + EX + JF + """ + i""" + EX + wanted = r""" + err := some_call() + if err != nil { + // pass + } + if true { + // pass + }""" + + +class ContextSnippets_PriorityKeyword(_VimTest): + files = { 'us/all.snippets': r""" + snippet i "desc" "True" e + a + endsnippet + + priority 100 + snippet i + b + endsnippet + """} + + keys = 'i' + EX + wanted = 'b' + + +class ContextSnippets_ReportError(_VimTest): + files = { 'us/all.snippets': r""" + snippet e "desc" "Tru" e + error + endsnippet + """} + + keys = 'e' + EX + wanted = 'e' + EX + expected_error = r"NameError: name 'Tru' is not defined" + + +class ContextSnippets_ReportErrorOnIndexOutOfRange(_VimTest): + # Working around: https://github.com/neovim/python-client/issues/128. + skip_if = lambda self: 'Bug in Neovim.' \ + if self.vim_flavor == 'neovim' else None + files = { 'us/all.snippets': r""" + snippet e "desc" "snip.buffer[123]" e + error + endsnippet + """} + + keys = 'e' + EX + wanted = 'e' + EX + expected_error = r"IndexError: line number out of range" + + +class ContextSnippets_CursorIsZeroBased(_VimTest): + files = { 'us/all.snippets': r""" + snippet e "desc" "snip.cursor" e + `!p snip.rv = str(snip.context)` + endsnippet + """} + + keys = "e" + EX + wanted = "(2, 1)" + +class ContextSnippets_ContextIsClearedBeforeExpand(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.context = 1 if snip.context is None else 2" + snippet e "desc" w + `!p snip.rv = str(snip.context)` + endsnippet + """} + + keys = "e" + EX + " " + "e" + EX + wanted = "1 1" diff --git a/test/test_Editing.py b/test/test_Editing.py new file mode 100644 index 0000000..06f4a7a --- /dev/null +++ b/test/test_Editing.py @@ -0,0 +1,122 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Undo of Snippet insertion {{{# + + +class Undo_RemoveMultilineSnippet(_VimTest): + snippets = ('test', 'Hello\naaa ${1} bbb\nWorld') + keys = 'test' + EX + ESC + 'u' + wanted = 'test' + + +class Undo_RemoveEditInTabstop(_VimTest): + snippets = ('test', '$1 Hello\naaa ${1} bbb\nWorld') + keys = 'hello test' + EX + 'upsi' + ESC + 'hh' + 'iabcdef' + ESC + 'u' + wanted = 'hello upsi Hello\naaa upsi bbb\nWorld' + + +class Undo_RemoveWholeSnippet(_VimTest): + snippets = ('test', 'Hello\n${1:Hello}World') + keys = 'first line\n\n\n\n\n\nthird line' + \ + ESC + '3k0itest' + EX + ESC + 'u6j' + wanted = 'first line\n\n\ntest\n\n\nthird line' + + +class Undo_RemoveOneSnippetByTime(_VimTest): + snippets = ('i', 'if:\n\t$1') + keys = 'i' + EX + 'i' + EX + ESC + 'u' + wanted = 'if:\n\ti' + + +class Undo_RemoveOneSnippetByTime2(_VimTest): + snippets = ('i', 'if:\n\t$1') + keys = 'i' + EX + 'i' + EX + ESC + 'uu' + wanted = 'if:\n\t' + + +class Undo_ChangesInPlaceholder(_VimTest): + snippets = ('i', 'if $1:\n\t$2') + keys = 'i' + EX + 'asd' + JF + ESC + 'u' + wanted = 'if :\n\t' + + +class Undo_CompletelyUndoSnippet(_VimTest): + snippets = ('i', 'if $1:\n\t$2') + # undo 'feh' + # undo 'asd' + # undo snippet expansion + # undo entering of 'i' + keys = 'i' + EX + 'asd' + JF + 'feh' + ESC + 'uuuu' + wanted = '' + + +class JumpForward_DefSnippet(_VimTest): + snippets = ( + 'test', + "${1}\n`!p snip.rv = '\\n'.join(t[1].split())`\n\n${0:pass}") + keys = 'test' + EX+ 'a b c' + JF + 'shallnot' + wanted = 'a b c\na\nb\nc\n\nshallnot' + + +class DeleteSnippetInsertion0(_VimTest): + snippets = ('test', '${1:hello} $1') + keys = 'test' + EX + ESC + 'Vkx' + 'i\nworld\n' + wanted = 'world' + + +class DeleteSnippetInsertion1(_VimTest): + snippets = ('test', r"$1${1/(.*)/(?0::.)/}") + keys = 'test' + EX + ESC + 'u' + wanted = 'test' + +class DoNotCrashOnUndoAndJumpInNestedSnippet(_VimTest): + snippets = ('test', r"if $1: $2") + keys = 'test' + EX + 'a' + JF + 'test' + EX + ESC + 'u' + JF + wanted = 'if a: test' +# End: Undo of Snippet insertion #}}} + +# Normal mode editing {{{# +# Test for bug #927844 + + +class DeleteLastTwoLinesInSnippet(_VimTest): + snippets = ('test', '$1hello\nnice\nworld') + keys = 'test' + EX + ESC + 'j2dd' + wanted = 'hello' + + +class DeleteCurrentTabStop1_JumpBack(_VimTest): + snippets = ('test', '${1:hi}\nend') + keys = 'test' + EX + ESC + 'ddi' + JB + wanted = 'end' + + +class DeleteCurrentTabStop2_JumpBack(_VimTest): + snippets = ('test', '${1:hi}\n${2:world}\nend') + keys = 'test' + EX + JF + ESC + 'ddi' + JB + 'hello' + wanted = 'hello\nend' + + +class DeleteCurrentTabStop3_JumpAround(_VimTest): + snippets = ('test', '${1:hi}\n${2:world}\nend') + keys = 'test' + EX + JF + ESC + 'ddkji' + JB + 'hello' + JF + 'world' + wanted = 'hello\nendworld' + +# End: Normal mode editing #}}} + +# Pressing BS in TabStop {{{# +# Test for Bug #774917 + + +class Backspace_TabStop_Zero(_VimTest): + snippets = ('test', 'A${1:C} ${0:DDD}', 'This is Case 1') + keys = 'test' + EX + 'A' + JF + BS + 'BBB' + wanted = 'AA BBB' + + +class Backspace_TabStop_NotZero(_VimTest): + snippets = ('test', 'A${1:C} ${2:DDD}', 'This is Case 1') + keys = 'test' + EX + 'A' + JF + BS + 'BBB' + wanted = 'AA BBB' +# End: Pressing BS in TabStop #}}} diff --git a/test/test_Expand.py b/test/test_Expand.py new file mode 100644 index 0000000..d231838 --- /dev/null +++ b/test/test_Expand.py @@ -0,0 +1,73 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Simple Expands {{{# + + +class _SimpleExpands(_VimTest): + snippets = ('hallo', 'Hallo Welt!') + + +class SimpleExpand_ExpectCorrectResult(_SimpleExpands): + keys = 'hallo' + EX + wanted = 'Hallo Welt!' + + +class SimpleExpandTwice_ExpectCorrectResult(_SimpleExpands): + keys = 'hallo' + EX + '\nhallo' + EX + wanted = 'Hallo Welt!\nHallo Welt!' + + +class SimpleExpandNewLineAndBackspae_ExpectCorrectResult(_SimpleExpands): + keys = 'hallo' + EX + '\nHallo Welt!\n\n\b\b\b\b\b' + wanted = 'Hallo Welt!\nHallo We' + + def _extra_vim_config(self, vim_config): + vim_config.append('set backspace=eol,start') + + +class SimpleExpandTypeAfterExpand_ExpectCorrectResult(_SimpleExpands): + keys = 'hallo' + EX + 'and again' + wanted = 'Hallo Welt!and again' + + +class SimpleExpandTypeAndDelete_ExpectCorrectResult(_SimpleExpands): + keys = 'na du hallo' + EX + 'and again\b\b\b\b\bblub' + wanted = 'na du Hallo Welt!and blub' + + +class DoNotExpandAfterSpace_ExpectCorrectResult(_SimpleExpands): + keys = 'hallo ' + EX + wanted = 'hallo ' + EX + + +class ExitSnippetModeAfterTabstopZero(_VimTest): + snippets = ('test', 'SimpleText') + keys = 'test' + EX + EX + wanted = 'SimpleText' + EX + + +class ExpandInTheMiddleOfLine_ExpectCorrectResult(_SimpleExpands): + keys = 'Wie hallo gehts' + ESC + 'bhi' + EX + wanted = 'Wie Hallo Welt! gehts' + + +class MultilineExpand_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'Hallo Welt!\nUnd Wie gehts') + keys = 'Wie hallo gehts' + ESC + 'bhi' + EX + wanted = 'Wie Hallo Welt!\nUnd Wie gehts gehts' + + +class MultilineExpandTestTyping_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'Hallo Welt!\nUnd Wie gehts') + wanted = 'Wie Hallo Welt!\nUnd Wie gehtsHuiui! gehts' + keys = 'Wie hallo gehts' + ESC + 'bhi' + EX + 'Huiui!' + + +class SimpleExpandEndingWithNewline_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'Hallo Welt\n') + keys = 'hallo' + EX + '\nAnd more' + wanted = 'Hallo Welt\n\nAnd more' + + +# End: Simple Expands #}}} diff --git a/test/test_Fixes.py b/test/test_Fixes.py new file mode 100644 index 0000000..5ceef5d --- /dev/null +++ b/test/test_Fixes.py @@ -0,0 +1,84 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Test for bug 1251994 {{{# + + +class Bug1251994(_VimTest): + snippets = ('test', '${2:#2} ${1:#1};$0') + keys = ' test' + EX + 'hello' + JF + 'world' + JF + 'blub' + wanted = ' world hello;blub' +# End: 1251994 #}}} + +# Test for https://github.com/SirVer/ultisnips/issues/157 (virtualedit) {{{# + + +class VirtualEdit(_VimTest): + snippets = ('pd', 'padding: ${1:0}px') + keys = '\t\t\tpd' + EX + '2' + wanted = '\t\t\tpadding: 2px' + + def _extra_vim_config(self, vim_config): + vim_config.append('set virtualedit=all') + vim_config.append('set noexpandtab') +# End: 1251994 #}}} + +# Test for Github Pull Request #134 - Retain unnamed register {{{# + + +class RetainsTheUnnamedRegister(_VimTest): + snippets = ('test', '${1:hello} ${2:world} ${0}') + keys = 'yank' + ESC + 'by4lea test' + EX + 'HELLO' + JF + JF + ESC + 'p' + wanted = 'yank HELLO world yank' + + +class RetainsTheUnnamedRegister_ButOnlyOnce(_VimTest): + snippets = ('test', '${1:hello} ${2:world} ${0}') + keys = 'blahfasel' + ESC + 'v' + 4 * ARR_L + \ + 'xotest' + EX + ESC + ARR_U + 'v0xo' + ESC + 'p' + wanted = '\nblah\nhello world ' +# End: Github Pull Request # 134 #}}} + +# Test to ensure that shiftwidth follows tabstop when it's set to zero post +# version 7.3.693. Prior to that version a shiftwidth of zero effectively +# removes tabs. + + +class ShiftWidthZero(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config += [ + "if exists('*shiftwidth')", + ' set shiftwidth=0', + 'endif', + ] + snippets = ('test', '\t${1}${0}') + keys = 'test' + EX + 'foo' + wanted = '\tfoo' + +# Test for https://github.com/SirVer/ultisnips/issues/171 {{{# +# Make sure that we don't crash when trying to save and restore the clipboard +# when it contains data that we can't coerce into Unicode. + + +class NonUnicodeDataInUnnamedRegister(_VimTest): + snippets = ('test', 'hello') + keys = 'test' + EX + ESC + \ + '\n'.join([':redir @a', + ':messages', + ':redir END', + (":if match(@a, 'Error') != -1 | " + + "call setline('.', 'error detected') | " + + '3put a | ' + + 'endif'), + '']) + wanted = 'hello' + + def _before_test(self): + # The string below was the one a user had on their clipboard when + # encountering the UnicodeDecodeError and could not be coerced into + # unicode. + self.vim.send_to_vim( + ':let @" = "\\x80kdI{\\x80@7 1},' + + '\\x80kh\\x80kh\\x80kd\\x80kdq\\x80kb\\x1b"\n') +# End: #171 #}}} diff --git a/test/test_Folding.py b/test/test_Folding.py new file mode 100644 index 0000000..3a31f77 --- /dev/null +++ b/test/test_Folding.py @@ -0,0 +1,51 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Folding Interaction {{{# + + +class FoldingEnabled_SnippetWithFold_ExpectNoFolding(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set foldlevel=0') + vim_config.append('set foldmethod=marker') + snippets = ('test', r"""Hello {{{ +${1:Welt} }}}""") + keys = 'test' + EX + 'Ball' + wanted = """Hello {{{ +Ball }}}""" + + +class FoldOverwrite_Simple_ECR(_VimTest): + snippets = ('fold', + """# ${1:Description} `!p snip.rv = vim.eval("&foldmarker").split(",")[0]` + +# End: $1 `!p snip.rv = vim.eval("&foldmarker").split(",")[1]`""") + keys = 'fold' + EX + 'hi' + wanted = '# hi {{{\n\n# End: hi }}}' + + +class Fold_DeleteMiddleLine_ECR(_VimTest): + snippets = ('fold', + """# ${1:Description} `!p snip.rv = vim.eval("&foldmarker").split(",")[0]` + + +# End: $1 `!p snip.rv = vim.eval("&foldmarker").split(",")[1]`""") + keys = 'fold' + EX + 'hi' + ESC + 'jdd' + wanted = '# hi {{{\n\n# End: hi }}}' + + +class PerlSyntaxFold(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set foldlevel=0') + vim_config.append('syntax enable') + vim_config.append('set foldmethod=syntax') + vim_config.append('let g:perl_fold = 1') + vim_config.append('so $VIMRUNTIME/syntax/perl.vim') + snippets = ('test', r"""package ${1:`!v printf('c%02d', 3)`}; +${0} +1;""") + keys = 'test' + EX + JF + 'sub junk {}' + wanted = 'package c03;\nsub junk {}\n1;' +# End: Folding Interaction #}}} diff --git a/test/test_Format.py b/test/test_Format.py new file mode 100644 index 0000000..905e77e --- /dev/null +++ b/test/test_Format.py @@ -0,0 +1,157 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * +from test.util import running_on_windows + +# ExpandTab {{{# + + +class _ExpandTabs(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=3') + vim_config.append('set expandtab') + + +class RecTabStopsWithExpandtab_SimpleExample_ECR(_ExpandTabs): + snippets = ('m', '\tBlaahblah \t\t ') + keys = 'm' + EX + wanted = ' Blaahblah \t\t ' + + +class RecTabStopsWithExpandtab_SpecialIndentProblem_ECR(_ExpandTabs): + # Windows indents the Something line after pressing return, though it + # shouldn't because it contains a manual indent. All other vim versions do + # not do this. Windows vim does not interpret the changes made by :py as + # changes made 'manually', while the other vim version seem to do so. Since + # the fault is not with UltiSnips, we simply skip this test on windows + # completely. + skip_if = lambda self: running_on_windows() + snippets = ( + ('m1', 'Something'), + ('m', '\t$0'), + ) + keys = 'm' + EX + 'm1' + EX + '\nHallo' + wanted = ' Something\n Hallo' + + def _extra_vim_config(self, vim_config): + _ExpandTabs._extra_vim_config(self, vim_config) + vim_config.append('set indentkeys=o,O,*,<>>,{,}') + vim_config.append('set indentexpr=8') +# End: ExpandTab #}}} + +# Proper Indenting {{{# + + +class ProperIndenting_SimpleCase_ECR(_VimTest): + snippets = ('test', 'for\n blah') + keys = ' test' + EX + 'Hui' + wanted = ' for\n blahHui' + + +class ProperIndenting_SingleLineNoReindenting_ECR(_VimTest): + snippets = ('test', 'hui') + keys = ' test' + EX + 'blah' + wanted = ' huiblah' + + +class ProperIndenting_AutoIndentAndNewline_ECR(_VimTest): + snippets = ('test', 'hui') + keys = ' test' + EX + '\n' + 'blah' + wanted = ' hui\n blah' + + def _extra_vim_config(self, vim_config): + vim_config.append('set autoindent') +# Test for bug 1073816 + + +class ProperIndenting_FirstLineInFile_ECR(_VimTest): + text_before = '' + text_after = '' + files = { 'us/all.snippets': r""" +global !p +def complete(t, opts): + if t: + opts = [ m[len(t):] for m in opts if m.startswith(t) ] + if len(opts) == 1: + return opts[0] + elif len(opts) > 1: + return "(" + "|".join(opts) + ")" + else: + return "" +endglobal + +snippet '^#?inc' "#include <>" !r +#include <$1`!p snip.rv = complete(t[1], ['cassert', 'cstdio', 'cstdlib', 'cstring', 'fstream', 'iostream', 'sstream'])`> +endsnippet + """} + keys = 'inc' + EX + 'foo' + wanted = '#include ' + + +class ProperIndenting_FirstLineInFileComplete_ECR( + ProperIndenting_FirstLineInFile_ECR): + keys = 'inc' + EX + 'cstdl' + wanted = '#include ' +# End: Proper Indenting #}}} + +# Format options tests {{{# + + +class _FormatoptionsBase(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set tw=20') + vim_config.append('set fo=lrqntc') + + +class FOSimple_Break_ExpectCorrectResult(_FormatoptionsBase): + snippets = ('test', '${1:longer expand}\n$1\n$0', '', 'f') + keys = 'test' + EX + \ + 'This is a longer text that should wrap as formatoptions are enabled' + \ + JF + 'end' + wanted = 'This is a longer\ntext that should\nwrap as\nformatoptions are\nenabled\n' + \ + 'This is a longer\ntext that should\nwrap as\nformatoptions are\nenabled\n' + \ + 'end' + + +class FOTextBeforeAndAfter_ExpectCorrectResult(_FormatoptionsBase): + snippets = ('test', 'Before${1:longer expand}After\nstart$1end') + keys = 'test' + EX + 'This is a longer text that should wrap' + wanted = \ + """BeforeThis is a +longer text that +should wrapAfter +startThis is a +longer text that +should wrapend""" + + +# Tests for https://bugs.launchpad.net/bugs/719998 +class FOTextAfter_ExpectCorrectResult(_FormatoptionsBase): + snippets = ('test', '${1:longer expand}after\nstart$1end') + keys = ('test' + EX + 'This is a longer snippet that should wrap properly ' + 'and the mirror below should work as well') + wanted = \ + """This is a longer +snippet that should +wrap properly and +the mirror below +should work as wellafter +startThis is a longer +snippet that should +wrap properly and +the mirror below +should work as wellend""" + + +class FOWrapOnLongWord_ExpectCorrectResult(_FormatoptionsBase): + snippets = ('test', '${1:longer expand}after\nstart$1end') + keys = ('test' + EX + 'This is a longersnippet that should wrap properly') + wanted = \ + """This is a +longersnippet that +should wrap properlyafter +startThis is a +longersnippet that +should wrap properlyend""" +# End: Format options tests #}}} diff --git a/test/test_Interpolation.py b/test/test_Interpolation.py new file mode 100644 index 0000000..b006355 --- /dev/null +++ b/test/test_Interpolation.py @@ -0,0 +1,460 @@ +# encoding: utf-8 +import os + +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import EX, JF, ESC +from test.util import running_on_windows + +# ShellCode Interpolation {{{# + + +class TabStop_Shell_SimpleExample(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = ('test', 'hi `echo hallo` you!') + keys = 'test' + EX + 'and more' + wanted = 'hi hallo you!and more' + + +class TabStop_Shell_WithUmlauts(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = ('test', 'hi `echo höüäh` you!') + keys = 'test' + EX + 'and more' + wanted = 'hi höüäh you!and more' + + +class TabStop_Shell_TextInNextLine(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = ('test', 'hi `echo hallo`\nWeiter') + keys = 'test' + EX + 'and more' + wanted = 'hi hallo\nWeiterand more' + + +class TabStop_Shell_InDefValue_Leave(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = ('test', 'Hallo ${1:now `echo fromecho`} end') + keys = 'test' + EX + JF + 'and more' + wanted = 'Hallo now fromecho endand more' + + +class TabStop_Shell_InDefValue_Overwrite(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = ('test', 'Hallo ${1:now `echo fromecho`} end') + keys = 'test' + EX + 'overwrite' + JF + 'and more' + wanted = 'Hallo overwrite endand more' + + +class TabStop_Shell_TestEscapedChars_Overwrite(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = ('test', r"""`echo \`echo "\\$hi"\``""") + keys = 'test' + EX + wanted = '$hi' + + +class TabStop_Shell_TestEscapedCharsAndShellVars_Overwrite(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = ('test', r"""`hi="blah"; echo \`echo "$hi"\``""") + keys = 'test' + EX + wanted = 'blah' + + +class TabStop_Shell_ShebangPython(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = ('test', """Hallo ${1:now `#!/usr/bin/env %s +print "Hallo Welt" +`} end""" % (os.environ.get('PYTHON', 'python2'),)) + keys = 'test' + EX + JF + 'and more' + wanted = 'Hallo now Hallo Welt endand more' +# End: ShellCode Interpolation #}}} +# VimScript Interpolation {{{# + + +class TabStop_VimScriptInterpolation_SimpleExample(_VimTest): + snippets = ('test', """hi `!v indent(".")` End""") + keys = ' test' + EX + wanted = ' hi 4 End' +# End: VimScript Interpolation #}}} +# PythonCode Interpolation {{{# +# Deprecated Implementation {{{# + + +class PythonCodeOld_SimpleExample(_VimTest): + snippets = ('test', """hi `!p res = "Hallo"` End""") + keys = 'test' + EX + wanted = 'hi Hallo End' + + +class PythonCodeOld_ReferencePlaceholderAfter(_VimTest): + snippets = ('test', """${1:hi} `!p res = t[1]+".blah"` End""") + keys = 'test' + EX + 'ho' + wanted = 'ho ho.blah End' + + +class PythonCodeOld_ReferencePlaceholderBefore(_VimTest): + snippets = ('test', """`!p res = len(t[1])*"#"`\n${1:some text}""") + keys = 'test' + EX + 'Hallo Welt' + wanted = '##########\nHallo Welt' + + +class PythonCodeOld_TransformedBeforeMultiLine(_VimTest): + snippets = ('test', """${1/.+/egal/m} ${1:`!p +res = "Hallo"`} End""") + keys = 'test' + EX + wanted = 'egal Hallo End' + + +class PythonCodeOld_IndentedMultiline(_VimTest): + snippets = ('test', """start `!p a = 1 +b = 2 +if b > a: + res = "b isbigger a" +else: + res = "a isbigger b"` end""") + keys = ' test' + EX + wanted = ' start b isbigger a end' +# End: Deprecated Implementation #}}} +# New Implementation {{{# + + +class PythonCode_UseNewOverOld(_VimTest): + snippets = ('test', """hi `!p res = "Old" +snip.rv = "New"` End""") + keys = 'test' + EX + wanted = 'hi New End' + + +class PythonCode_SimpleExample(_VimTest): + snippets = ('test', """hi `!p snip.rv = "Hallo"` End""") + keys = 'test' + EX + wanted = 'hi Hallo End' + + +class PythonCode_SimpleExample_ReturnValueIsEmptyString(_VimTest): + snippets = ('test', """hi`!p snip.rv = ""`End""") + keys = 'test' + EX + wanted = 'hiEnd' + + +class PythonCode_ReferencePlaceholder(_VimTest): + snippets = ('test', """${1:hi} `!p snip.rv = t[1]+".blah"` End""") + keys = 'test' + EX + 'ho' + wanted = 'ho ho.blah End' + + +class PythonCode_ReferencePlaceholderBefore(_VimTest): + snippets = ('test', """`!p snip.rv = len(t[1])*"#"`\n${1:some text}""") + keys = 'test' + EX + 'Hallo Welt' + wanted = '##########\nHallo Welt' + + +class PythonCode_TransformedBeforeMultiLine(_VimTest): + snippets = ('test', """${1/.+/egal/m} ${1:`!p +snip.rv = "Hallo"`} End""") + keys = 'test' + EX + wanted = 'egal Hallo End' + + +class PythonCode_MultilineIndented(_VimTest): + snippets = ('test', """start `!p a = 1 +b = 2 +if b > a: + snip.rv = "b isbigger a" +else: + snip.rv = "a isbigger b"` end""") + keys = ' test' + EX + wanted = ' start b isbigger a end' + + +class PythonCode_SimpleAppend(_VimTest): + snippets = ('test', """hi `!p snip.rv = "Hallo1" +snip += "Hallo2"` End""") + keys = 'test' + EX + wanted = 'hi Hallo1\nHallo2 End' + + +class PythonCode_MultiAppend(_VimTest): + snippets = ('test', """hi `!p snip.rv = "Hallo1" +snip += "Hallo2" +snip += "Hallo3"` End""") + keys = 'test' + EX + wanted = 'hi Hallo1\nHallo2\nHallo3 End' + + +class PythonCode_MultiAppendSimpleIndent(_VimTest): + snippets = ('test', """hi +`!p snip.rv="Hallo1" +snip += "Hallo2" +snip += "Hallo3"` +End""") + keys = """ + test""" + EX + wanted = """ + hi + Hallo1 + Hallo2 + Hallo3 + End""" + + +class PythonCode_SimpleMkline(_VimTest): + snippets = ('test', r"""hi +`!p snip.rv="Hallo1\n" +snip.rv += snip.mkline("Hallo2") + "\n" +snip.rv += snip.mkline("Hallo3")` +End""") + keys = """ + test""" + EX + wanted = """ + hi + Hallo1 + Hallo2 + Hallo3 + End""" + + +class PythonCode_MultiAppendShift(_VimTest): + snippets = ('test', r"""hi +`!p snip.rv="i1" +snip += "i1" +snip >> 1 +snip += "i2" +snip << 2 +snip += "i0" +snip >> 3 +snip += "i3"` +End""") + keys = """ + test""" + EX + wanted = """ + hi + i1 + i1 + i2 +i0 + i3 + End""" + + +class PythonCode_MultiAppendShiftMethods(_VimTest): + snippets = ('test', r"""hi +`!p snip.rv="i1\n" +snip.rv += snip.mkline("i1\n") +snip.shift(1) +snip.rv += snip.mkline("i2\n") +snip.unshift(2) +snip.rv += snip.mkline("i0\n") +snip.shift(3) +snip.rv += snip.mkline("i3")` +End""") + keys = """ + test""" + EX + wanted = """ + hi + i1 + i1 + i2 +i0 + i3 + End""" + + +class PythonCode_ResetIndent(_VimTest): + snippets = ('test', r"""hi +`!p snip.rv="i1" +snip >> 1 +snip += "i2" +snip.reset_indent() +snip += "i1" +snip << 1 +snip += "i0" +snip.reset_indent() +snip += "i1"` +End""") + keys = """ + test""" + EX + wanted = """ + hi + i1 + i2 + i1 +i0 + i1 + End""" + + +class PythonCode_IndentEtSw(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=3') + vim_config.append('set expandtab') + snippets = ('test', r"""hi +`!p snip.rv = "i1" +snip >> 1 +snip += "i2" +snip << 2 +snip += "i0" +snip >> 1 +snip += "i1" +` +End""") + keys = """ test""" + EX + wanted = """ hi + i1 + i2 +i0 + i1 + End""" + + +class PythonCode_IndentEtSwOffset(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=3') + vim_config.append('set expandtab') + snippets = ('test', r"""hi +`!p snip.rv = "i1" +snip >> 1 +snip += "i2" +snip << 2 +snip += "i0" +snip >> 1 +snip += "i1" +` +End""") + keys = """ test""" + EX + wanted = """ hi + i1 + i2 + i0 + i1 + End""" + + +class PythonCode_IndentNoetSwTs(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=3') + vim_config.append('set ts=4') + snippets = ('test', r"""hi +`!p snip.rv = "i1" +snip >> 1 +snip += "i2" +snip << 2 +snip += "i0" +snip >> 1 +snip += "i1" +` +End""") + keys = """ test""" + EX + wanted = """ hi + i1 +\t i2 +i0 + i1 + End""" + +# Test using 'opt' + + +class PythonCode_OptExists(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('let g:UStest="yes"') + snippets = ( + 'test', + r"""hi `!p snip.rv = snip.opt("g:UStest") or "no"` End""") + keys = """test""" + EX + wanted = """hi yes End""" + + +class PythonCode_OptNoExists(_VimTest): + snippets = ( + 'test', + r"""hi `!p snip.rv = snip.opt("g:UStest") or "no"` End""") + keys = """test""" + EX + wanted = """hi no End""" + + +class PythonCode_IndentProblem(_VimTest): + # A test case which is likely related to bug 719649 + snippets = ('test', r"""hi `!p +snip.rv = "World" +` End""") + keys = ' ' * 8 + 'test' + EX # < 8 works. + wanted = """ hi World End""" + + +class PythonCode_TrickyReferences(_VimTest): + snippets = ( + 'test', + r"""${2:${1/.+/egal/}} ${1:$3} ${3:`!p snip.rv = "hi"`}""") + keys = 'ups test' + EX + wanted = 'ups egal hi hi' +# locals + + +class PythonCode_Locals(_VimTest): + snippets = ('test', r"""hi `!p a = "test" +snip.rv = "nothing"` `!p snip.rv = a +` End""") + keys = """test""" + EX + wanted = """hi nothing test End""" + + +class PythonCode_LongerTextThanSource_Chars(_VimTest): + snippets = ('test', r"""hi`!p snip.rv = "a" * 100`end""") + keys = """test""" + EX + 'ups' + wanted = 'hi' + 100 * 'a' + 'endups' + + +class PythonCode_LongerTextThanSource_MultiLine(_VimTest): + snippets = ( + 'test', + r"""hi`!p snip.rv = "a" * 100 + '\n'*100 + "a"*100`end""") + keys = """test""" + EX + 'ups' + wanted = 'hi' + 100 * 'a' + 100 * '\n' + 100 * 'a' + 'endups' + + +class PythonCode_AccessKilledTabstop_OverwriteSecond(_VimTest): + snippets = ( + 'test', + r"`!p snip.rv = t[2].upper()`${1:h${2:welt}o}`!p snip.rv = t[2].upper()`") + keys = 'test' + EX + JF + 'okay' + wanted = 'OKAYhokayoOKAY' + + +class PythonCode_AccessKilledTabstop_OverwriteFirst(_VimTest): + snippets = ( + 'test', + r"`!p snip.rv = t[2].upper()`${1:h${2:welt}o}`!p snip.rv = t[2].upper()`") + keys = 'test' + EX + 'aaa' + wanted = 'aaa' + + +class PythonVisual_NoVisualSelection_Ignore(_VimTest): + snippets = ('test', 'h`!p snip.rv = snip.v.mode + snip.v.text`b') + keys = 'test' + EX + 'abc' + wanted = 'hbabc' + + +class PythonVisual_SelectOneWord(_VimTest): + snippets = ('test', 'h`!p snip.rv = snip.v.mode + snip.v.text`b') + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + wanted = 'hvblablubb' + + +class PythonVisual_LineSelect_Simple(_VimTest): + snippets = ('test', 'h`!p snip.rv = snip.v.mode + snip.v.text`b') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + wanted = 'hVhello\nnice\nworld\nb' + +# Tests for https://bugs.launchpad.net/bugs/1259349 + + +class Python_WeirdScoping_Error(_VimTest): + snippets = ( + 'test', + "h`!p import re; snip.rv = '%i' % len([re.search for i in 'aiiia'])`b") + keys = 'test' + EX + wanted = 'h5b' +# End: New Implementation #}}} +# End: PythonCode Interpolation #}}} diff --git a/test/test_ListSnippets.py b/test/test_ListSnippets.py new file mode 100644 index 0000000..4b54631 --- /dev/null +++ b/test/test_ListSnippets.py @@ -0,0 +1,43 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# List Snippets {{{# + + +class _ListAllSnippets(_VimTest): + snippets = (('testblah', 'BLAAH', 'Say BLAH'), + ('test', 'TEST ONE', 'Say tst one'), + ('aloha', 'OHEEEE', 'Say OHEE'), + ) + + +class ListAllAvailable_NothingTyped_ExpectCorrectResult(_ListAllSnippets): + keys = '' + LS + '3\n' + wanted = 'BLAAH' + + +class ListAllAvailable_SpaceInFront_ExpectCorrectResult(_ListAllSnippets): + keys = ' ' + LS + '3\n' + wanted = ' BLAAH' + + +class ListAllAvailable_BraceInFront_ExpectCorrectResult(_ListAllSnippets): + keys = '} ' + LS + '3\n' + wanted = '} BLAAH' + + +class ListAllAvailable_testtyped_ExpectCorrectResult(_ListAllSnippets): + keys = 'hallo test' + LS + '2\n' + wanted = 'hallo BLAAH' + + +class ListAllAvailable_testtypedSecondOpt_ExpectCorrectResult( + _ListAllSnippets): + keys = 'hallo test' + LS + '1\n' + wanted = 'hallo TEST ONE' + + +class ListAllAvailable_NonDefined_NoExpectionShouldBeRaised(_ListAllSnippets): + keys = 'hallo qualle' + LS + 'Hi' + wanted = 'hallo qualleHi' +# End: List Snippets #}}} diff --git a/test/test_Mirror.py b/test/test_Mirror.py new file mode 100644 index 0000000..a6076bf --- /dev/null +++ b/test/test_Mirror.py @@ -0,0 +1,272 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Mirrors {{{# + + +class TextTabStopTextAfterTab_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 Hinten\n$1') + keys = 'test' + EX + 'hallo' + wanted = 'hallo Hinten\nhallo' + + +class TextTabStopTextBeforeTab_ExpectCorrectResult(_VimTest): + snippets = ('test', 'Vorne $1\n$1') + keys = 'test' + EX + 'hallo' + wanted = 'Vorne hallo\nhallo' + + +class TextTabStopTextSurroundedTab_ExpectCorrectResult(_VimTest): + snippets = ('test', 'Vorne $1 Hinten\n$1') + keys = 'test' + EX + 'hallo test' + wanted = 'Vorne hallo test Hinten\nhallo test' + + +class TextTabStopTextBeforeMirror_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\nVorne $1') + keys = 'test' + EX + 'hallo' + wanted = 'hallo\nVorne hallo' + + +class TextTabStopAfterMirror_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\n$1 Hinten') + keys = 'test' + EX + 'hallo' + wanted = 'hallo\nhallo Hinten' + + +class TextTabStopSurroundMirror_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\nVorne $1 Hinten') + keys = 'test' + EX + 'hallo welt' + wanted = 'hallo welt\nVorne hallo welt Hinten' + + +class TextTabStopAllSurrounded_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ObenVorne $1 ObenHinten\nVorne $1 Hinten') + keys = 'test' + EX + 'hallo welt' + wanted = 'ObenVorne hallo welt ObenHinten\nVorne hallo welt Hinten' + + +class MirrorBeforeTabstopLeave_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1:this is it} $1') + keys = 'test' + EX + wanted = 'this is it this is it this is it' + + +class MirrorBeforeTabstopOverwrite_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1:this is it} $1') + keys = 'test' + EX + 'a' + wanted = 'a a a' + + +class TextTabStopSimpleMirrorMultiline_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\n$1') + keys = 'test' + EX + 'hallo' + wanted = 'hallo\nhallo' + + +class SimpleMirrorMultilineMany_ExpectCorrectResult(_VimTest): + snippets = ('test', ' $1\n$1\na$1b\n$1\ntest $1 mich') + keys = 'test' + EX + 'hallo' + wanted = ' hallo\nhallo\nahallob\nhallo\ntest hallo mich' + + +class MultilineTabStopSimpleMirrorMultiline_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\n\n$1\n\n$1') + keys = 'test' + EX + 'hallo Du\nHi' + wanted = 'hallo Du\nHi\n\nhallo Du\nHi\n\nhallo Du\nHi' + + +class MultilineTabStopSimpleMirrorMultiline1_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\n$1\n$1') + keys = 'test' + EX + 'hallo Du\nHi' + wanted = 'hallo Du\nHi\nhallo Du\nHi\nhallo Du\nHi' + + +class MultilineTabStopSimpleMirrorDeleteInLine_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\n$1\n$1') + keys = 'test' + EX + 'hallo Du\nHi\b\bAch Blah' + wanted = 'hallo Du\nAch Blah\nhallo Du\nAch Blah\nhallo Du\nAch Blah' + + +class TextTabStopSimpleMirrorMultilineMirrorInFront_ECR(_VimTest): + snippets = ('test', '$1\n${1:sometext}') + keys = 'test' + EX + 'hallo\nagain' + wanted = 'hallo\nagain\nhallo\nagain' + + +class SimpleMirrorDelete_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\n$1') + keys = 'test' + EX + 'hallo\b\b' + wanted = 'hal\nhal' + + +class SimpleMirrorSameLine_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 $1') + keys = 'test' + EX + 'hallo' + wanted = 'hallo hallo' + + +class SimpleMirrorSameLine_InText_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 $1') + keys = 'ups test blah' + ESC + '02f i' + EX + 'hallo' + wanted = 'ups hallo hallo blah' + + +class SimpleMirrorSameLineBeforeTabDefVal_ECR(_VimTest): + snippets = ('test', '$1 ${1:replace me}') + keys = 'test' + EX + 'hallo foo' + wanted = 'hallo foo hallo foo' + + +class SimpleMirrorSameLineBeforeTabDefVal_DelB4Typing_ECR(_VimTest): + snippets = ('test', '$1 ${1:replace me}') + keys = 'test' + EX + BS + 'hallo foo' + wanted = 'hallo foo hallo foo' + + +class SimpleMirrorSameLineMany_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 $1 $1 $1') + keys = 'test' + EX + 'hallo du' + wanted = 'hallo du hallo du hallo du hallo du' + + +class SimpleMirrorSameLineManyMultiline_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 $1 $1 $1') + keys = 'test' + EX + 'hallo du\nwie gehts' + wanted = 'hallo du\nwie gehts hallo du\nwie gehts hallo du\nwie gehts' \ + ' hallo du\nwie gehts' + + +class SimpleMirrorDeleteSomeEnterSome_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1\n$1') + keys = 'test' + EX + 'hallo\b\bhups' + wanted = 'halhups\nhalhups' + + +class SimpleTabstopWithDefaultSimpelType_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha ${1:defa}\n$1') + keys = 'test' + EX + 'world' + wanted = 'ha world\nworld' + + +class SimpleTabstopWithDefaultComplexType_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha ${1:default value} $1\nanother: $1 mirror') + keys = 'test' + EX + 'world' + wanted = 'ha world world\nanother: world mirror' + + +class SimpleTabstopWithDefaultSimpelKeep_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha ${1:defa}\n$1') + keys = 'test' + EX + wanted = 'ha defa\ndefa' + + +class SimpleTabstopWithDefaultComplexKeep_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha ${1:default value} $1\nanother: $1 mirror') + keys = 'test' + EX + wanted = 'ha default value default value\nanother: default value mirror' + + +class TabstopWithMirrorManyFromAll_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha $5 ${1:blub} $4 $0 ${2:$1.h} $1 $3 ${4:More}') + keys = 'test' + EX + 'hi' + JF + 'hu' + JF + 'hub' + JF + 'hulla' + \ + JF + 'blah' + JF + 'end' + wanted = 'ha blah hi hulla end hu hi hub hulla' + + +class TabstopWithMirrorInDefaultNoType_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha ${1:blub} ${2:$1.h}') + keys = 'test' + EX + wanted = 'ha blub blub.h' + + +class TabstopWithMirrorInDefaultNoType1_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha ${1:blub} ${2:$1}') + keys = 'test' + EX + wanted = 'ha blub blub' + + +class TabstopWithMirrorInDefaultTwiceAndExtra_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha $1 ${2:$1.h $1.c}\ntest $1') + keys = 'test' + EX + 'stdin' + wanted = 'ha stdin stdin.h stdin.c\ntest stdin' + + +class TabstopWithMirrorInDefaultMultipleLeave_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha $1 ${2:snip} ${3:$1.h $2}') + keys = 'test' + EX + 'stdin' + wanted = 'ha stdin snip stdin.h snip' + + +class TabstopWithMirrorInDefaultMultipleOverwrite_ExpectCorrectResult( + _VimTest): + snippets = ('test', 'ha $1 ${2:snip} ${3:$1.h $2}') + keys = 'test' + EX + 'stdin' + JF + 'do snap' + wanted = 'ha stdin do snap stdin.h do snap' + + +class TabstopWithMirrorInDefaultOverwrite_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha $1 ${2:$1.h}') + keys = 'test' + EX + 'stdin' + JF + 'overwritten' + wanted = 'ha stdin overwritten' + + +class TabstopWithMirrorInDefaultOverwrite1_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha $1 ${2:$1}') + keys = 'test' + EX + 'stdin' + JF + 'overwritten' + wanted = 'ha stdin overwritten' + + +class TabstopWithMirrorInDefaultNoOverwrite1_ExpectCorrectResult(_VimTest): + snippets = ('test', 'ha $1 ${2:$1}') + keys = 'test' + EX + 'stdin' + JF + JF + 'end' + wanted = 'ha stdin stdinend' + + +class MirrorRealLifeExample_ExpectCorrectResult(_VimTest): + snippets = ( + ('for', 'for(size_t ${2:i} = 0; $2 < ${1:count}; ${3:++$2})' + '\n{\n\t${0:/* code */}\n}'), + ) + keys = 'for' + EX + '100' + JF + 'avar\b\b\b\ba_variable' + JF + \ + 'a_variable *= 2' + JF + '// do nothing' + wanted = """for(size_t a_variable = 0; a_variable < 100; a_variable *= 2) +{ +\t// do nothing +}""" + + +class Mirror_TestKill_InsertBefore_NoKill(_VimTest): + snippets = 'test', '$1 $1_' + keys = 'hallo test' + EX + 'auch' + ESC + \ + 'wihi' + ESC + 'bb' + 'ino' + JF + 'end' + wanted = 'hallo noauch hinoauch_end' + + +class Mirror_TestKill_InsertAfter_NoKill(_VimTest): + snippets = 'test', '$1 $1_' + keys = 'hallo test' + EX + 'auch' + ESC + \ + 'eiab' + ESC + 'bb' + 'ino' + JF + 'end' + wanted = 'hallo noauch noauchab_end' + + +class Mirror_TestKill_InsertBeginning_Kill(_VimTest): + snippets = 'test', '$1 $1_' + keys = 'hallo test' + EX + 'auch' + ESC + \ + 'wahi' + ESC + 'bb' + 'ino' + JF + 'end' + wanted = 'hallo noauch ahiuch_end' + + +class Mirror_TestKill_InsertEnd_Kill(_VimTest): + snippets = 'test', '$1 $1_' + keys = 'hallo test' + EX + 'auch' + ESC + \ + 'ehihi' + ESC + 'bb' + 'ino' + JF + 'end' + wanted = 'hallo noauch auchih_end' + + +class Mirror_TestKillTabstop_Kill(_VimTest): + snippets = 'test', 'welt${1:welt${2:welt}welt} $2' + keys = 'hallo test' + EX + 'elt' + wanted = 'hallo weltelt ' + +# End: Mirrors #}}} diff --git a/test/test_Movement.py b/test/test_Movement.py new file mode 100644 index 0000000..51b0c10 --- /dev/null +++ b/test/test_Movement.py @@ -0,0 +1,83 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Cursor Movement {{{# + + +class CursorMovement_Multiline_ECR(_VimTest): + snippets = ('test', r"$1 ${1:a tab}") + keys = 'test' + EX + 'this is something\nvery nice\nnot' + JF + 'more text' + wanted = 'this is something\nvery nice\nnot ' \ + 'this is something\nvery nice\nnotmore text' + + +class CursorMovement_BS_InEditMode(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set backspace=eol,indent,start') + snippets = ('\n\t$1\n\t$2\n\n$3') + keys = '= (3, 0) + + +def python3(): + if PYTHON3: + return 'Test does not work on python3.' + +# Plugin: YouCompleteMe {{{# +# TODO(sirver): disabled because it fails right now. +# class Plugin_YouCompleteMe_IntegrationTest(_VimTest): + # def skip_if(self): + # r = python3() + # if r: + # return r + # if "7.4" not in self.version: + # return "Needs Vim 7.4." + # plugins = ["Valloric/YouCompleteMe"] + # snippets = ("superlongtrigger", "Hello") + # keys = "superlo\ty" + # wanted = "Hello" + + # def _extra_vim_config(self, vim_config): + # # Not sure why, but I need to make a new tab for this to work. + # vim_config.append('let g:UltiSnipsExpandTrigger="y"') + # vim_config.append('tabnew') + + # def _before_test(self): + # self.vim.send(":set ft=python\n") + # # Give ycm a chance to catch up. + # time.sleep(1) +# End: Plugin: YouCompleteMe #}}} +# Plugin: Neocomplete {{{# +# TODO(sirver): disabled because it fails right now. +# class Plugin_Neocomplete_BugTest(_VimTest): + # Test for https://github.com/SirVer/ultisnips/issues/228 + + # def skip_if(self): + # if '+lua' not in self.version: + # return 'Needs +lua' + # plugins = ['Shougo/neocomplete.vim'] + # snippets = ('t', 'Hello', '', 'w') + # keys = 'iab\\ t' + EX + # wanted = 'iab\\ Hello' + + # def _extra_vim_config(self, vim_config): + # vim_config.append(r'set iskeyword+=\\ ') + # vim_config.append('let g:neocomplete#enable_at_startup = 1') + # vim_config.append('let g:neocomplete#enable_smart_case = 1') + # vim_config.append('let g:neocomplete#enable_camel_case = 1') + # vim_config.append('let g:neocomplete#enable_auto_delimiter = 1') + # vim_config.append('let g:neocomplete#enable_refresh_always = 1') +# End: Plugin: Neocomplete #}}} +# Plugin: unite {{{# + +# TODO(sirver): Disable since it is flaky. +# class Plugin_unite_BugTest(_VimTest): + # plugins = ['Shougo/unite.vim'] + # snippets = ('t', 'Hello', '', 'w') + # keys = 'iab\\ t=UltiSnipsCallUnite()\n' + # wanted = 'iab\\ Hello ' + + # def _extra_vim_config(self, vim_config): + # vim_config.append(r'set iskeyword+=\\ ') + # vim_config.append('function! UltiSnipsCallUnite()') + # vim_config.append( + # ' Unite -start-insert -winheight=100 -immediately -no-empty ultisnips') + # vim_config.append(' return ""') + # vim_config.append('endfunction') +# End: Plugin: unite #}}} +# Plugin: Supertab {{{# + + +class Plugin_SuperTab_SimpleTest(_VimTest): + plugins = ['ervandew/supertab'] + snippets = ('long', 'Hello', '', 'w') + keys = ('longtextlongtext\n' + + 'longt' + EX + '\n' + # Should complete word + 'long' + EX) # Should expand + wanted = 'longtextlongtext\nlongtextlongtext\nHello' + + def _before_test(self): + # Make sure that UltiSnips has the keymap + self.vim.send_to_vim(':call UltiSnips#map_keys#MapKeys()\n') + + def _extra_vim_config(self, vim_config): + assert EX == '\t' # Otherwise this test needs changing. + vim_config.append('let g:SuperTabDefaultCompletionType = ""') + vim_config.append('let g:SuperTabRetainCompletionDuration = "insert"') + vim_config.append('let g:SuperTabLongestHighlight = 1') + vim_config.append('let g:SuperTabCrMapping = 0') +# End: Plugin: Supertab #}}} diff --git a/test/test_Recursive.py b/test/test_Recursive.py new file mode 100644 index 0000000..5e93594 --- /dev/null +++ b/test/test_Recursive.py @@ -0,0 +1,265 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Recursive (Nested) Snippets {{{# + + +class RecTabStops_SimpleCase_ExpectCorrectResult(_VimTest): + snippets = ('m', '[ ${1:first} ${2:sec} ]') + keys = 'm' + EX + 'm' + EX + 'hello' + \ + JF + 'world' + JF + 'ups' + JF + 'end' + wanted = '[ [ hello world ]ups end ]' + + +class RecTabStops_SimpleCaseLeaveSecondSecond_ExpectCorrectResult(_VimTest): + snippets = ('m', '[ ${1:first} ${2:sec} ]') + keys = 'm' + EX + 'm' + EX + 'hello' + JF + 'world' + JF + JF + JF + 'end' + wanted = '[ [ hello world ] sec ]end' + + +class RecTabStops_SimpleCaseLeaveFirstSecond_ExpectCorrectResult(_VimTest): + snippets = ('m', '[ ${1:first} ${2:sec} ]') + keys = 'm' + EX + 'm' + EX + 'hello' + JF + JF + JF + 'world' + JF + 'end' + wanted = '[ [ hello sec ] world ]end' + + +class RecTabStops_InnerWOTabStop_ECR(_VimTest): + snippets = ( + ('m1', 'Just some Text'), + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm1' + EX + 'hi' + JF + 'two' + JF + 'end' + wanted = '[ Just some Texthi two ]end' + + +class RecTabStops_InnerWOTabStopTwiceDirectly_ECR(_VimTest): + snippets = ( + ('m1', 'JST'), + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm1' + EX + ' m1' + EX + 'hi' + JF + 'two' + JF + 'end' + wanted = '[ JST JSThi two ]end' + + +class RecTabStops_InnerWOTabStopTwice_ECR(_VimTest): + snippets = ( + ('m1', 'JST'), + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm1' + EX + JF + 'm1' + EX + 'hi' + JF + 'end' + wanted = '[ JST JSThi ]end' + + +class RecTabStops_OuterOnlyWithZeroTS_ECR(_VimTest): + snippets = ( + ('m', 'A $0 B'), + ('m1', 'C $1 D $0 E'), + ) + keys = 'm' + EX + 'm1' + EX + 'CD' + JF + 'DE' + wanted = 'A C CD D DE E B' + + +class RecTabStops_OuterOnlyWithZero_ECR(_VimTest): + snippets = ( + ('m', 'A $0 B'), + ('m1', 'C $1 D $0 E'), + ) + keys = 'm' + EX + 'm1' + EX + 'CD' + JF + 'DE' + wanted = 'A C CD D DE E B' + + +class RecTabStops_ExpandedInZeroTS_ECR(_VimTest): + snippets = ( + ('m', 'A $0 B $1'), + ('m1', 'C $1 D $0 E'), + ) + keys = 'm' + EX + 'hi' + JF + 'm1' + EX + 'CD' + JF + 'DE' + wanted = 'A C CD D DE E B hi' + + +class RecTabStops_ExpandedInZeroTSTwice_ECR(_VimTest): + snippets = ( + ('m', 'A $0 B $1'), + ('m1', 'C $1 D $0 E'), + ) + keys = 'm' + EX + 'hi' + JF + 'm' + EX + 'again' + JF + 'm1' + \ + EX + 'CD' + JF + 'DE' + wanted = 'A A C CD D DE E B again B hi' + + +class RecTabStops_ExpandedInZeroTSSecondTime_ECR(_VimTest): + snippets = ( + ('m', 'A $0 B $1'), + ('m1', 'C $1 D $0 E'), + ) + keys = 'm' + EX + 'hi' + JF + 'm' + EX + \ + 'm1' + EX + 'CD' + JF + 'DE' + JF + 'AB' + wanted = 'A A AB B C CD D DE E B hi' + + +class RecTabsStops_TypeInZero_ECR(_VimTest): + snippets = ( + ('v', r"\vec{$1}", 'Vector', 'w'), + ('frac', r"\frac{${1:one}}${0:zero}{${2:two}}", 'Fractio', 'w'), + ) + keys = 'v' + EX + 'frac' + EX + 'a' + JF + 'b' + JF + 'frac' + EX + 'aa' + JF + JF + 'cc' + JF + \ + 'hello frac' + EX + JF + JF + 'world' + wanted = r"\vec{\frac{a}\frac{aa}cc{two}{b}}hello \frac{one}world{two}" + + +class RecTabsStops_TypeInZero2_ECR(_VimTest): + snippets = ( + ('m', r"_${0:explicit zero}", 'snip', 'i'), + ) + keys = 'm' + EX + 'hello m' + EX + 'world m' + EX + 'end' + wanted = r"_hello _world _end" + + +class RecTabsStops_BackspaceZero_ECR(_VimTest): + snippets = ( + ('m', r"${1:one}${0:explicit zero}${2:two}", 'snip', 'i'), + ) + keys = 'm' + EX + JF + JF + BS + 'm' + EX + wanted = r"oneoneexplicit zerotwotwo" + + +class RecTabStops_MirrorInnerSnippet_ECR(_VimTest): + snippets = ( + ('m', '[ $1 $2 ] $1'), + ('m1', 'ASnip $1 ASnip $2 ASnip'), + ) + keys = 'm' + EX + 'm1' + EX + 'Hallo' + JF + 'Hi' + \ + JF + 'endone' + JF + 'two' + JF + 'totalend' + wanted = '[ ASnip Hallo ASnip Hi ASnipendone two ] ASnip Hallo ASnip Hi ASnipendonetotalend' + + +class RecTabStops_NotAtBeginningOfTS_ExpectCorrectResult(_VimTest): + snippets = ('m', '[ ${1:first} ${2:sec} ]') + keys = 'm' + EX + 'hello m' + EX + 'hi' + JF + 'two' + JF + 'ups' + JF + 'three' + \ + JF + 'end' + wanted = '[ hello [ hi two ]ups three ]end' + + +class RecTabStops_InNewlineInTabstop_ExpectCorrectResult(_VimTest): + snippets = ('m', '[ ${1:first} ${2:sec} ]') + keys = 'm' + EX + 'hello\nm' + EX + 'hi' + JF + 'two' + JF + 'ups' + JF + 'three' + \ + JF + 'end' + wanted = '[ hello\n[ hi two ]ups three ]end' + + +class RecTabStops_InNewlineInTabstopNotAtBeginOfLine_ECR(_VimTest): + snippets = ('m', '[ ${1:first} ${2:sec} ]') + keys = 'm' + EX + 'hello\nhello again m' + EX + 'hi' + JF + 'two' + \ + JF + 'ups' + JF + 'three' + JF + 'end' + wanted = '[ hello\nhello again [ hi two ]ups three ]end' + + +class RecTabStops_InNewlineMultiline_ECR(_VimTest): + snippets = ('m', 'M START\n$0\nM END') + keys = 'm' + EX + 'm' + EX + wanted = 'M START\nM START\n\nM END\nM END' + + +class RecTabStops_InNewlineManualIndent_ECR(_VimTest): + snippets = ('m', 'M START\n$0\nM END') + keys = 'm' + EX + ' m' + EX + 'hi' + wanted = 'M START\n M START\n hi\n M END\nM END' + + +class RecTabStops_InNewlineManualIndentTextInFront_ECR(_VimTest): + snippets = ('m', 'M START\n$0\nM END') + keys = 'm' + EX + ' hallo m' + EX + 'hi' + wanted = 'M START\n hallo M START\n hi\n M END\nM END' + + +class RecTabStops_InNewlineMultilineWithIndent_ECR(_VimTest): + snippets = ('m', 'M START\n $0\nM END') + keys = 'm' + EX + 'm' + EX + 'hi' + wanted = 'M START\n M START\n hi\n M END\nM END' + + +class RecTabStops_InNewlineMultilineWithNonZeroTS_ECR(_VimTest): + snippets = ('m', 'M START\n $1\nM END -> $0') + keys = 'm' + EX + 'm' + EX + 'hi' + JF + 'hallo' + JF + 'end' + wanted = 'M START\n M START\n hi\n M END -> hallo\n' \ + 'M END -> end' + + +class RecTabStops_BarelyNotLeavingInner_ECR(_VimTest): + snippets = ( + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm' + EX + 'a' + 3 * ARR_L + JF + 'hallo' + \ + JF + 'ups' + JF + 'world' + JF + 'end' + wanted = '[ [ a hallo ]ups world ]end' + + +class RecTabStops_LeavingInner_ECR(_VimTest): + snippets = ( + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm' + EX + 'a' + 4 * ARR_L + JF + 'hallo' + \ + JF + 'world' + wanted = '[ [ a sec ] hallo ]world' + + +class RecTabStops_LeavingInnerInner_ECR(_VimTest): + snippets = ( + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm' + EX + 'm' + EX + 'a' + 4 * ARR_L + JF + 'hallo' + \ + JF + 'ups' + JF + 'world' + JF + 'end' + wanted = '[ [ [ a sec ] hallo ]ups world ]end' + + +class RecTabStops_LeavingInnerInnerTwo_ECR(_VimTest): + snippets = ( + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm' + EX + 'm' + EX + 'a' + 6 * ARR_L + JF + 'hallo' + \ + JF + 'end' + wanted = '[ [ [ a sec ] sec ] hallo ]end' + + +class RecTabStops_ZeroTSisNothingSpecial_ECR(_VimTest): + snippets = ( + ('m1', '[ ${1:first} $0 ${2:sec} ]'), + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm1' + EX + 'one' + JF + 'two' + \ + JF + 'three' + JF + 'four' + JF + 'end' + wanted = '[ [ one three two ] four ]end' + + +class RecTabStops_MirroredZeroTS_ECR(_VimTest): + snippets = ( + ('m1', '[ ${1:first} ${0:Year, some default text} $0 ${2:sec} ]'), + ('m', '[ ${1:first} ${2:sec} ]'), + ) + keys = 'm' + EX + 'm1' + EX + 'one' + JF + 'two' + \ + JF + 'three' + JF + 'four' + JF + 'end' + wanted = '[ [ one three three two ] four ]end' + + +class RecTabStops_ChildTriggerContainsParentTextObjects(_VimTest): + # https://bugs.launchpad.net/bugs/1191617 + files = { 'us/all.snippets': r""" +global !p +def complete(t, opts): + if t: + opts = [ q[len(t):] for q in opts if q.startswith(t) ] + if len(opts) == 0: + return '' + return opts[0] if len(opts) == 1 else "(" + '|'.join(opts) + ')' +def autocomplete_options(t, string, attr=None): + return complete(t[1], [opt for opt in attr if opt not in string]) +endglobal +snippet /form_for(.*){([^|]*)/ "form_for html options" rw! +`!p +auto = autocomplete_options(t, match.group(2), attr=["id: ", "class: ", "title: "]) +snip.rv = "form_for" + match.group(1) + "{"`$1`!p if (snip.c != auto) : snip.rv=auto` +endsnippet +"""} + keys = 'form_for user, namespace: some_namespace, html: {i' + EX + 'i' + EX + wanted = 'form_for user, namespace: some_namespace, html: {(id: |class: |title: )d: ' +# End: Recursive (Nested) Snippets #}}} diff --git a/test/test_Selection.py b/test/test_Selection.py new file mode 100644 index 0000000..6eeb3a9 --- /dev/null +++ b/test/test_Selection.py @@ -0,0 +1,125 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# Unmap SelectMode Mappings {{{# +# Test for bug 427298 # + + +class _SelectModeMappings(_VimTest): + snippets = ('test', '${1:World}') + keys = 'test' + EX + 'Hello' + wanted = 'Hello' + maps = ('', '') + buffer_maps = ('', '') + do_unmapping = True + ignores = [] + + def _extra_vim_config(self, vim_config): + vim_config.append( + ':let g:UltiSnipsRemoveSelectModeMappings=%i' % int( + self.do_unmapping)) + vim_config.append( + ':let g:UltiSnipsMappingsToIgnore=%s' % + repr( + self.ignores)) + + if not isinstance(self.maps[0], tuple): + self.maps = (self.maps,) + if not isinstance(self.buffer_maps[0], tuple): + self.buffer_maps = (self.buffer_maps,) + + for key, m in self.maps: + if not len(key): + continue + vim_config.append(':smap %s %s' % (key, m)) + for key, m in self.buffer_maps: + if not len(key): + continue + vim_config.append(':smap %s %s' % (key, m)) + + +class SelectModeMappings_RemoveBeforeSelecting_ECR(_SelectModeMappings): + maps = ('H', 'x') + wanted = 'Hello' + + +class SelectModeMappings_DisableRemoveBeforeSelecting_ECR(_SelectModeMappings): + do_unmapping = False + maps = ('H', 'x') + wanted = 'xello' + + +class SelectModeMappings_IgnoreMappings_ECR(_SelectModeMappings): + ignores = ['e'] + maps = ('H', 'x'), ('e', 'l') + wanted = 'Hello' + + +class SelectModeMappings_IgnoreMappings1_ECR(_SelectModeMappings): + ignores = ['H'] + maps = ('H', 'x'), ('e', 'l') + wanted = 'xello' + + +class SelectModeMappings_IgnoreMappings2_ECR(_SelectModeMappings): + ignores = ['e', 'H'] + maps = ('e', 'l'), ('H', 'x') + wanted = 'xello' + + +class SelectModeMappings_BufferLocalMappings_ECR(_SelectModeMappings): + buffer_maps = ('H', 'blah') + wanted = 'Hello' +# End: Unmap SelectMode Mappings #}}} + +# Exclusive Selection {{{# + + +class _ES_Base(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set selection=exclusive') + + +class ExclusiveSelection_SimpleTabstop_Test(_ES_Base): + snippets = ('test', 'h${1:blah}w $1') + keys = 'test' + EX + 'ui' + JF + wanted = 'huiw ui' + + +class ExclusiveSelection_RealWorldCase_Test(_ES_Base): + snippets = ('for', + """for ($${1:i} = ${2:0}; $$1 < ${3:count}; $$1${4:++}) { + ${5:// code} +}""") + keys = 'for' + EX + 'k' + JF + wanted = """for ($k = 0; $k < count; $k++) { + // code +}""" +# End: Exclusive Selection #}}} + +# Old Selection {{{# + + +class _OS_Base(_VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set selection=old') + + +class OldSelection_SimpleTabstop_Test(_OS_Base): + snippets = ('test', 'h${1:blah}w $1') + keys = 'test' + EX + 'ui' + JF + wanted = 'huiw ui' + + +class OldSelection_RealWorldCase_Test(_OS_Base): + snippets = ('for', + """for ($${1:i} = ${2:0}; $$1 < ${3:count}; $$1${4:++}) { + ${5:// code} +}""") + keys = 'for' + EX + 'k' + JF + wanted = """for ($k = 0; $k < count; $k++) { + // code +}""" +# End: Old Selection #}}} diff --git a/test/test_SnipMate.py b/test/test_SnipMate.py new file mode 100644 index 0000000..f8cdbde --- /dev/null +++ b/test/test_SnipMate.py @@ -0,0 +1,176 @@ +# encoding: utf-8 +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# snipMate support {{{# + + +class snipMate_SimpleSnippet(_VimTest): + files = { 'snippets/_.snippets': """ +snippet hello +\tThis is a test snippet +\t# With a comment"""} + keys = 'hello' + EX + wanted = 'This is a test snippet\n# With a comment' + +class snipMate_Disabled(_VimTest): + files = { 'snippets/_.snippets': """ +snippet hello +\tThis is a test snippet +\t# With a comment"""} + keys = 'hello' + EX + wanted = 'hello' + EX + + def _extra_vim_config(self, vim_config): + vim_config.append("let g:UltiSnipsEnableSnipMate=0"); + + +class snipMate_OtherFiletype(_VimTest): + files = { 'snippets/blubi.snippets': """ +snippet hello +\tworked"""} + keys = 'hello' + EX + ESC + ':set ft=blubi\nohello' + EX + wanted = 'hello' + EX + '\nworked' + + +class snipMate_MultiMatches(_VimTest): + files = { 'snippets/_.snippets': """ +snippet hello The first snippet." +\tone +snippet hello The second snippet. +\ttwo"""} + keys = 'hello' + EX + '2\n' + wanted = 'two' + + +class snipMate_SimpleSnippetSubDirectory(_VimTest): + files = { 'snippets/_/blub.snippets': """ +snippet hello +\tThis is a test snippet"""} + keys = 'hello' + EX + wanted = 'This is a test snippet' + + +class snipMate_SimpleSnippetInSnippetFile(_VimTest): + files = { + 'snippets/_/hello.snippet': """This is a stand alone snippet""", + 'snippets/_/hello1.snippet': """This is two stand alone snippet""", + 'snippets/_/hello2/this_is_my_cool_snippet.snippet': """Three""", + } + keys = 'hello' + EX + '\nhello1' + EX + '\nhello2' + EX + wanted = 'This is a stand alone snippet\nThis is two stand alone snippet\nThree' + + +class snipMate_Interpolation(_VimTest): + files = { 'snippets/_.snippets': """ +snippet test +\tla`printf('c%02d', 3)`lu"""} + keys = 'test' + EX + wanted = 'lac03lu' + + +class snipMate_InterpolationWithSystem(_VimTest): + files = { 'snippets/_.snippets': """ +snippet test +\tla`system('echo -ne öäü')`lu"""} + keys = 'test' + EX + wanted = 'laöäülu' + + +class snipMate_TestMirrors(_VimTest): + files = { 'snippets/_.snippets': """ +snippet for +\tfor (${2:i}; $2 < ${1:count}; $1++) { +\t\t${4} +\t}"""} + keys = 'for' + EX + 'blub' + JF + 'j' + JF + 'hi' + wanted = 'for (j; j < blub; blub++) {\n\thi\n}' + + +class snipMate_TestNoBraceTabstops(_VimTest): + files = { 'snippets/_.snippets': """ +snippet test +\t$1 is $2"""} + keys = 'test' + EX + 'blub' + JF + 'blah' + wanted = 'blub is blah' + + +class snipMate_TestNoBraceTabstopsAndMirrors(_VimTest): + files = { 'snippets/_.snippets': """ +snippet test +\t$1 is $1, $2 is ${2}"""} + keys = 'test' + EX + 'blub' + JF + 'blah' + wanted = 'blub is blub, blah is blah' + + +class snipMate_TestMirrorsInPlaceholders(_VimTest): + files = { 'snippets/_.snippets': """ +snippet opt +\t"""} + keys = 'opt' + EX + 'some' + JF + JF + 'ende' + wanted = """ende""" + + +class snipMate_TestMirrorsInPlaceholders_Overwrite(_VimTest): + files = { 'snippets/_.snippets': """ +snippet opt +\t"""} + keys = 'opt' + EX + 'some' + JF + 'not' + JF + 'ende' + wanted = """ende""" + + +class snipMate_Visual_Simple(_VimTest): + files = { 'snippets/_.snippets': """ +snippet v +\th${VISUAL}b"""} + keys = 'blablub' + ESC + '0v6l' + EX + 'v' + EX + wanted = 'hblablubb' + + +class snipMate_NoNestedTabstops(_VimTest): + files = { 'snippets/_.snippets': """ +snippet test +\th$${1:${2:blub}}$$"""} + keys = 'test' + EX + JF + 'hi' + wanted = 'h$${2:blub}$$hi' + + +class snipMate_Extends(_VimTest): + files = { 'snippets/a.snippets': """ +extends b +snippet test +\tblub""", 'snippets/b.snippets': """ +snippet test1 +\tblah""" + } + keys = ESC + ':set ft=a\n' + 'itest1' + EX + wanted = 'blah' + + +class snipMate_EmptyLinesContinueSnippets(_VimTest): + files = { 'snippets/_.snippets': """ +snippet test +\tblub + +\tblah + +snippet test1 +\ta""" + } + keys = 'test' + EX + wanted = 'blub\n\nblah\n' + + +class snipMate_OverwrittenByRegExpTrigger(_VimTest): + files = { 'snippets/_.snippets': """ +snippet def +\tsnipmate +""", + 'us/all.snippets': r""" +snippet "(de)?f" "blub" r +ultisnips +endsnippet +""" } + keys = 'def' + EX + wanted = 'ultisnips' +# End: snipMate support #}}} diff --git a/test/test_SnippetActions.py b/test/test_SnippetActions.py new file mode 100644 index 0000000..403e234 --- /dev/null +++ b/test/test_SnippetActions.py @@ -0,0 +1,370 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + + +class SnippetActions_PreActionModifiesBuffer(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line:snip.line] = ['\n']" + snippet a "desc" "True" e + abc + endsnippet + """} + keys = 'a' + EX + wanted = '\nabc' + + +class SnippetActions_PostActionModifiesBuffer(_VimTest): + files = { 'us/all.snippets': r""" + post_expand "snip.buffer[snip.line+1:snip.line+1] = ['\n']" + snippet a "desc" "True" e + abc + endsnippet + """} + keys = 'a' + EX + wanted = 'abc\n' + +class SnippetActions_ErrorOnBufferModificationThroughCommand(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "vim.command('normal O')" + snippet a "desc" "True" e + abc + endsnippet + """} + keys = 'a' + EX + expected_error = 'changes are untrackable' + + +class SnippetActions_ErrorOnModificationSnippetLine(_VimTest): + files = { 'us/all.snippets': r""" + post_expand "vim.command('normal dd')" + snippet i "desc" "True" e + if: + $1 + endsnippet + """} + keys = 'i' + EX + expected_error = 'line under the cursor was modified' + + +class SnippetActions_EnsureIndent(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line] = ' '*4; snip.cursor[1] = 4" + snippet i "desc" "True" e + if: + $1 + endsnippet + """} + keys = '\ni' + EX + 'i' + EX + 'x' + wanted = """ + if: + if: + x""" + + +class SnippetActions_PostActionCanUseSnippetRange(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def ensure_newlines(start, end): + snip.buffer[start[0]:start[0]] = ['\n'] * 2 + snip.buffer[end[0]+1:end[0]+1] = ['\n'] * 1 + endglobal + + post_expand "ensure_newlines(snip.snippet_start, snip.snippet_end)" + snippet i "desc" + if + $1 + else + $2 + end + endsnippet + """} + keys = '\ni' + EX + 'x' + JF + 'y' + wanted = """ + + +if + x +else + y +end +""" + + +class SnippetActions_CanModifyParentBody(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def ensure_newlines(start, end): + snip.buffer[start[0]:start[0]] = ['\n'] * 2 + endglobal + + post_expand "ensure_newlines(snip.snippet_start, snip.snippet_end)" + snippet i "desc" + if + $1 + else + $2 + end + endsnippet + """} + keys = '\ni' + EX + 'i' + EX + 'x' + JF + 'y' + JF + JF + 'z' + wanted = """ + + +if + + + if + x + else + y + end +else + z +end""" + + +class SnippetActions_MoveParentSnippetFromChildInPreAction(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def insert_import(): + snip.buffer[2:2] = ['import smthing', ''] + endglobal + + pre_expand "insert_import()" + snippet p "desc" + print(smthing.traceback()) + endsnippet + + snippet i "desc" + if + $1 + else + $2 + end + endsnippet + """} + keys = 'i' + EX + 'p' + EX + JF + 'z' + wanted = """import smthing + +if + print(smthing.traceback()) +else + z +end""" + + +class SnippetActions_CanExpandSnippetInDifferentPlace(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def expand_after_if(snip): + snip.buffer[snip.line] = snip.buffer[snip.line][:snip.column] + \ + snip.buffer[snip.line][snip.column+1:] + snip.cursor[1] = snip.buffer[snip.line].index('if ')+3 + endglobal + + pre_expand "expand_after_if(snip)" + snippet n "append not to if" w + not $0 + endsnippet + + snippet i "if cond" w + if $1: $2 + endsnippet + """} + keys = 'i' + EX + 'blah' + JF + 'n' + EX + JF + 'pass' + wanted = """if not blah: pass""" + + +class SnippetActions_MoveVisual(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def extract_method(snip): + del snip.buffer[snip.line] + snip.buffer[len(snip.buffer)-1:len(snip.buffer)-1] = [''] + snip.cursor.set(len(snip.buffer)-2, 0) + endglobal + + pre_expand "extract_method(snip)" + snippet n "append not to if" w + def $1: + ${VISUAL} + + endsnippet + """} + + keys = """ +def a: + x() + y() + z()""" + ESC + 'kVk' + EX + 'n' + EX + 'b' + + wanted = """ +def a: + z() + +def b: + x() + y()""" + + +class SnippetActions_CanMirrorTabStopsOutsideOfSnippet(_VimTest): + files = { 'us/all.snippets': r""" + post_jump "snip.buffer[2] = 'debug({})'.format(snip.tabstops[1].current_text)" + snippet i "desc" + if $1: + $2 + endsnippet + """} + keys = """ +--- +i""" + EX + "test(some(complex(cond(a))))" + JF + "x" + wanted = """debug(test(some(complex(cond(a))))) +--- +if test(some(complex(cond(a)))): + x""" + + +class SnippetActions_CanExpandAnonSnippetInJumpAction(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def expand_anon(snip): + if snip.tabstop == 0: + snip.expand_anon("a($2, $1)") + endglobal + + post_jump "expand_anon(snip)" + snippet i "desc" + if ${1:cond}: + $0 + endsnippet + """} + keys = "i" + EX + "x" + JF + "1" + JF + "2" + JF + ";" + wanted = """if x: + a(2, 1);""" + + +class SnippetActions_CanExpandAnonSnippetInJumpActionWhileSelected(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def expand_anon(snip): + if snip.tabstop == 0: + snip.expand_anon(" // a($2, $1)") + endglobal + + post_jump "expand_anon(snip)" + snippet i "desc" + if ${1:cond}: + ${2:pass} + endsnippet + """} + keys = "i" + EX + "x" + JF + JF + "1" + JF + "2" + JF + ";" + wanted = """if x: + pass // a(2, 1);""" + + +class SnippetActions_CanUseContextFromContextMatch(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line:snip.line] = [snip.context]" + snippet i "desc" "'some context'" e + body + endsnippet + """} + keys = "i" + EX + wanted = """some context +body""" + +class SnippetActions_CanExpandAnonSnippetOnFirstJump(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def expand_new_snippet_on_first_jump(snip): + if snip.tabstop == 1: + snip.expand_anon("some_check($1, $2, $3)") + endglobal + + post_jump "expand_new_snippet_on_first_jump(snip)" + snippet "test" "test new features" "True" bwre + if $1: $2 + endsnippet + """} + keys = "test" + EX + "1" + JF + "2" + JF + "3" + JF + " or 4" + JF + "5" + wanted = """if some_check(1, 2, 3) or 4: 5""" + +class SnippetActions_CanExpandAnonOnPreExpand(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('totally_different($2, $1)')" + snippet test "test new features" wb + endsnippet + """} + keys = "test" + EX + "1" + JF + "2" + JF + "3" + wanted = """totally_different(2, 1)3""" + +class SnippetActions_CanEvenWrapSnippetInPreAction(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('some_wrapper($1): $2')" + snippet test "test new features" wb + wrapme($2, $1) + endsnippet + """} + keys = "test" + EX + "1" + JF + "2" + JF + "3" + JF + "4" + wanted = """some_wrapper(wrapme(2, 1)3): 4""" + +class SnippetActions_CanVisuallySelectFirstPlaceholderInAnonSnippetInPre(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('${1:asd}, ${2:blah}')" + snippet test "test new features" wb + endsnippet + """} + keys = "test" + EX + "1" + JF + "2" + wanted = """1, 2""" + +class SnippetActions_UseCorrectJumpActions(_VimTest): + files = { 'us/all.snippets': r""" + post_jump "snip.buffer[-2:-2]=['a' + str(snip.tabstop)]" + snippet a "a" wb + $1 { + $2 + } + endsnippet + + snippet b "b" wb + bbb + endsnippet + + post_jump "snip.buffer[-2:-2]=['c' + str(snip.tabstop)]" + snippet c "c" w + $1 : $2 : $3 + endsnippet + """} + keys = "a" + EX + "1" + JF + "b" + EX + " c" + EX + "2" + JF + "3" + JF + "4" + JF + JF + wanted = """1 { +bbb 2 : 3 : 4 +} +a1 +a2 +c1 +c2 +c3 +c0 +a0""" + +class SnippetActions_PostActionModifiesCharAfterSnippet(_VimTest): + files = { 'us/all.snippets': r""" + post_expand "snip.buffer[snip.snippet_end[0]] = snip.buffer[snip.snippet_end[0]][:-1]" + snippet a "desc" i + ($1) + endsnippet + """} + keys = '[]' + ARR_L + 'a' + EX + '1' + JF + '2' + wanted = '[(1)2' + + +class SnippetActions_PostActionModifiesLineAfterSnippet(_VimTest): + files = { 'us/all.snippets': r""" + post_expand "snip.buffer[snip.snippet_end[0]+1:snip.snippet_end[0]+2] = []" + snippet a "desc" + 1: $1 + $0 + endsnippet + """} + keys = '\n3' + ARR_U + 'a' + EX + '1' + JF + '2' + wanted = '1: 1\n2' diff --git a/test/test_SnippetOptions.py b/test/test_SnippetOptions.py new file mode 100644 index 0000000..cd18b6a --- /dev/null +++ b/test/test_SnippetOptions.py @@ -0,0 +1,357 @@ +# encoding: utf-8 +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * +from test.util import running_on_windows + +# Snippet Options {{{# + + +class SnippetOptions_OnlyExpandWhenWSInFront_Expand(_VimTest): + snippets = ('test', 'Expand me!', '', 'b') + keys = 'test' + EX + wanted = 'Expand me!' + + +class SnippetOptions_OnlyExpandWhenWSInFront_Expand2(_VimTest): + snippets = ('test', 'Expand me!', '', 'b') + keys = ' test' + EX + wanted = ' Expand me!' + + +class SnippetOptions_OnlyExpandWhenWSInFront_DontExpand(_VimTest): + snippets = ('test', 'Expand me!', '', 'b') + keys = 'a test' + EX + wanted = 'a test' + EX + + +class SnippetOptions_OnlyExpandWhenWSInFront_OneWithOneWO(_VimTest): + snippets = ( + ('test', 'Expand me!', '', 'b'), + ('test', 'not at beginning', '', ''), + ) + keys = 'a test' + EX + wanted = 'a not at beginning' + + +class SnippetOptions_OnlyExpandWhenWSInFront_OneWithOneWOChoose(_VimTest): + snippets = ( + ('test', 'Expand me!', '', 'b'), + ('test', 'not at beginning', '', ''), + ) + keys = ' test' + EX + '1\n' + wanted = ' Expand me!' + + +class SnippetOptions_ExpandInwordSnippets_SimpleExpand(_VimTest): + snippets = (('test', 'Expand me!', '', 'i'), ) + keys = 'atest' + EX + wanted = 'aExpand me!' + + +class SnippetOptions_ExpandInwordSnippets_ExpandSingle(_VimTest): + snippets = (('test', 'Expand me!', '', 'i'), ) + keys = 'test' + EX + wanted = 'Expand me!' + + +class SnippetOptions_ExpandInwordSnippetsWithOtherChars_Expand(_VimTest): + snippets = (('test', 'Expand me!', '', 'i'), ) + keys = '$test' + EX + wanted = '$Expand me!' + + +class SnippetOptions_ExpandInwordSnippetsWithOtherChars_Expand2(_VimTest): + snippets = (('test', 'Expand me!', '', 'i'), ) + keys = '-test' + EX + wanted = '-Expand me!' + + +class SnippetOptions_ExpandInwordSnippetsWithOtherChars_Expand3(_VimTest): + skip_if = lambda self: running_on_windows() + snippets = (('test', 'Expand me!', '', 'i'), ) + keys = 'ßßtest' + EX + wanted = 'ßßExpand me!' + + +class _SnippetOptions_ExpandWordSnippets(_VimTest): + snippets = (('test', 'Expand me!', '', 'w'), ) + + +class SnippetOptions_ExpandWordSnippets_NormalExpand( + _SnippetOptions_ExpandWordSnippets): + keys = 'test' + EX + wanted = 'Expand me!' + + +class SnippetOptions_ExpandWordSnippets_NoExpand( + _SnippetOptions_ExpandWordSnippets): + keys = 'atest' + EX + wanted = 'atest' + EX + + +class SnippetOptions_ExpandWordSnippets_ExpandSuffix( + _SnippetOptions_ExpandWordSnippets): + keys = 'a-test' + EX + wanted = 'a-Expand me!' + + +class SnippetOptions_ExpandWordSnippets_ExpandSuffix2( + _SnippetOptions_ExpandWordSnippets): + keys = 'a(test' + EX + wanted = 'a(Expand me!' + + +class SnippetOptions_ExpandWordSnippets_ExpandSuffix3( + _SnippetOptions_ExpandWordSnippets): + keys = '[[test' + EX + wanted = '[[Expand me!' + + +class _No_Tab_Expand(_VimTest): + snippets = ('test', '\t\tExpand\tme!\t', '', 't') + + +class No_Tab_Expand_Simple(_No_Tab_Expand): + keys = 'test' + EX + wanted = '\t\tExpand\tme!\t' + + +class No_Tab_Expand_Leading_Spaces(_No_Tab_Expand): + keys = ' test' + EX + wanted = ' \t\tExpand\tme!\t' + + +class No_Tab_Expand_Leading_Tabs(_No_Tab_Expand): + keys = '\ttest' + EX + wanted = '\t\t\tExpand\tme!\t' + + +class No_Tab_Expand_No_TS(_No_Tab_Expand): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=3') + vim_config.append('set sts=3') + keys = 'test' + EX + wanted = '\t\tExpand\tme!\t' + + +class No_Tab_Expand_ET(_No_Tab_Expand): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=3') + vim_config.append('set expandtab') + keys = 'test' + EX + wanted = '\t\tExpand\tme!\t' + + +class No_Tab_Expand_ET_Leading_Spaces(_No_Tab_Expand): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=3') + vim_config.append('set expandtab') + keys = ' test' + EX + wanted = ' \t\tExpand\tme!\t' + + +class No_Tab_Expand_ET_SW(_No_Tab_Expand): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=8') + vim_config.append('set expandtab') + keys = 'test' + EX + wanted = '\t\tExpand\tme!\t' + + +class No_Tab_Expand_ET_SW_TS(_No_Tab_Expand): + + def _extra_vim_config(self, vim_config): + vim_config.append('set sw=3') + vim_config.append('set sts=3') + vim_config.append('set ts=3') + vim_config.append('set expandtab') + keys = 'test' + EX + wanted = '\t\tExpand\tme!\t' + + +class _TabExpand_RealWorld(object): + snippets = ('hi', + r"""hi +`!p snip.rv="i1\n" +snip.rv += snip.mkline("i1\n") +snip.shift(1) +snip.rv += snip.mkline("i2\n") +snip.unshift(2) +snip.rv += snip.mkline("i0\n") +snip.shift(3) +snip.rv += snip.mkline("i3")` +snip.rv = repr(snip.rv) +End""") + + +class No_Tab_Expand_RealWorld(_TabExpand_RealWorld, _VimTest): + + def _extra_vim_config(self, vim_config): + vim_config.append('set noexpandtab') + keys = '\t\thi' + EX + wanted = """\t\thi +\t\ti1 +\t\ti1 +\t\t\ti2 +\ti0 +\t\t\t\ti3 +\t\tsnip.rv = repr(snip.rv) +\t\tEnd""" + + +class SnippetOptions_Regex_Expand(_VimTest): + snippets = ('(test)', 'Expand me!', '', 'r') + keys = 'test' + EX + wanted = 'Expand me!' + + +class SnippetOptions_Regex_WithSpace(_VimTest): + snippets = ('test ', 'Expand me!', '', 'r') + keys = 'test ' + EX + wanted = 'Expand me!' + + +class SnippetOptions_Regex_Multiple(_VimTest): + snippets = ('(test *)+', 'Expand me!', '', 'r') + keys = 'test test test' + EX + wanted = 'Expand me!' + + +class _Regex_Self(_VimTest): + snippets = ('((?<=\W)|^)(\.)', 'self.', '', 'r') + + +class SnippetOptions_Regex_Self_Start(_Regex_Self): + keys = '.' + EX + wanted = 'self.' + + +class SnippetOptions_Regex_Self_Space(_Regex_Self): + keys = ' .' + EX + wanted = ' self.' + + +class SnippetOptions_Regex_Self_TextAfter(_Regex_Self): + keys = ' .a' + EX + wanted = ' .a' + EX + + +class SnippetOptions_Regex_Self_TextBefore(_Regex_Self): + keys = 'a.' + EX + wanted = 'a.' + EX + + +class SnippetOptions_Regex_PythonBlockMatch(_VimTest): + snippets = (r"([abc]+)([def]+)", r"""`!p m = match +snip.rv += m.group(2) +snip.rv += m.group(1) +`""", '', 'r') + keys = 'test cabfed' + EX + wanted = 'test fedcab' + + +class SnippetOptions_Regex_PythonBlockNoMatch(_VimTest): + snippets = (r"cabfed", r"""`!p snip.rv = match or "No match"`""") + keys = 'test cabfed' + EX + wanted = 'test No match' +# Tests for Bug #691575 + + +class SnippetOptions_Regex_SameLine_Long_End(_VimTest): + snippets = ('(test.*)', 'Expand me!', '', 'r') + keys = 'test test abc' + EX + wanted = 'Expand me!' + + +class SnippetOptions_Regex_SameLine_Long_Start(_VimTest): + snippets = ('(.*test)', 'Expand me!', '', 'r') + keys = 'abc test test' + EX + wanted = 'Expand me!' + + +class SnippetOptions_Regex_SameLine_Simple(_VimTest): + snippets = ('(test)', 'Expand me!', '', 'r') + keys = 'abc test test' + EX + wanted = 'abc test Expand me!' + + +class MultiWordSnippet_Simple(_VimTest): + snippets = ('test me', 'Expand me!') + keys = 'test me' + EX + wanted = 'Expand me!' + + +class MultiWord_SnippetOptions_OnlyExpandWhenWSInFront_Expand(_VimTest): + snippets = ('test it', 'Expand me!', '', 'b') + keys = 'test it' + EX + wanted = 'Expand me!' + + +class MultiWord_SnippetOptions_OnlyExpandWhenWSInFront_Expand2(_VimTest): + snippets = ('test it', 'Expand me!', '', 'b') + keys = ' test it' + EX + wanted = ' Expand me!' + + +class MultiWord_SnippetOptions_OnlyExpandWhenWSInFront_DontExpand(_VimTest): + snippets = ('test it', 'Expand me!', '', 'b') + keys = 'a test it' + EX + wanted = 'a test it' + EX + + +class MultiWord_SnippetOptions_OnlyExpandWhenWSInFront_OneWithOneWO(_VimTest): + snippets = ( + ('test it', 'Expand me!', '', 'b'), + ('test it', 'not at beginning', '', ''), + ) + keys = 'a test it' + EX + wanted = 'a not at beginning' + + +class MultiWord_SnippetOptions_OnlyExpandWhenWSInFront_OneWithOneWOChoose( + _VimTest): + snippets = ( + ('test it', 'Expand me!', '', 'b'), + ('test it', 'not at beginning', '', ''), + ) + keys = ' test it' + EX + '1\n' + wanted = ' Expand me!' + + +class MultiWord_SnippetOptions_ExpandInwordSnippets_SimpleExpand(_VimTest): + snippets = (('test it', 'Expand me!', '', 'i'), ) + keys = 'atest it' + EX + wanted = 'aExpand me!' + + +class MultiWord_SnippetOptions_ExpandInwordSnippets_ExpandSingle(_VimTest): + snippets = (('test it', 'Expand me!', '', 'i'), ) + keys = 'test it' + EX + wanted = 'Expand me!' + + +class _MultiWord_SnippetOptions_ExpandWordSnippets(_VimTest): + snippets = (('test it', 'Expand me!', '', 'w'), ) + + +class MultiWord_SnippetOptions_ExpandWordSnippets_NormalExpand( + _MultiWord_SnippetOptions_ExpandWordSnippets): + keys = 'test it' + EX + wanted = 'Expand me!' + + +class MultiWord_SnippetOptions_ExpandWordSnippets_NoExpand( + _MultiWord_SnippetOptions_ExpandWordSnippets): + keys = 'atest it' + EX + wanted = 'atest it' + EX + + +class MultiWord_SnippetOptions_ExpandWordSnippets_ExpandSuffix( + _MultiWord_SnippetOptions_ExpandWordSnippets): + keys = 'a-test it' + EX + wanted = 'a-Expand me!' +# Snippet Options #}}} diff --git a/test/test_SnippetPriorities.py b/test/test_SnippetPriorities.py new file mode 100644 index 0000000..be595cc --- /dev/null +++ b/test/test_SnippetPriorities.py @@ -0,0 +1,158 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import EX, ESC + +# Snippet Priority {{{# + + +class SnippetPriorities_MultiWordTriggerOverwriteExisting(_VimTest): + snippets = ( + ('test me', '${1:Hallo}', 'Types Hallo'), + ('test me', '${1:World}', 'Types World'), + ('test me', 'We overwrite', 'Overwrite the two', '', 1), + ) + keys = 'test me' + EX + wanted = 'We overwrite' + + +class SnippetPriorities_DoNotCareAboutNonMatchings(_VimTest): + snippets = ( + ('test1', 'Hallo', 'Types Hallo'), + ('test2', 'We overwrite', 'Overwrite the two', '', 1), + ) + keys = 'test1' + EX + wanted = 'Hallo' + + +class SnippetPriorities_OverwriteExisting(_VimTest): + snippets = ( + ('test', '${1:Hallo}', 'Types Hallo'), + ('test', '${1:World}', 'Types World'), + ('test', 'We overwrite', 'Overwrite the two', '', 1), + ) + keys = 'test' + EX + wanted = 'We overwrite' + + +class SnippetPriorities_OverwriteTwice_ECR(_VimTest): + snippets = ( + ('test', '${1:Hallo}', 'Types Hallo'), + ('test', '${1:World}', 'Types World'), + ('test', 'We overwrite', 'Overwrite the two', '', 1), + ('test', 'again', 'Overwrite again', '', 2), + ) + keys = 'test' + EX + wanted = 'again' + + +class SnippetPriorities_OverwriteThenChoose_ECR(_VimTest): + snippets = ( + ('test', '${1:Hallo}', 'Types Hallo'), + ('test', '${1:World}', 'Types World'), + ('test', 'We overwrite', 'Overwrite the two', '', 1), + ('test', 'No overwrite', 'Not overwritten', '', 1), + ) + keys = 'test' + EX + '1\n\n' + 'test' + EX + '2\n' + wanted = 'We overwrite\nNo overwrite' + + +class SnippetPriorities_AddedHasHigherThanFile(_VimTest): + files = { 'us/all.snippets': r""" + snippet test "Test Snippet" b + This is a test snippet + endsnippet + """} + snippets = ( + ('test', 'We overwrite', 'Overwrite the two', '', 1), + ) + keys = 'test' + EX + wanted = 'We overwrite' + + +class SnippetPriorities_FileHasHigherThanAdded(_VimTest): + files = { 'us/all.snippets': r""" + snippet test "Test Snippet" b + This is a test snippet + endsnippet + """} + snippets = ( + ('test', 'We do not overwrite', 'Overwrite the two', '', -1), + ) + keys = 'test' + EX + wanted = 'This is a test snippet' + + +class SnippetPriorities_FileHasHigherThanAdded_neg_prio(_VimTest): + files = { 'us/all.snippets': r""" + priority -3 + snippet test "Test Snippet" b + This is a test snippet + endsnippet + """} + snippets = ( + ('test', 'We overwrite', 'Overwrite the two', '', -5), + ) + keys = 'test' + EX + wanted = 'This is a test snippet' + + +class SnippetPriorities_SimpleClear(_VimTest): + files = { + 'us/all.snippets': r""" + priority 1 + clearsnippets + priority -1 + snippet test "Test Snippet" + Should not expand to this. + endsnippet + """ + } + keys = 'test' + EX + wanted = 'test' + EX + + +class SnippetPriorities_SimpleClear2(_VimTest): + files = { + 'us/all.snippets': r""" + clearsnippets + snippet test "Test snippet" + Should not expand to this. + endsnippet + """ + } + keys = 'test' + EX + wanted = 'test' + EX + + +class SnippetPriorities_ClearedByParent(_VimTest): + files = { + 'us/p.snippets': r""" + clearsnippets + """, + 'us/c.snippets': r""" + extends p + snippet test "Test snippets" + Should not expand to this. + endsnippet + """ + } + keys = ESC + ':set ft=c\n' + 'itest' + EX + wanted = 'test' + EX + + +class SnippetPriorities_ClearedByChild(_VimTest): + files = { + 'us/p.snippets': r""" + snippet test "Test snippets" + Should only expand in p. + endsnippet + """, + 'us/c.snippets': r""" + extends p + clearsnippets + """ + } + keys = (ESC + ':set ft=p\n' + 'itest' + EX + '\n' + + ESC + ':set ft=c\n' + 'itest' + EX + ESC + ':set ft=p') + wanted = 'Should only expand in p.\ntest' + EX + +# End: Snippet Priority #}}} diff --git a/test/test_TabStop.py b/test/test_TabStop.py new file mode 100644 index 0000000..c9e7648 --- /dev/null +++ b/test/test_TabStop.py @@ -0,0 +1,408 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + + +class TabStopSimpleReplace_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo ${0:End} ${1:Beginning}') + keys = 'hallo' + EX + 'na' + JF + 'Du Nase' + wanted = 'hallo Du Nase na' + + +class TabStopSimpleReplaceZeroLengthTabstops_ExpectCorrectResult(_VimTest): + snippets = ('test', r":latex:\`$1\`$0") + keys = 'test' + EX + 'Hello' + JF + 'World' + wanted = ':latex:`Hello`World' + + +class TabStopSimpleReplaceReversed_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo ${1:End} ${0:Beginning}') + keys = 'hallo' + EX + 'na' + JF + 'Du Nase' + wanted = 'hallo na Du Nase' + + +class TabStopSimpleReplaceSurrounded_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo ${0:End} a small feed') + keys = 'hallo' + EX + 'Nase' + wanted = 'hallo Nase a small feed' + + +class TabStopSimpleReplaceSurrounded1_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo $0 a small feed') + keys = 'hallo' + EX + 'Nase' + wanted = 'hallo Nase a small feed' + + +class TabStop_Exit_ExpectCorrectResult(_VimTest): + snippets = ('echo', '$0 run') + keys = 'echo' + EX + 'test' + wanted = 'test run' + + +class TabStopNoReplace_ExpectCorrectResult(_VimTest): + snippets = ('echo', 'echo ${1:Hallo}') + keys = 'echo' + EX + wanted = 'echo Hallo' + + +class TabStop_EscapingCharsBackticks(_VimTest): + snippets = ('test', r"snip \` literal") + keys = 'test' + EX + wanted = 'snip ` literal' + + +class TabStop_EscapingCharsDollars(_VimTest): + snippets = ('test', r"snip \$0 $$0 end") + keys = 'test' + EX + 'hi' + wanted = 'snip $0 $hi end' + + +class TabStop_EscapingCharsDollars1(_VimTest): + snippets = ('test', r"a\${1:literal}") + keys = 'test' + EX + wanted = 'a${1:literal}' + + +class TabStop_EscapingCharsDollars_BeginningOfLine(_VimTest): + snippets = ('test', '\n\\${1:literal}') + keys = 'test' + EX + wanted = '\n${1:literal}' + + +class TabStop_EscapingCharsDollars_BeginningOfDefinitionText(_VimTest): + snippets = ('test', '\\${1:literal}') + keys = 'test' + EX + wanted = '${1:literal}' + + +class TabStop_EscapingChars_Backslash(_VimTest): + snippets = ('test', r"This \ is a backslash!") + keys = 'test' + EX + wanted = 'This \\ is a backslash!' + + +class TabStop_EscapingChars_Backslash2(_VimTest): + snippets = ('test', r"This is a backslash \\ done") + keys = 'test' + EX + wanted = r"This is a backslash \ done" + + +class TabStop_EscapingChars_Backslash3(_VimTest): + snippets = ('test', r"These are two backslashes \\\\ done") + keys = 'test' + EX + wanted = r"These are two backslashes \\ done" + + +class TabStop_EscapingChars_Backslash4(_VimTest): + # Test for bug 746446 + snippets = ('test', r"\\$1{$2}") + keys = 'test' + EX + 'hello' + JF + 'world' + wanted = r"\hello{world}" + + +class TabStop_EscapingChars_RealLife(_VimTest): + snippets = ('test', r"usage: \`basename \$0\` ${1:args}") + keys = 'test' + EX + '[ -u -v -d ]' + wanted = 'usage: `basename $0` [ -u -v -d ]' + + +class TabStopEscapingWhenSelected_ECR(_VimTest): + snippets = ('test', 'snip ${1:default}') + keys = 'test' + EX + ESC + '0ihi' + wanted = 'hisnip default' + + +class TabStopEscapingWhenSelectedSingleCharTS_ECR(_VimTest): + snippets = ('test', 'snip ${1:i}') + keys = 'test' + EX + ESC + '0ihi' + wanted = 'hisnip i' + + +class TabStopEscapingWhenSelectedNoCharTS_ECR(_VimTest): + snippets = ('test', 'snip $1') + keys = 'test' + EX + ESC + '0ihi' + wanted = 'hisnip ' + + +class TabStopWithOneChar_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'nothing ${1:i} hups') + keys = 'hallo' + EX + 'ship' + wanted = 'nothing ship hups' + + +class TabStopTestJumping_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo ${2:End} mitte ${1:Beginning}') + keys = 'hallo' + EX + JF + 'Test' + JF + 'Hi' + wanted = 'hallo Test mitte BeginningHi' + + +class TabStopTestJumping2_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo $2 $1') + keys = 'hallo' + EX + JF + 'Test' + JF + 'Hi' + wanted = 'hallo Test Hi' + + +class TabStopTestJumpingRLExampleWithZeroTab_ExpectCorrectResult(_VimTest): + snippets = ('test', 'each_byte { |${1:byte}| $0 }') + keys = 'test' + EX + JF + 'Blah' + wanted = 'each_byte { |byte| Blah }' + + +class TabStopTestJumpingDontJumpToEndIfThereIsTabZero_ExpectCorrectResult( + _VimTest): + snippets = ('hallo', 'hallo $0 $1') + keys = 'hallo' + EX + 'Test' + JF + 'Hi' + JF + JF + 'du' + wanted = 'hallo Hi' + 2 * JF + 'du Test' + + +class TabStopTestBackwardJumping_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo ${2:End} mitte${1:Beginning}') + keys = 'hallo' + EX + 'Somelengthy Text' + JF + 'Hi' + JB + \ + 'Lets replace it again' + JF + 'Blah' + JF + JB * 2 + JF + wanted = 'hallo Blah mitteLets replace it again' + JB * 2 + JF + + +class TabStopTestBackwardJumping2_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo $2 $1') + keys = 'hallo' + EX + 'Somelengthy Text' + JF + 'Hi' + JB + \ + 'Lets replace it again' + JF + 'Blah' + JF + JB * 2 + JF + wanted = 'hallo Blah Lets replace it again' + JB * 2 + JF + + +class TabStopTestMultilineExpand_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'hallo $0\nnice $1 work\n$3 $2\nSeem to work') + keys = 'test hallo World' + ESC + '02f i' + EX + 'world' + JF + 'try' + \ + JF + 'test' + JF + 'one more' + JF + wanted = 'test hallo one more' + JF + '\nnice world work\n' \ + 'test try\nSeem to work World' + + +class TabStop_TSInDefaultTextRLExample_OverwriteNone_ECR(_VimTest): + snippets = ('test', """\n $0\n""") + keys = 'test' + EX + wanted = """
\n \n
""" + + +class TabStop_TSInDefaultTextRLExample_OverwriteFirst_NoJumpBack(_VimTest): + snippets = ('test', """\n $0\n""") + keys = 'test' + EX + ' blah' + JF + 'Hallo' + wanted = """
\n Hallo\n
""" + + +class TabStop_TSInDefaultTextRLExample_DeleteFirst(_VimTest): + snippets = ('test', """\n $0\n""") + keys = 'test' + EX + BS + JF + 'Hallo' + wanted = """
\n Hallo\n
""" + + +class TabStop_TSInDefaultTextRLExample_OverwriteFirstJumpBack(_VimTest): + snippets = ('test', """\n $3 $0\n""") + keys = 'test' + EX + 'Hi' + JF + 'Hallo' + JB + 'SomethingElse' + JF + \ + 'Nupl' + JF + 'Nox' + wanted = """\n Nupl Nox\n""" + + +class TabStop_TSInDefaultTextRLExample_OverwriteSecond(_VimTest): + snippets = ('test', """\n $0\n""") + keys = 'test' + EX + JF + 'no' + JF + 'End' + wanted = """
\n End\n
""" + + +class TabStop_TSInDefaultTextRLExample_OverwriteSecondTabBack(_VimTest): + snippets = ('test', """\n $3 $0\n""") + keys = 'test' + EX + JF + 'no' + JF + 'End' + JB + 'yes' + JF + 'Begin' \ + + JF + 'Hi' + wanted = """
\n Begin Hi\n
""" + + +class TabStop_TSInDefaultTextRLExample_OverwriteSecondTabBackTwice(_VimTest): + snippets = ('test', """\n $3 $0\n""") + keys = 'test' + EX + JF + 'no' + JF + 'End' + JB + 'yes' + JB + \ + ' allaway' + JF + 'Third' + JF + 'Last' + wanted = """
\n Third Last\n
""" + + +class TabStop_TSInDefaultText_ZeroLengthNested_OverwriteSecond(_VimTest): + snippets = ('test', """h${1:a$2b}l""") + keys = 'test' + EX + JF + 'ups' + JF + 'End' + wanted = """haupsblEnd""" + + +class TabStop_TSInDefaultText_ZeroLengthZerothTabstop(_VimTest): + snippets = ('test', """Test: ${1:snippet start\nNested tabstop: $0\nsnippet end}\nTrailing text""") + keys = 'test' + EX + JF + 'hello' + wanted = "Test: snippet start\nNested tabstop: hello\nsnippet end\nTrailing text" + +class TabStop_TSInDefaultText_ZeroLengthZerothTabstop_Override(_VimTest): + snippets = ('test', """Test: ${1:snippet start\nNested tabstop: $0\nsnippet end}\nTrailing text""") + keys = 'test' + EX + 'blub' + JF + 'hello' + wanted = "Test: blub\nTrailing texthello" + +class TabStop_TSInDefaultText_ZeroLengthNested_OverwriteFirst(_VimTest): + snippets = ('test', """h${1:a$2b}l""") + keys = 'test' + EX + 'ups' + JF + 'End' + wanted = """hupslEnd""" + + +class TabStop_TSInDefaultText_ZeroLengthNested_OverwriteSecondJumpBackOverwrite( + _VimTest): + snippets = ('test', """h${1:a$2b}l""") + keys = 'test' + EX + JF + 'longertext' + JB + 'overwrite' + JF + 'End' + wanted = """hoverwritelEnd""" + + +class TabStop_TSInDefaultText_ZeroLengthNested_OverwriteSecondJumpBackAndForward0( + _VimTest): + snippets = ('test', """h${1:a$2b}l""") + keys = 'test' + EX + JF + 'longertext' + JB + JF + 'overwrite' + JF + 'End' + wanted = """haoverwriteblEnd""" + + +class TabStop_TSInDefaultText_ZeroLengthNested_OverwriteSecondJumpBackAndForward1( + _VimTest): + snippets = ('test', """h${1:a$2b}l""") + keys = 'test' + EX + JF + 'longertext' + JB + JF + JF + 'End' + wanted = """halongertextblEnd""" + + +class TabStop_TSInDefaultNested_OverwriteOneJumpBackToOther(_VimTest): + snippets = ('test', 'hi ${1:this ${2:second ${3:third}}} $4') + keys = 'test' + EX + JF + 'Hallo' + JF + 'Ende' + wanted = 'hi this Hallo Ende' + + +class TabStop_TSInDefaultNested_OverwriteOneJumpToThird(_VimTest): + snippets = ('test', 'hi ${1:this ${2:second ${3:third}}} $4') + keys = 'test' + EX + JF + JF + 'Hallo' + JF + 'Ende' + wanted = 'hi this second Hallo Ende' + + +class TabStop_TSInDefaultNested_OverwriteOneJumpAround(_VimTest): + snippets = ('test', 'hi ${1:this ${2:second ${3:third}}} $4') + keys = 'test' + EX + JF + JF + 'Hallo' + JB + JB + 'Blah' + JF + 'Ende' + wanted = 'hi Blah Ende' + + +class TabStop_TSInDefault_MirrorsOutside_DoNothing(_VimTest): + snippets = ('test', 'hi ${1:this ${2:second}} $2') + keys = 'test' + EX + wanted = 'hi this second second' + + +class TabStop_TSInDefault_MirrorsOutside_OverwriteSecond(_VimTest): + snippets = ('test', 'hi ${1:this ${2:second}} $2') + keys = 'test' + EX + JF + 'Hallo' + wanted = 'hi this Hallo Hallo' + + +class TabStop_TSInDefault_MirrorsOutside_Overwrite0(_VimTest): + snippets = ('test', 'hi ${1:this ${2:second}} $2') + keys = 'test' + EX + 'Hallo' + wanted = 'hi Hallo ' + + +class TabStop_TSInDefault_MirrorsOutside_Overwrite1(_VimTest): + snippets = ('test', "$1: ${1:'${2:second}'} $2") + keys = 'test' + EX + 'Hallo' + wanted = 'Hallo: Hallo ' + + +class TabStop_TSInDefault_MirrorsOutside_OverwriteSecond1(_VimTest): + snippets = ('test', "$1: ${1:'${2:second}'} $2") + keys = 'test' + EX + JF + 'Hallo' + wanted = "'Hallo': 'Hallo' Hallo" + + +class TabStop_TSInDefault_MirrorsOutside_OverwriteFirstSwitchNumbers(_VimTest): + snippets = ('test', "$2: ${2:'${1:second}'} $1") + keys = 'test' + EX + 'Hallo' + wanted = "'Hallo': 'Hallo' Hallo" + + +class TabStop_TSInDefault_MirrorsOutside_OverwriteFirst_RLExample(_VimTest): + snippets = ( + 'test', + """`!p snip.rv = t[1].split('/')[-1].lower().strip("'")` = require(${1:'${2:sys}'})""") + keys = 'test' + EX + 'WORLD' + JF + 'End' + wanted = 'world = require(WORLD)End' + + +class TabStop_TSInDefault_MirrorsOutside_OverwriteSecond_RLExample(_VimTest): + snippets = ( + 'test', + """`!p snip.rv = t[1].split('/')[-1].lower().strip("'")` = require(${1:'${2:sys}'})""") + keys = 'test' + EX + JF + 'WORLD' + JF + 'End' + wanted = "world = require('WORLD')End" + + +class TabStop_Multiline_Leave(_VimTest): + snippets = ('test', 'hi ${1:first line\nsecond line} world') + keys = 'test' + EX + wanted = 'hi first line\nsecond line world' + + +class TabStop_Multiline_Overwrite(_VimTest): + snippets = ('test', 'hi ${1:first line\nsecond line} world') + keys = 'test' + EX + 'Nothing' + wanted = 'hi Nothing world' + + +class TabStop_Multiline_MirrorInFront_Leave(_VimTest): + snippets = ('test', 'hi $1 ${1:first line\nsecond line} world') + keys = 'test' + EX + wanted = 'hi first line\nsecond line first line\nsecond line world' + + +class TabStop_Multiline_MirrorInFront_Overwrite(_VimTest): + snippets = ('test', 'hi $1 ${1:first line\nsecond line} world') + keys = 'test' + EX + 'Nothing' + wanted = 'hi Nothing Nothing world' + + +class TabStop_Multiline_DelFirstOverwriteSecond_Overwrite(_VimTest): + snippets = ('test', 'hi $1 $2 ${1:first line\nsecond line} ${2:Hi} world') + keys = 'test' + EX + BS + JF + 'Nothing' + wanted = 'hi Nothing Nothing world' + + +class TabStopNavigatingInInsertModeSimple_ExpectCorrectResult(_VimTest): + snippets = ('hallo', 'Hallo ${1:WELT} ups') + keys = 'hallo' + EX + 'haselnut' + 2 * ARR_L + 'hips' + JF + 'end' + wanted = 'Hallo haselnhipsut upsend' + + +class TabStop_CROnlyOnSelectedNear(_VimTest): + snippets = ('test', 't$1t${2: }t{\n\t$0\n}') + keys = 'test' + EX + JF + '\n' + JF + 't' + wanted = 'tt\nt{\n\tt\n}' + + +class TabStop_AdjacentTabStopAddText_ExpectCorrectResult(_VimTest): + snippets = ('test', '[ $1$2 ] $1') + keys = 'test' + EX + 'Hello' + JF + 'World' + JF + wanted = '[ HelloWorld ] Hello' + + +class TabStop_KeepCorrectJumpListOnOverwriteOfPartOfSnippet(_VimTest): + files = { 'us/all.snippets': r""" + snippet i + ia$1: $2 + endsnippet + + snippet ia + ia($1, $2) + endsnippet"""} + keys = 'i' + EX + EX + '1' + JF + '2' + JF + ' after' + JF + '3' + wanted = 'ia(1, 2) after: 3' + + +class TabStop_KeepCorrectJumpListOnOverwriteOfPartOfSnippetRE(_VimTest): + files = { 'us/all.snippets': r""" + snippet i + ia$1: $2 + endsnippet + + snippet "^ia" "regexp" r + ia($1, $2) + endsnippet"""} + keys = 'i' + EX + EX + '1' + JF + '2' + JF + ' after' + JF + '3' + wanted = 'ia(1, 2) after: 3' diff --git a/test/test_Transformation.py b/test/test_Transformation.py new file mode 100644 index 0000000..9f7bfac --- /dev/null +++ b/test/test_Transformation.py @@ -0,0 +1,248 @@ +# encoding: utf-8 +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * +from test.util import no_unidecode_available + +# Transformations {{{# + + +class Transformation_SimpleCase_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/foo/batzl/}') + keys = 'test' + EX + 'hallo foo boy' + wanted = 'hallo foo boy hallo batzl boy' + + +class Transformation_SimpleCaseNoTransform_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/foo/batzl/}') + keys = 'test' + EX + 'hallo' + wanted = 'hallo hallo' + + +class Transformation_SimpleCaseTransformInFront_ExpectCorrectResult(_VimTest): + snippets = ('test', '${1/foo/batzl/} $1') + keys = 'test' + EX + 'hallo foo' + wanted = 'hallo batzl hallo foo' + + +class Transformation_SimpleCaseTransformInFrontDefVal_ECR(_VimTest): + snippets = ('test', '${1/foo/batzl/} ${1:replace me}') + keys = 'test' + EX + 'hallo foo' + wanted = 'hallo batzl hallo foo' + + +class Transformation_MultipleTransformations_ECR(_VimTest): + snippets = ('test', '${1:Some Text}${1/.+/\\U$0\E/}\n${1/.+/\L$0\E/}') + keys = 'test' + EX + 'SomE tExt ' + wanted = 'SomE tExt SOME TEXT \nsome text ' + + +class Transformation_TabIsAtEndAndDeleted_ECR(_VimTest): + snippets = ('test', '${1/.+/is something/}${1:some}') + keys = 'hallo test' + EX + 'some\b\b\b\b\b' + wanted = 'hallo ' + + +class Transformation_TabIsAtEndAndDeleted1_ECR(_VimTest): + snippets = ('test', '${1/.+/is something/}${1:some}') + keys = 'hallo test' + EX + 'some\b\b\b\bmore' + wanted = 'hallo is somethingmore' + + +class Transformation_TabIsAtEndNoTextLeave_ECR(_VimTest): + snippets = ('test', '${1/.+/is something/}${1}') + keys = 'hallo test' + EX + wanted = 'hallo ' + + +class Transformation_TabIsAtEndNoTextType_ECR(_VimTest): + snippets = ('test', '${1/.+/is something/}${1}') + keys = 'hallo test' + EX + 'b' + wanted = 'hallo is somethingb' + + +class Transformation_InsideTabLeaveAtDefault_ECR(_VimTest): + snippets = ('test', r"$1 ${2:${1/.+/(?0:defined $0)/}}") + keys = 'test' + EX + 'sometext' + JF + wanted = 'sometext defined sometext' + + +class Transformation_InsideTabOvertype_ECR(_VimTest): + snippets = ('test', r"$1 ${2:${1/.+/(?0:defined $0)/}}") + keys = 'test' + EX + 'sometext' + JF + 'overwrite' + wanted = 'sometext overwrite' + + +class Transformation_Backreference_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/([ab])oo/$1ull/}') + keys = 'test' + EX + 'foo boo aoo' + wanted = 'foo boo aoo foo bull aoo' + + +class Transformation_BackreferenceTwice_ExpectCorrectResult(_VimTest): + snippets = ('test', r"$1 ${1/(dead) (par[^ ]*)/this $2 is a bit $1/}") + keys = 'test' + EX + 'dead parrot' + wanted = 'dead parrot this parrot is a bit dead' + + +class Transformation_CleverTransformUpercaseChar_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/(.)/\\u$1/}') + keys = 'test' + EX + 'hallo' + wanted = 'hallo Hallo' + + +class Transformation_CleverTransformLowercaseChar_ExpectCorrectResult( + _VimTest): + snippets = ('test', '$1 ${1/(.*)/\l$1/}') + keys = 'test' + EX + 'Hallo' + wanted = 'Hallo hallo' + + +class Transformation_CleverTransformLongUpper_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/(.*)/\\U$1\E/}') + keys = 'test' + EX + 'hallo' + wanted = 'hallo HALLO' + + +class Transformation_CleverTransformLongLower_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/(.*)/\L$1\E/}') + keys = 'test' + EX + 'HALLO' + wanted = 'HALLO hallo' + + +class Transformation_SimpleCaseAsciiResult(_VimTest): + skip_if = lambda self: no_unidecode_available() + snippets = ('ascii', '$1 ${1/(.*)/$1/a}') + keys = 'ascii' + EX + 'éèàçôïÉÈÀÇÔÏ€' + wanted = 'éèàçôïÉÈÀÇÔÏ€ eeacoiEEACOIEU' + + +class Transformation_LowerCaseAsciiResult(_VimTest): + skip_if = lambda self: no_unidecode_available() + snippets = ('ascii', '$1 ${1/(.*)/\L$1\E/a}') + keys = 'ascii' + EX + 'éèàçôïÉÈÀÇÔÏ€' + wanted = 'éèàçôïÉÈÀÇÔÏ€ eeacoieeacoieu' + + +class Transformation_ConditionalInsertionSimple_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/(^a).*/(?0:began with an a)/}') + keys = 'test' + EX + 'a some more text' + wanted = 'a some more text began with an a' + + +class Transformation_CIBothDefinedNegative_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/(?:(^a)|(^b)).*/(?1:yes:no)/}') + keys = 'test' + EX + 'b some' + wanted = 'b some no' + + +class Transformation_CIBothDefinedPositive_ExpectCorrectResult(_VimTest): + snippets = ('test', '$1 ${1/(?:(^a)|(^b)).*/(?1:yes:no)/}') + keys = 'test' + EX + 'a some' + wanted = 'a some yes' + + +class Transformation_ConditionalInsertRWEllipsis_ECR(_VimTest): + snippets = ('test', r"$1 ${1/(\w+(?:\W+\w+){,7})\W*(.+)?/$1(?2:...)/}") + keys = 'test' + EX + 'a b c d e f ghhh h oha' + wanted = 'a b c d e f ghhh h oha a b c d e f ghhh h...' + + +class Transformation_ConditionalInConditional_ECR(_VimTest): + snippets = ('test', r"$1 ${1/^.*?(-)?(>)?$/(?2::(?1:>:.))/}") + keys = 'test' + EX + 'hallo' + ESC + '$a\n' + \ + 'test' + EX + 'hallo-' + ESC + '$a\n' + \ + 'test' + EX + 'hallo->' + wanted = 'hallo .\nhallo- >\nhallo-> ' + + +class Transformation_CINewlines_ECR(_VimTest): + snippets = ('test', r"$1 ${1/, */\n/}") + keys = 'test' + EX + 'test, hallo' + wanted = 'test, hallo test\nhallo' + + +class Transformation_CITabstop_ECR(_VimTest): + snippets = ('test', r"$1 ${1/, */\t/}") + keys = 'test' + EX + 'test, hallo' + wanted = 'test, hallo test\thallo' + + +class Transformation_CIEscapedParensinReplace_ECR(_VimTest): + snippets = ('test', r"$1 ${1/hal((?:lo)|(?:ul))/(?1:ha\($1\))/}") + keys = 'test' + EX + 'test, halul' + wanted = 'test, halul test, ha(ul)' + + +class Transformation_OptionIgnoreCase_ECR(_VimTest): + snippets = ('test', r"$1 ${1/test/blah/i}") + keys = 'test' + EX + 'TEST' + wanted = 'TEST blah' + + +class Transformation_OptionReplaceGlobal_ECR(_VimTest): + snippets = ('test', r"$1 ${1/, */-/g}") + keys = 'test' + EX + 'a, nice, building' + wanted = 'a, nice, building a-nice-building' + + +class Transformation_OptionReplaceGlobalMatchInReplace_ECR(_VimTest): + snippets = ('test', r"$1 ${1/, */, /g}") + keys = 'test' + EX + 'a, nice, building' + wanted = 'a, nice, building a, nice, building' + + +class TransformationUsingBackspaceToDeleteDefaultValueInFirstTab_ECR(_VimTest): + snippets = ('test', 'snip ${1/.+/(?0:m1)/} ${2/.+/(?0:m2)/} ' + '${1:default} ${2:def}') + keys = 'test' + EX + BS + JF + 'hi' + wanted = 'snip m2 hi' + + +class TransformationUsingBackspaceToDeleteDefaultValueInSecondTab_ECR( + _VimTest): + snippets = ('test', 'snip ${1/.+/(?0:m1)/} ${2/.+/(?0:m2)/} ' + '${1:default} ${2:def}') + keys = 'test' + EX + 'hi' + JF + BS + wanted = 'snip m1 hi ' + + +class TransformationUsingBackspaceToDeleteDefaultValueTypeSomethingThen_ECR( + _VimTest): + snippets = ('test', 'snip ${1/.+/(?0:matched)/} ${1:default}') + keys = 'test' + EX + BS + 'hallo' + wanted = 'snip matched hallo' + + +class TransformationUsingBackspaceToDeleteDefaultValue_ECR(_VimTest): + snippets = ('test', 'snip ${1/.+/(?0:matched)/} ${1:default}') + keys = 'test' + EX + BS + wanted = 'snip ' + + +class Transformation_TestKill_InsertBefore_NoKill(_VimTest): + snippets = 'test', r"$1 ${1/.*/\L$0$0\E/}_" + keys = 'hallo test' + EX + 'AUCH' + ESC + \ + 'wihi' + ESC + 'bb' + 'ino' + JF + 'end' + wanted = 'hallo noAUCH hinoauchnoauch_end' + + +class Transformation_TestKill_InsertAfter_NoKill(_VimTest): + snippets = 'test', r"$1 ${1/.*/\L$0$0\E/}_" + keys = 'hallo test' + EX + 'AUCH' + ESC + \ + 'eiab' + ESC + 'bb' + 'ino' + JF + 'end' + wanted = 'hallo noAUCH noauchnoauchab_end' + + +class Transformation_TestKill_InsertBeginning_Kill(_VimTest): + snippets = 'test', r"$1 ${1/.*/\L$0$0\E/}_" + keys = 'hallo test' + EX + 'AUCH' + ESC + \ + 'wahi' + ESC + 'bb' + 'ino' + JF + 'end' + wanted = 'hallo noAUCH ahiuchauch_end' + + +class Transformation_TestKill_InsertEnd_Kill(_VimTest): + snippets = 'test', r"$1 ${1/.*/\L$0$0\E/}_" + keys = 'hallo test' + EX + 'AUCH' + ESC + \ + 'ehihi' + ESC + 'bb' + 'ino' + JF + 'end' + wanted = 'hallo noAUCH auchauchih_end' +# End: Transformations #}}} diff --git a/test/test_UltiSnipFunc.py b/test/test_UltiSnipFunc.py new file mode 100644 index 0000000..a0dde3d --- /dev/null +++ b/test/test_UltiSnipFunc.py @@ -0,0 +1,170 @@ +# encoding: utf-8 +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * +from test.util import running_on_windows + +# AddSnippet Function {{{# + + +class _AddFuncBase(_VimTest): + args = '' + + def _before_test(self): + self.vim.send_to_vim(':call UltiSnips#AddSnippetWithPriority(%s)\n' % self.args) + + +class AddFunc_Simple(_AddFuncBase): + args = '"test", "simple expand", "desc", "", "all", 0' + keys = 'abc test' + EX + wanted = 'abc simple expand' + + +class AddFunc_Opt(_AddFuncBase): + args = '".*test", "simple expand", "desc", "r", "all", 0' + keys = 'abc test' + EX + wanted = 'simple expand' +# End: AddSnippet Function #}}} + +# Langmap Handling {{{# +# Test for bug 501727 # + + +class TestNonEmptyLangmap_ExpectCorrectResult(_VimTest): + snippets = ('testme', + """my snipped ${1:some_default} +and a mirror: $1 +$2...$3 +$0""") + keys = 'testme' + EX + 'hi1' + JF + 'hi2' + JF + 'hi3' + JF + 'hi4' + wanted = """my snipped hi1 +and a mirror: hi1 +hi2...hi3 +hi4""" + + def _extra_vim_config(self, vim_config): + vim_config.append('set langmap=dj,rk,nl,ln,jd,kr,DJ,RK,NL,LN,JD,KR') + +# Test for https://bugs.launchpad.net/bugs/501727 # + + +class TestNonEmptyLangmapWithSemi_ExpectCorrectResult(_VimTest): + snippets = ('testme', + """my snipped ${1:some_default} +and a mirror: $1 +$2...$3 +$0""") + keys = 'testme' + EX + 'hi;' + JF + 'hi2' + \ + JF + 'hi3' + JF + 'hi4' + ESC + ';Hello' + wanted = """my snipped hi; +and a mirror: hi; +hi2...hi3 +hi4Hello""" + + def _before_test(self): + self.vim.send_to_vim(':set langmap=\\\\;;A\n') + +# Test for bug 871357 # + + +class TestLangmapWithUtf8_ExpectCorrectResult(_VimTest): + # SendKeys can't send UTF characters + skip_if = lambda self: running_on_windows() + snippets = ('testme', + """my snipped ${1:some_default} +and a mirror: $1 +$2...$3 +$0""") + keys = 'testme' + EX + 'hi1' + JF + 'hi2' + JF + 'hi3' + JF + 'hi4' + wanted = """my snipped hi1 +and a mirror: hi1 +hi2...hi3 +hi4""" + + def _before_test(self): + self.vim.send_to_vim( + ":set langmap=йq,цw,уe,кr,еt,нy,гu,шi,щo,зp,х[,ъ],фa,ыs,вd,аf,пg,рh,оj,лk,дl,ж\\;,э',яz,чx,сc,мv,иb,тn,ьm,ю.,ё',ЙQ,ЦW,УE,КR,ЕT,НY,ГU,ШI,ЩO,ЗP,Х\{,Ъ\},ФA,ЫS,ВD,АF,ПG,РH,ОJ,ЛK,ДL,Ж\:,Э\",ЯZ,ЧX,СC,МV,ИB,ТN,ЬM,Б\<,Ю\>\n") + +# End: Langmap Handling #}}} + +# SnippetsInCurrentScope {{{# + + +class VerifyVimDict1(_VimTest): + + """check: + correct type (4 means vim dictionary) + correct length of dictionary (in this case we have on element if the use same prefix, dictionary should have 1 element) + correct description (including the apostrophe) + if the prefix is mismatched no resulting dict should have 0 elements + """ + + snippets = ('testâ', 'abc123ά', '123\'êabc') + keys = ('test=(type(UltiSnips#SnippetsInCurrentScope()) . len(UltiSnips#SnippetsInCurrentScope()) . ' + + 'UltiSnips#SnippetsInCurrentScope()["testâ"]' + ')\n' + + '=len(UltiSnips#SnippetsInCurrentScope())\n') + + wanted = 'test41123\'êabc0' + + +class VerifyVimDict2(_VimTest): + + """check: + can use " in trigger + """ + + snippets = ('te"stâ', 'abc123ά', '123êabc') + akey = "'te{}stâ'".format('"') + keys = ( + 'te"=(UltiSnips#SnippetsInCurrentScope()[{}]'.format(akey) + ')\n') + wanted = 'te"123êabc' + + +class VerifyVimDict3(_VimTest): + + """check: + can use ' in trigger + """ + + snippets = ("te'stâ", 'abc123ά', '123êabc') + akey = '"te{}stâ"'.format("'") + keys = ( + "te'=(UltiSnips#SnippetsInCurrentScope()[{}]".format(akey) + ')\n') + wanted = "te'123êabc" +# End: SnippetsInCurrentScope #}}} + +# Snippet Source {{{# + + +class AddNewSnippetSource(_VimTest): + keys = ('blumba' + EX + ESC + + ':%(python)s UltiSnips_Manager.register_snippet_source(' + + "'temp', MySnippetSource())\n" + + 'oblumba' + EX + ESC + + ":%(python)s UltiSnips_Manager.unregister_snippet_source('temp')\n" + + 'oblumba' + EX) % {'python': 'py3' if PYTHON3 else 'py'} + wanted = ( + 'blumba' + EX + '\n' + + 'this is a dynamic snippet' + '\n' + + 'blumba' + EX + ) + + def _extra_vim_config(self, vim_config): + self._create_file('snippet_source.py', """ +from UltiSnips.snippet.source import SnippetSource +from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition + +class MySnippetSource(SnippetSource): + def get_snippets(self, filetypes, before, possible, autotrigger_only): + if before.endswith('blumba') and autotrigger_only == False: + return [ + UltiSnipsSnippetDefinition( + -100, "blumba", "this is a dynamic snippet", "", "", {}, "blub", + None, {}) + ] + return [] +""") + pyfile = 'py3file' if PYTHON3 else 'pyfile' + vim_config.append( + '%s %s' % + (pyfile, self.name_temp('snippet_source.py'))) +# End: Snippet Source #}}} diff --git a/test/test_Visual.py b/test/test_Visual.py new file mode 100644 index 0000000..d6d9d8a --- /dev/null +++ b/test/test_Visual.py @@ -0,0 +1,204 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + +# ${VISUAL} {{{# + + +class Visual_NoVisualSelection_Ignore(_VimTest): + snippets = ('test', 'h${VISUAL}b') + keys = 'test' + EX + 'abc' + wanted = 'hbabc' + + +class Visual_SelectOneWord(_VimTest): + snippets = ('test', 'h${VISUAL}b') + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + wanted = 'hblablubb' + + +class Visual_SelectOneWord_ProblemAfterTab(_VimTest): + snippets = ('test', 'h${VISUAL}b', '', 'i') + keys = '\tblablub' + ESC + '5hv3l' + EX + 'test' + EX + wanted = '\tbhlablbub' + + +class VisualWithDefault_ExpandWithoutVisual(_VimTest): + snippets = ('test', 'h${VISUAL:world}b') + keys = 'test' + EX + 'hi' + wanted = 'hworldbhi' + + +class VisualWithDefaultWithSlashes_ExpandWithoutVisual(_VimTest): + snippets = ('test', r"h${VISUAL:\/\/ body}b") + keys = 'test' + EX + 'hi' + wanted = 'h// bodybhi' + + +class VisualWithDefault_ExpandWithVisual(_VimTest): + snippets = ('test', 'h${VISUAL:world}b') + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + wanted = 'hblablubb' + + +class Visual_ExpandTwice(_VimTest): + snippets = ('test', 'h${VISUAL}b') + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + '\ntest' + EX + wanted = 'hblablubb\nhb' + + +class Visual_SelectOneWord_TwiceVisual(_VimTest): + snippets = ('test', 'h${VISUAL}b${VISUAL}a') + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + wanted = 'hblablubbblabluba' + + +class Visual_SelectOneWord_Inword(_VimTest): + snippets = ('test', 'h${VISUAL}b', 'Description', 'i') + keys = 'blablub' + ESC + '0lv4l' + EX + 'test' + EX + wanted = 'bhlablubb' + + +class Visual_SelectOneWord_TillEndOfLine(_VimTest): + snippets = ('test', 'h${VISUAL}b', 'Description', 'i') + keys = 'blablub' + ESC + '0v$' + EX + 'test' + EX + ESC + 'o' + wanted = 'hblablub\nb' + + +class Visual_SelectOneWordWithTabstop_TillEndOfLine(_VimTest): + snippets = ('test', 'h${2:ahh}${VISUAL}${1:ups}b', 'Description', 'i') + keys = 'blablub' + ESC + '0v$' + EX + 'test' + \ + EX + 'mmm' + JF + 'n' + JF + 'done' + ESC + 'o' + wanted = 'hnblablub\nmmmbdone' + + +class Visual_InDefaultText_SelectOneWord_NoOverwrite(_VimTest): + snippets = ('test', 'h${1:${VISUAL}}b') + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + JF + 'hello' + wanted = 'hblablubbhello' + + +class Visual_InDefaultText_SelectOneWord(_VimTest): + snippets = ('test', 'h${1:${VISUAL}}b') + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + 'hello' + wanted = 'hhellob' + + +class Visual_CrossOneLine(_VimTest): + snippets = ('test', 'h${VISUAL}b') + keys = 'bla blub\n helloi' + ESC + '0k4lvjll' + EX + 'test' + EX + wanted = 'bla hblub\n hellobi' + + +class Visual_LineSelect_Simple(_VimTest): + snippets = ('test', 'h${VISUAL}b') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + wanted = 'hhello\n nice\n worldb' + + +class Visual_InDefaultText_LineSelect_NoOverwrite(_VimTest): + snippets = ('test', 'h${1:bef${VISUAL}aft}b') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + JF + 'hi' + wanted = 'hbefhello\n nice\n worldaftbhi' + + +class Visual_InDefaultText_LineSelect_Overwrite(_VimTest): + snippets = ('test', 'h${1:bef${VISUAL}aft}b') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + \ + EX + 'test' + EX + 'jup' + JF + 'hi' + wanted = 'hjupbhi' + + +class Visual_LineSelect_CheckIndentSimple(_VimTest): + snippets = ('test', 'beg\n\t${VISUAL}\nend') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + wanted = 'beg\n\thello\n\tnice\n\tworld\nend' + + +class Visual_LineSelect_CheckIndentTwice(_VimTest): + snippets = ('test', 'beg\n\t${VISUAL}\nend') + keys = ' hello\n nice\n\tworld' + ESC + 'Vkk' + EX + 'test' + EX + wanted = 'beg\n\t hello\n\t nice\n\t\tworld\nend' + + +class Visual_InDefaultText_IndentSpacesToTabstop_NoOverwrite(_VimTest): + snippets = ('test', 'h${1:beforea${VISUAL}aft}b') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + JF + 'hi' + wanted = 'hbeforeahello\n\tnice\n\tworldaftbhi' + + +class Visual_InDefaultText_IndentSpacesToTabstop_Overwrite(_VimTest): + snippets = ('test', 'h${1:beforea${VISUAL}aft}b') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + \ + EX + 'test' + EX + 'ups' + JF + 'hi' + wanted = 'hupsbhi' + + +class Visual_InDefaultText_IndentSpacesToTabstop_NoOverwrite1(_VimTest): + snippets = ('test', 'h${1:beforeaaa${VISUAL}aft}b') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + JF + 'hi' + wanted = 'hbeforeaaahello\n\t nice\n\t worldaftbhi' + + +class Visual_InDefaultText_IndentBeforeTabstop_NoOverwrite(_VimTest): + snippets = ('test', 'hello\n\t ${1:${VISUAL}}\nend') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + JF + 'hi' + wanted = 'hello\n\t hello\n\t nice\n\t world\nendhi' + + +class Visual_LineSelect_WithTabStop(_VimTest): + snippets = ('test', 'beg\n\t${VISUAL}\n\t${1:here_we_go}\nend') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + \ + EX + 'test' + EX + 'super' + JF + 'done' + wanted = 'beg\n\thello\n\tnice\n\tworld\n\tsuper\nenddone' + + +class Visual_LineSelect_CheckIndentWithTS_NoOverwrite(_VimTest): + snippets = ('test', 'beg\n\t${0:${VISUAL}}\nend') + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + wanted = 'beg\n\thello\n\tnice\n\tworld\nend' + + +class Visual_LineSelect_DedentLine(_VimTest): + snippets = ('if', 'if {\n\t${VISUAL}$0\n}') + keys = 'if' + EX + 'one\n\ttwo\n\tthree' + ESC + \ + ARR_U * 2 + 'V' + ARR_D + EX + '\tif' + EX + wanted = 'if {\n\tif {\n\t\tone\n\t\ttwo\n\t}\n\tthree\n}' + + +class VisualTransformation_SelectOneWord(_VimTest): + snippets = ('test', r"h${VISUAL/./\U$0\E/g}b") + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + wanted = 'hBLABLUBb' + + +class VisualTransformationWithDefault_ExpandWithoutVisual(_VimTest): + snippets = ('test', r"h${VISUAL:world/./\U$0\E/g}b") + keys = 'test' + EX + 'hi' + wanted = 'hWORLDbhi' + + +class VisualTransformationWithDefault_ExpandWithVisual(_VimTest): + snippets = ('test', r"h${VISUAL:world/./\U$0\E/g}b") + keys = 'blablub' + ESC + '0v6l' + EX + 'test' + EX + wanted = 'hBLABLUBb' + + +class VisualTransformation_LineSelect_Simple(_VimTest): + snippets = ('test', r"h${VISUAL/./\U$0\E/g}b") + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + wanted = 'hHELLO\n NICE\n WORLDb' + + +class VisualTransformation_InDefaultText_LineSelect_NoOverwrite(_VimTest): + snippets = ('test', r"h${1:bef${VISUAL/./\U$0\E/g}aft}b") + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX + JF + 'hi' + wanted = 'hbefHELLO\n NICE\n WORLDaftbhi' + + +class VisualTransformation_InDefaultText_LineSelect_Overwrite(_VimTest): + snippets = ('test', r"h${1:bef${VISUAL/./\U$0\E/g}aft}b") + keys = 'hello\nnice\nworld' + ESC + 'Vkk' + \ + EX + 'test' + EX + 'jup' + JF + 'hi' + wanted = 'hjupbhi' + +# End: ${VISUAL} #}}} diff --git a/test/util.py b/test/util.py new file mode 100644 index 0000000..74d8d5b --- /dev/null +++ b/test/util.py @@ -0,0 +1,17 @@ +import platform + +try: + import unidecode + UNIDECODE_IMPORTED = True +except ImportError: + UNIDECODE_IMPORTED = False + + +def running_on_windows(): + if platform.system() == 'Windows': + return 'Does not work on Windows.' + + +def no_unidecode_available(): + if not UNIDECODE_IMPORTED: + return 'unidecode is not available.' diff --git a/test/vim_interface.py b/test/vim_interface.py new file mode 100644 index 0000000..cff69ff --- /dev/null +++ b/test/vim_interface.py @@ -0,0 +1,264 @@ +# encoding: utf-8 + +import os +import re +import shutil +import subprocess +import tempfile +import textwrap +import time + +from test.constant import (ARR_D, ARR_L, ARR_R, ARR_U, BS, ESC, PYTHON3, + SEQUENCES) + + +def wait_until_file_exists(file_path, times=None, interval=0.01): + while times is None or times: + if os.path.exists(file_path): + return True + time.sleep(interval) + if times is not None: + times -= 1 + return False + + +def read_text_file(filename): + """Reads the contens of a text file.""" + if PYTHON3: + return open(filename, 'r', encoding='utf-8').read() + else: + return open(filename, 'r').read() + + +def is_process_running(pid): + """Returns true if a process with pid is running, false otherwise.""" + # from + # http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid + try: + os.kill(pid, 0) + except OSError: + return False + else: + return True + + +def silent_call(cmd): + """Calls 'cmd' and returns the exit value.""" + return subprocess.call(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + +def create_directory(dirname): + """Creates 'dirname' and its parents if it does not exist.""" + try: + os.makedirs(dirname) + except OSError: + pass + + +class TempFileManager(object): + + def __init__(self, name=''): + self._temp_dir = tempfile.mkdtemp(prefix='UltiSnipsTest_' + name) + + def name_temp(self, file_path): + return os.path.join(self._temp_dir, file_path) + + def write_temp(self, file_path, content): + abs_path = self.name_temp(file_path) + create_directory(os.path.dirname(abs_path)) + if PYTHON3: + with open(abs_path, 'w', encoding='utf-8') as f: + f.write(content) + else: + with open(abs_path, 'w') as f: + f.write(content) + return abs_path + + def unique_name_temp(self, suffix='', prefix=''): + file_handler, abspath = tempfile.mkstemp( + suffix, prefix, self._temp_dir) + os.close(file_handler) + os.remove(abspath) + return abspath + + def clear_temp(self): + shutil.rmtree(self._temp_dir) + create_directory(self._temp_dir) + + +class VimInterface(TempFileManager): + + def __init__(self, vim_executable, name): + TempFileManager.__init__(self, name) + self._vim_executable = vim_executable + + def get_buffer_data(self): + buffer_path = self.unique_name_temp(prefix='buffer_') + self.send_to_vim(ESC + ':w! %s\n' % buffer_path) + if wait_until_file_exists(buffer_path, 50): + return read_text_file(buffer_path)[:-1] + + def send_to_terminal(self, s): + """Types 's' into the terminal.""" + raise NotImplementedError() + + def send_to_vim(self, s): + """Types 's' into the vim instance under test.""" + raise NotImplementedError() + + def launch(self, config=[]): + """Returns the python version in Vim as a string, e.g. '2.7'""" + pid_file = self.name_temp('vim.pid') + done_file = self.name_temp('loading_done') + if os.path.exists(done_file): + os.remove(done_file) + + post_config = [] + post_config.append('%s << EOF' % ('py3' if PYTHON3 else 'py')) + post_config.append('import vim, sys') + post_config.append( + "with open('%s', 'w') as pid_file: pid_file.write(vim.eval('getpid()'))" % + pid_file) + post_config.append("with open('%s', 'w') as done_file:" % done_file) + post_config.append(" done_file.write('%i.%i.%i' % sys.version_info[:3])") + post_config.append('EOF') + + config_path = self.write_temp('vim_config.vim', + textwrap.dedent(os.linesep.join(config + post_config) + '\n')) + + # Note the space to exclude it from shell history. Also we always set + # NVIM_LISTEN_ADDRESS, even when running vanilla Vim, because it will + # just not care. + self.send_to_terminal(""" NVIM_LISTEN_ADDRESS=/tmp/nvim %s -u %s\r\n""" % ( + self._vim_executable, config_path)) + wait_until_file_exists(done_file) + self._vim_pid = int(read_text_file(pid_file)) + return read_text_file(done_file).strip() + + def leave_with_wait(self): + self.send_to_vim(3 * ESC + ':qa!\n') + while is_process_running(self._vim_pid): + time.sleep(.2) + + +class VimInterfaceTmux(VimInterface): + + def __init__(self, vim_executable, session): + VimInterface.__init__(self, vim_executable, 'Tmux') + self.session = session + self._check_version() + + def _send(self, s): + # I did not find any documentation on what needs escaping when sending + # to tmux, but it seems like this is all that is needed for now. + s = s.replace(';', r'\;') + + if PYTHON3: + s = s.encode('utf-8') + silent_call(['tmux', 'send-keys', '-t', self.session, '-l', s]) + + def send_to_terminal(self, s): + return self._send(s) + + def send_to_vim(self, s): + return self._send(s) + + def _check_version(self): + stdout, _ = subprocess.Popen(['tmux', '-V'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + if PYTHON3: + stdout = stdout.decode('utf-8') + m = re.match(r"tmux (\d+).(\d+)", stdout) + if not m or not (int(m.group(1)), int(m.group(2))) >= (1, 8): + raise RuntimeError( + 'Need at least tmux 1.8, you have %s.' % + stdout.strip()) + +class VimInterfaceTmuxNeovim(VimInterfaceTmux): + + def __init__(self, vim_executable, session): + VimInterfaceTmux.__init__(self, vim_executable, session) + self._nvim = None + + def send_to_vim(self, s): + if s == ARR_L: + s = "" + elif s == ARR_R: + s = "" + elif s == ARR_U: + s = "" + elif s == ARR_D: + s = "" + elif s == BS: + s = "" + elif s == ESC: + s = "" + elif s == "<": + s = "" + self._nvim.input(s) + + def launch(self, config=[]): + import neovim + rv = VimInterfaceTmux.launch(self, config) + self._nvim = neovim.attach('socket', path='/tmp/nvim') + return rv + +class VimInterfaceWindows(VimInterface): + BRACES = re.compile('([}{])') + WIN_ESCAPES = ['+', '^', '%', '~', '[', ']', '<', '>', '(', ')'] + WIN_REPLACES = [ + (BS, '{BS}'), + (ARR_L, '{LEFT}'), + (ARR_R, '{RIGHT}'), + (ARR_U, '{UP}'), + (ARR_D, '{DOWN}'), + ('\t', '{TAB}'), + ('\n', '~'), + (ESC, '{ESC}'), + + # On my system ` waits for a second keystroke, so `+SPACE = "`". On + # most systems, `+Space = "` ". I work around this, by sending the host + # ` as `+_+BS. Awkward, but the only way I found to get this working. + ('`', '`_{BS}'), + ('´', '´_{BS}'), + ('{^}', '{^}_{BS}'), + ] + + def __init__(self): + # import windows specific modules + import win32com.client + import win32gui + self.win32gui = win32gui + self.shell = win32com.client.Dispatch('WScript.Shell') + + def is_focused(self, title=None): + cur_title = self.win32gui.GetWindowText( + self.win32gui.GetForegroundWindow()) + if (title or '- GVIM') in cur_title: + return True + return False + + def focus(self, title=None): + if not self.shell.AppActivate(title or '- GVIM'): + raise Exception('Failed to switch to GVim window') + time.sleep(1) + + def convert_keys(self, keys): + keys = self.BRACES.sub(r"{\1}", keys) + for k in self.WIN_ESCAPES: + keys = keys.replace(k, '{%s}' % k) + for f, r in self.WIN_REPLACES: + keys = keys.replace(f, r) + return keys + + def send(self, keys): + keys = self.convert_keys(keys) + + if not self.is_focused(): + time.sleep(2) + self.focus() + if not self.is_focused(): + # This is the only way I can find to stop test execution + raise KeyboardInterrupt('Failed to focus GVIM') + + self.shell.SendKeys(keys) diff --git a/test/vim_test_case.py b/test/vim_test_case.py new file mode 100644 index 0000000..f3a6e08 --- /dev/null +++ b/test/vim_test_case.py @@ -0,0 +1,208 @@ +# encoding: utf-8 + +# pylint: skip-file + +import os +import subprocess +import tempfile +import textwrap +import time +import unittest + +from test.constant import PYTHON3, SEQUENCES, EX +from test.vim_interface import create_directory, TempFileManager + + +def plugin_cache_dir(): + """The directory that we check out our bundles to.""" + return os.path.join(tempfile.gettempdir(), 'UltiSnips_test_vim_plugins') + + +class VimTestCase(unittest.TestCase, TempFileManager): + snippets = () + files = {} + text_before = ' --- some text before --- \n\n' + text_after = '\n\n --- some text after --- ' + expected_error = '' + wanted = '' + keys = '' + sleeptime = 0.00 + output = '' + plugins = [] + # Skip this test for the given reason or None for not skipping it. + skip_if = lambda self: None + version = None # Will be set to vim --version output + maxDiff = None # Show all diff output, always. + vim_flavor = None # will be 'vim' or 'neovim'. + expected_python_version = None # If set, we need to check that our Vim is running this python version. + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + TempFileManager.__init__(self, 'Case') + + def runTest(self): + if self.expected_python_version: + self.assertEqual(self.in_vim_python_version, self.expected_python_version) + + # Only checks the output. All work is done in setUp(). + wanted = self.text_before + self.wanted + self.text_after + for i in range(self.retries): + if self.output and self.expected_error: + self.assertRegexpMatches(self.output, self.expected_error) + return + if self.output != wanted or self.output is None: + # Redo this, but slower + self.sleeptime += 0.15 + self.tearDown() + self.setUp() + self.assertMultiLineEqual(self.output, wanted) + + def _extra_vim_config(self, vim_config): + """Adds extra lines to the vim_config list.""" + + def _before_test(self): + """Send these keys before the test runs. + + Used for buffer local variables and other options. + + """ + + def _create_file(self, file_path, content): + """Creates a file in the runtimepath that is created for this test. + + Returns the absolute path to the file. + + """ + return self.write_temp(file_path, textwrap.dedent(content + '\n')) + + def _link_file(self, source, relative_destination): + """Creates a link from 'source' to the 'relative_destination' in our + temp dir.""" + absdir = self.name_temp(relative_destination) + create_directory(absdir) + os.symlink(source, os.path.join(absdir, os.path.basename(source))) + + def setUp(self): + # TODO(sirver): this uses 'vim', but must use --vim from the commandline. + if not VimTestCase.version: + VimTestCase.version, _ = subprocess.Popen(['vim', '--version'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + if PYTHON3: + VimTestCase.version = VimTestCase.version.decode('utf-8') + + if self.plugins and not self.test_plugins: + return self.skipTest('Not testing integration with other plugins.') + reason_for_skipping = self.skip_if() + if reason_for_skipping is not None: + return self.skipTest(reason_for_skipping) + + vim_config = [] + vim_config.append('set nocompatible') + vim_config.append('set runtimepath=$VIMRUNTIME,%s,%s' % ( + os.path.dirname(os.path.dirname(__file__)), self._temp_dir)) + + if self.plugins: + self._link_file( + os.path.join( + plugin_cache_dir(), + 'vim-pathogen', + 'autoload'), + '.') + for plugin in self.plugins: + self._link_file( + os.path.join( + plugin_cache_dir(), + os.path.basename(plugin)), + 'bundle') + vim_config.append('execute pathogen#infect()') + + # Some configurations are unnecessary for vanilla Vim, but Neovim + # defines some defaults differently. + vim_config.append('syntax on') + vim_config.append('filetype plugin indent on') + vim_config.append('set nosmarttab') + vim_config.append('set noautoindent') + vim_config.append('set backspace=""') + vim_config.append('set clipboard=""') + vim_config.append('set encoding=utf-8') + vim_config.append('set fileencoding=utf-8') + vim_config.append('set buftype=nofile') + vim_config.append('set shortmess=at') + vim_config.append('let @" = ""') + assert EX == "\t" # Otherwise you need to change the next line + vim_config.append('let g:UltiSnipsExpandTrigger=""') + vim_config.append('let g:UltiSnipsJumpForwardTrigger="?"') + vim_config.append('let g:UltiSnipsJumpBackwardTrigger="+"') + vim_config.append('let g:UltiSnipsListSnippets="@"') + vim_config.append( + 'let g:UltiSnipsUsePythonVersion="%i"' % + (3 if PYTHON3 else 2)) + vim_config.append('let g:UltiSnipsSnippetDirectories=["us"]') + if self.python_host_prog: + vim_config.append('let g:python_host_prog="%s"' % self.python_host_prog) + if self.python3_host_prog: + vim_config.append('let g:python3_host_prog="%s"' % self.python3_host_prog) + + self._extra_vim_config(vim_config) + + # Finally, add the snippets and some configuration for the test. + vim_config.append('%s << EOF' % ('py3' if PYTHON3 else 'py')) + vim_config.append('from UltiSnips import UltiSnips_Manager\n') + + if len(self.snippets) and not isinstance(self.snippets[0], tuple): + self.snippets = (self.snippets, ) + for s in self.snippets: + sv, content = s[:2] + description = '' + options = '' + priority = 0 + if len(s) > 2: + description = s[2] + if len(s) > 3: + options = s[3] + if len(s) > 4: + priority = s[4] + vim_config.append('UltiSnips_Manager.add_snippet(%r, %r, %r, %r, priority=%i)' % ( + sv, content, description, options, priority)) + + # fill buffer with default text and place cursor in between. + prefilled_text = (self.text_before + self.text_after).splitlines() + vim_config.append('import vim\n') + vim_config.append('vim.current.buffer[:] = %r\n' % prefilled_text) + vim_config.append( + 'vim.current.window.cursor = (max(len(vim.current.buffer)//2, 1), 0)') + + # End of python stuff. + vim_config.append('EOF') + + for name, content in self.files.items(): + self._create_file(name, content) + + self.in_vim_python_version = self.vim.launch(vim_config) + + self._before_test() + + if not self.interrupt: + # Go into insert mode and type the keys but leave Vim some time to + # react. + text = 'i' + self.keys + while text: + to_send = None + for seq in SEQUENCES: + if text.startswith(seq): + to_send = seq + break + to_send = to_send or text[0] + self.vim.send_to_vim(to_send) + time.sleep(self.sleeptime) + text = text[len(to_send):] + self.output = self.vim.get_buffer_data() + + def tearDown(self): + if self.interrupt: + print('Working directory: %s' % (self._temp_dir)) + return + self.vim.leave_with_wait() + self.clear_temp() + +# vim:fileencoding=utf-8:foldmarker={{{#,#}}}: diff --git a/test_all.py b/test_all.py new file mode 100755 index 0000000..7819e02 --- /dev/null +++ b/test_all.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +# encoding: utf-8 +# +# To execute this test requires two terminals, one for running Vim and one +# for executing the test script. Both terminals should have their current +# working directories set to this directory (the one containing this +# test_all.py script). +# +# In one terminal, launch a tmux session named ``vim``: +# $ tmux new -s vim +# +# Now, from another terminal, launch the testsuite: +# $ ./test_all.py +# +# Note: if you want to use Vim against the Python 3 bindings, you must launch the +# test suite using Python 3. For example: +# $ python3 ./test_all.py +# +# For each test, the test_all.py script will launch vim with a vimrc, run the +# test, compare the output and exit vim again. The keys are send using tmux send-keys. +# +# To limit the tests that are executed, specify a pattern to be used to match +# the beginning of the test name. For instance, the following will execute all +# tests that start with "SimpleExpand": +# $ ./test_all.py SimpleExpand +# +# NOTE: The test suite is not working under Windows right now as I have no +# access to a windows system for fixing it. Volunteers welcome. Here are some +# comments from the last time I got the test suite running under windows. +# +# Under windows, COM's SendKeys is used to send keystrokes to the gvim window. +# Note that Gvim must use english keyboard input (choose in windows registry) +# for this to work properly as SendKeys is a piece of chunk. (i.e. it sends +# when you send a | symbol while using german key mappings) + +# pylint: skip-file + +import os +import platform +import subprocess +import unittest +from test.vim_interface import ( + create_directory, tempfile, VimInterfaceTmux, VimInterfaceTmuxNeovim) + + +def plugin_cache_dir(): + """The directory that we check out our bundles to.""" + return os.path.join(tempfile.gettempdir(), 'UltiSnips_test_vim_plugins') + + +def clone_plugin(plugin): + """Clone the given plugin into our plugin directory.""" + dirname = os.path.join(plugin_cache_dir(), os.path.basename(plugin)) + print('Cloning %s -> %s' % (plugin, dirname)) + if os.path.exists(dirname): + print('Skip cloning of %s. Already there.' % plugin) + return + create_directory(dirname) + subprocess.call(['git', 'clone', '--recursive', + '--depth', '1', 'https://github.com/%s' % plugin, dirname]) + + if plugin == 'Valloric/YouCompleteMe': + # CLUTCH: this plugin needs something extra. + subprocess.call(os.path.join(dirname, './install.sh'), cwd=dirname) + + +def setup_other_plugins(all_plugins): + """Creates /tmp/UltiSnips_test_vim_plugins and clones all plugins into + this.""" + clone_plugin('tpope/vim-pathogen') + for plugin in all_plugins: + clone_plugin(plugin) + +if __name__ == '__main__': + import optparse + import sys + + def parse_args(): + p = optparse.OptionParser('%prog [OPTIONS] ') + + p.set_defaults(session='vim', interrupt=False, + verbose=False, retries=4, plugins=False) + + p.add_option('-v', '--verbose', dest='verbose', action='store_true', + help='print name of tests as they are executed') + p.add_option('--clone-plugins', action='store_true', + help='Only clones dependant plugins and exits the test runner.') + p.add_option('--plugins', action='store_true', + help='Run integration tests with other Vim plugins.') + p.add_option('-s', '--session', dest='session', metavar='SESSION', + help='session parameters for the terminal multiplexer SESSION [%default]') + p.add_option('-i', '--interrupt', dest='interrupt', + action='store_true', + help='Stop after defining the snippet. This allows the user ' + 'to interactively test the snippet in vim. You must give ' + 'exactly one test case on the cmdline. The test will always fail.' + ) + p.add_option('-r', '--retries', dest='retries', type=int, + help='How often should each test be retried before it is ' + 'considered failed. Works around flakyness in the terminal ' + 'multiplexer and race conditions in writing to the file system.') + p.add_option('-x', '--exitfirst', dest='exitfirst', action='store_true', + help='exit instantly on first error or failed test.') + p.add_option('--vim', dest='vim', type=str, default='vim', + help='executable to run when launching vim.') + p.add_option('--interface', dest='interface', type=str, default='tmux', + help="Interface to use. Use 'tmux' with vanilla Vim and 'tmux_nvim' " + 'with Neovim.') + p.add_option('--python-host-prog', dest='python_host_prog', type=str, default='', + help='Neovim needs a variable to tell it which python interpretor to use for ' + 'py blocks. This needs to be set to point to the correct python interpretor. ' + 'It is ignored for vanilla Vim.') + p.add_option('--python3-host-prog', dest='python3_host_prog', type=str, default='', + help='See --python-host-prog.') + p.add_option('--expected-python-version', dest='expected_python_version', type=str, default='', + help='If set, each test will check sys.version inside of vim to ' + 'verify we are testing against the expected Python version.') + + o, args = p.parse_args() + return o, args + + def flatten_test_suite(suite): + flatten = unittest.TestSuite() + for test in suite: + if isinstance(test, unittest.TestSuite): + flatten.addTests(flatten_test_suite(test)) + else: + flatten.addTest(test) + return flatten + + def main(): + options, selected_tests = parse_args() + + all_test_suites = unittest.defaultTestLoader.discover(start_dir='test') + + vim = None + vim_flavor = 'vim' + if options.interface == 'tmux': + vim = VimInterfaceTmux(options.vim, options.session) + vim_flavor = 'vim' + else: + vim = VimInterfaceTmuxNeovim(options.vim, options.session) + vim_flavor = 'neovim' + + if not options.clone_plugins and platform.system() == 'Windows': + raise RuntimeError( + 'TODO: TestSuite is broken under windows. Volunteers wanted!.') + # vim = VimInterfaceWindows() + # vim.focus() + + all_other_plugins = set() + + tests = set() + suite = unittest.TestSuite() + + for test in flatten_test_suite(all_test_suites): + test.interrupt = options.interrupt + test.retries = options.retries + test.test_plugins = options.plugins + test.python_host_prog = options.python_host_prog + test.python3_host_prog = options.python3_host_prog + test.expected_python_version = options.expected_python_version + test.vim = vim + test.vim_flavor = vim_flavor + all_other_plugins.update(test.plugins) + + if len(selected_tests): + id = test.id().split('.')[1] + if not any([id.startswith(t) for t in selected_tests]): + continue + tests.add(test) + suite.addTests(tests) + + if options.plugins or options.clone_plugins: + setup_other_plugins(all_other_plugins) + if options.clone_plugins: + return + + v = 2 if options.verbose else 1 + successfull = unittest.TextTestRunner(verbosity=v, + failfast=options.exitfirst).run(suite).wasSuccessful() + return 0 if successfull else 1 + sys.exit(main()) + +# vim:fileencoding=utf-8:foldmarker={{{#,#}}}: diff --git a/travis_install.sh b/travis_install.sh new file mode 100755 index 0000000..d0db077 --- /dev/null +++ b/travis_install.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# Installs a known version of vim in the travis test runner. + +set -ex + +PYTHON="python${TRAVIS_PYTHON_VERSION}" + +build_vanilla_vim () { + mkdir ~/vim_build + pushd ~/vim_build + + if [[ $VIM_VERSION == "74" ]]; then + until curl ftp://ftp.vim.org/pub/vim/unix/vim-7.4.tar.bz2 -o vim.tar.bz2; do sleep 10; done + tar xjf vim.tar.bz2 + cd vim${VIM_VERSION} + elif [[ $VIM_VERSION == "mercurial" ]]; then + hg clone https://vim.googlecode.com/hg/ vim + cd vim + fi + + local PYTHON_CONFIG_DIR=$(dirname $(find $($PYTHON-config --prefix)/lib -iname 'config.c') | grep $TRAVIS_PYTHON_VERSION) + local PYTHON_BUILD_CONFIG="" + if [[ $TRAVIS_PYTHON_VERSION =~ ^2\. ]]; then + PYTHON_BUILD_CONFIG="--enable-pythoninterp --with-python-config-dir=${PYTHON_CONFIG_DIR}" + else + PYTHON_BUILD_CONFIG="--enable-python3interp --with-python3-config-dir=${PYTHON_CONFIG_DIR}" + fi + export LDFLAGS="$($PYTHON-config --ldflags) -L$($PYTHON-config --prefix)/lib" + export CFLAGS="$($PYTHON-config --cflags)" + + # This is needed so that vim finds the shared libraries it was build against + # - they are not on the regular path. + export LD_LIBRARY_PATH="$($PYTHON-config --prefix)/lib" + + echo $LDFLAGS + echo $CFLAGS + ./configure \ + --prefix=${HOME} \ + --disable-nls \ + --disable-sysmouse \ + --disable-gpm \ + --enable-gui=no \ + --enable-multibyte \ + --with-features=huge \ + --with-tlib=ncurses \ + --without-x \ + ${PYTHON_BUILD_CONFIG} || cat $(find . -name 'config.log') + + make install + popd + + rm -rf vim_build +} + +if [[ $VIM_VERSION = "74" || $VIM_VERSION = "mercurial" ]]; then + build_vanilla_vim +elif [[ $VIM_VERSION == "NEOVIM" ]]; then + PIP=$(which pip) + $PIP install neovim +else + echo "Unknown VIM_VERSION: $VIM_VERSION" + exit 1 +fi + +# Clone the dependent plugins we want to use. +PYTHON_CMD="$(which $PYTHON)" +$PYTHON_CMD ./test_all.py --clone-plugins diff --git a/travis_test.sh b/travis_test.sh new file mode 100755 index 0000000..1a6a24a --- /dev/null +++ b/travis_test.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -ex + +PYTHON="python${TRAVIS_PYTHON_VERSION}" +PYTHON_CMD="$(which ${PYTHON})" + +if [[ $VIM_VERSION = "74" || $VIM_VERSION = "mercurial" ]]; then + INTERFACE="--interface tmux" + VIM="${HOME}/bin/vim" + # This is needed so that vim finds the shared libraries it was build against - + # they are not on the regular path. + export LD_LIBRARY_PATH="$($PYTHON-config --prefix)/lib" + +elif [[ $VIM_VERSION == "NEOVIM" ]]; then + VIM="$(which nvim)" + if [[ $TRAVIS_PYTHON_VERSION =~ ^2\. ]]; then + INTERFACE="--interface tmux_nvim --python-host-prog=$PYTHON_CMD" + else + INTERFACE="--interface tmux_nvim --python3-host-prog=$PYTHON_CMD" + fi +else + echo "Unknown VIM_VERSION: $VIM_VERSION" + exit 1 +fi + +PYTHON_VERSION=$($PYTHON_CMD -c 'import sys;print(sys.version.split()[0])') +echo "Using python from: $PYTHON_CMD Version: $PYTHON_VERSION" +echo "Using vim from: $VIM. Version: $($VIMn)" + +tmux new -d -s vim + +$PYTHON_CMD ./test_all.py \ + -v \ + --plugins \ + --session vim \ + --vim $VIM \ + $INTERFACE \ + --expected-python-version $PYTHON_VERSION diff --git a/utils/get_tm_snippets.py b/utils/get_tm_snippets.py new file mode 100755 index 0000000..eb2481d --- /dev/null +++ b/utils/get_tm_snippets.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# encoding: utf-8 + +import urllib +import re +from xml.etree import ElementTree +from xml.parsers.expat import ExpatError +import htmlentitydefs +import os +import glob + + +_UNESCAPE = re.compile(ur'&\w+?;', re.UNICODE) + + +def unescape(s): + if s is None: + return '' + + def fixup(m): + ent = m.group(0)[1:-1] + return unichr(htmlentitydefs.name2codepoint[ent]) + try: + return _UNESCAPE.sub(fixup, s) + except: + print 'unescape failed: %s' % repr(s) + raise + + +class UnknownVariable(Exception): + pass + + +class UnsupportedVariableExpression(Exception): + pass + + +def replace_vars(m): + """Replace vars in 'content' portion. + + :m: match object + :returns: string + + """ + var = m.group(1) + default = m.group(2) + + if not re.match(r'\w+$', var): + raise UnsupportedVariableExpression(var) + + translate_vars = { + 'TM_PHP_OPEN_TAG_WITH_ECHO': 'g:UltiSnipsOpenTagWithEcho', + 'TM_PHP_OPEN_TAG': 'g:UltiSnipsOpenTag', + 'PHPDOC_AUTHOR': 'g:snips_author', + } + # TODO: TM_SELECTED_TEXT/([\t ]*).*/$1/m + + if var in translate_vars: + newvar = translate_vars[var] + else: + # TODO: this could be autogenerated + raise UnknownVariable(var) + + return "`!v exists('%s') ? %s : '%s'`" % (newvar, newvar, default) + + +def parse_content(c): + try: + data = ElementTree.fromstring(c)[0] + + rv = {} + for k, v in zip(data[::2], data[1::2]): + rv[k.text] = unescape(v.text) + + if re.search(r'\$\{\D', rv['content']): + rv['content'] = re.sub( + r'\$\{([^\d}][^}:]*)(?::([^}]*))?\}', + replace_vars, + rv['content']) + + return rv + except (ExpatError, ElementTree.ParseError) as detail: + print ' Syntax Error: %s' % (detail,) + print c + return None + except UnknownVariable as detail: + print ' Unknown variable: %s' % (detail,) + return None + except UnsupportedVariableExpression as detail: + print ' Unsupported variable expression: %s' % (detail,) + return None + + +def fetch_snippets_from_svn(name): + base_url = 'http://svn.textmate.org/trunk/Bundles/' + name + '.tmbundle/' + snippet_idx = base_url + 'Snippets/' + + idx_list = urllib.urlopen(snippet_idx).read() + + rv = [] + for link in re.findall('
  • (.*?)
  • ', idx_list): + m = re.match(r'(.*)', link) + link, name = m.groups() + if name == '..': + continue + + name = unescape(name.rsplit('.', 1)[0]) # remove Extension + print "Fetching data for Snippet '%s'" % name + content = urllib.urlopen(snippet_idx + link).read() + + cont = parse_content(content) + if cont: + rv.append((name, cont)) + + return rv + + +def fetch_snippets_from_dir(path): + """Fetch snippets from a given path.""" + + rv = [] + for filename in glob.glob(os.path.join(path, '*.tmSnippet')): + print 'Reading file %s' % filename + f = open(filename) + content = f.read() + + cont = parse_content(content) + if cont: + name = os.path.splitext(os.path.basename(filename))[0] + rv.append((name, cont)) + return rv + + +def write_snippets(snip_descr, f): + + for name, d in snip_descr: + if 'tabTrigger' not in d: + continue + + if 'content' not in d or d['content'] is None: + print 'SKIP: %s (no content)' % (d,) + continue + + f.write('snippet %s "%s"\n' % (d['tabTrigger'], name)) + f.write(d['content'].encode('utf-8') + '\n') + f.write('endsnippet\n\n') + + +if __name__ == '__main__': + import sys + + bundle = sys.argv[1] + + if os.path.isdir(bundle): + name = sys.argv[2] + rv = fetch_snippets_from_dir(bundle) + else: + rv = fetch_snippets_from_svn(bundle) + name = bundle.lower() + + write_snippets(rv, open('tm_' + name + '.snippets', 'w'))