├── .github └── workflows │ └── mle_test.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── buffer.c ├── bview.c ├── cmd.c ├── cursor.c ├── editor.c ├── keys.h ├── main.c ├── mark.c ├── mlbuf.h ├── mle.1 ├── mle.h ├── snapcraft.yaml ├── termbox2.h ├── tests ├── Makefile ├── func │ ├── test.sh │ ├── test_blist.sh │ ├── test_block.sh │ ├── test_browse.sh │ ├── test_close.sh │ ├── test_coarse_undo.sh │ ├── test_cursor.sh │ ├── test_cut_copy.sh │ ├── test_delete.sh │ ├── test_indent_outdent.sh │ ├── test_insert.sh │ ├── test_issue_51.sh │ ├── test_kmap.sh │ ├── test_lua.sh │ ├── test_macro.sh │ ├── test_move.sh │ ├── test_multi_cursor.sh │ ├── test_open.sh │ ├── test_repeat.sh │ ├── test_search_replace.sh │ ├── test_shell.sh │ ├── test_show_help.sh │ ├── test_syntax.sh │ ├── test_tab.sh │ ├── test_temp_anchor.sh │ ├── test_undo_redo.sh │ └── test_window.sh ├── run.sh └── unit │ ├── .gitignore │ ├── test.c │ ├── test.h │ ├── test_bline_delete.c │ ├── test_bline_get_col.c │ ├── test_bline_insert.c │ ├── test_buffer_add_mark.c │ ├── test_buffer_add_srule.c │ ├── test_buffer_delete.c │ ├── test_buffer_destroy.c │ ├── test_buffer_get.c │ ├── test_buffer_get_bline.c │ ├── test_buffer_get_bline_col.c │ ├── test_buffer_get_offset.c │ ├── test_buffer_insert.c │ ├── test_buffer_new.c │ ├── test_buffer_redo.c │ ├── test_buffer_register.c │ ├── test_buffer_remove_srule.c │ ├── test_buffer_replace.c │ ├── test_buffer_set.c │ ├── test_buffer_set_callback.c │ ├── test_buffer_set_tab_width.c │ ├── test_buffer_srule_overlap.c.todo │ ├── test_buffer_substr.c │ ├── test_buffer_undo.c │ ├── test_kinput.c │ ├── test_mark_clone.c │ ├── test_mark_cmp.c │ ├── test_mark_delete_after.c │ ├── test_mark_delete_before.c │ ├── test_mark_delete_between_mark.c │ ├── test_mark_delete_between_mark_2.c │ ├── test_mark_find_bracket_pair.c │ ├── test_mark_get_between_mark.c │ ├── test_mark_get_nchars_between.c │ ├── test_mark_get_offset.c │ ├── test_mark_insert_after.c │ ├── test_mark_insert_before.c │ ├── test_mark_is_at_word_bound.c │ ├── test_mark_is_gt.c │ ├── test_mark_lettered.c │ ├── test_mark_move_bol.c │ ├── test_mark_move_bracket_pair.c │ ├── test_mark_move_by.c │ ├── test_mark_move_col.c │ ├── test_mark_move_eol.c │ ├── test_mark_move_next_re.c │ ├── test_mark_move_next_str.c │ ├── test_mark_move_offset.c │ ├── test_mark_move_prev_re.c │ ├── test_mark_move_prev_str.c │ ├── test_mark_move_to.c │ ├── test_mark_move_vert.c │ ├── test_mark_set_pcre_ovector.c │ ├── test_mark_swap_with_mark.c │ └── test_recalloc.c ├── uscript.c ├── uscript.inc.c ├── uscript.inc.php ├── uscript.lua ├── util.c └── vendor ├── Makefile └── lua5.4 /.github/workflows/mle_test.yml: -------------------------------------------------------------------------------- 1 | name: mle_test 2 | on: [push, pull_request] 3 | jobs: 4 | mle_test_job: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | with: 9 | submodules: recursive 10 | - run: make test mle_vendor=1 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gdb_history 2 | *.o 3 | mle 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "uthash"] 2 | path = vendor/uthash 3 | url = https://github.com/troydhanson/uthash.git 4 | [submodule "lua"] 5 | path = vendor/lua 6 | url = https://github.com/lua/lua.git 7 | [submodule "pcre2"] 8 | path = vendor/pcre2 9 | url = https://github.com/PhilipHazel/pcre2.git 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.7.2] - 2023-05-24 4 | 5 | ### changed 6 | 7 | - switch from pcre to pcre2 ([`8d8673c`](https://github.com/adsr/mle/commit/8d8673c)) 8 | - rewrite syntax highlighting code ([`7231152`](https://github.com/adsr/mle/commit/7231152)) 9 | - upgrade to uthash 2.3.0 ([`fe53013`](https://github.com/adsr/mle/commit/fe53013)) 10 | - avoid `chdir` in `cmd_browse` ([`3253070`](https://github.com/adsr/mle/commit/3253070)) 11 | 12 | ### added 13 | 14 | - add block (overtype) mode ([`bffa40f`](https://github.com/adsr/mle/commit/bffa40f)) 15 | - add mouse support ([`37919fe`](https://github.com/adsr/mle/commit/37919fe), [`7791647`](https://github.com/adsr/mle/commit/7791647)) 16 | - add lettered-mark support ([`358eec1`](https://github.com/adsr/mle/commit/358eec1)) 17 | - add bview navigation commands ([`619ff20`](https://github.com/adsr/mle/commit/619ff20), [`cd34aed`](https://github.com/adsr/mle/commit/cd34aed)) 18 | - add commands for reverse searching ([`ee39003`](https://github.com/adsr/mle/commit/ee39003), [`9682069`](https://github.com/adsr/mle/commit/9682069), [`2cfbcce`](https://github.com/adsr/mle/commit/2cfbcce)) 19 | - add runtime debug features ([`b3c7f5c`](https://github.com/adsr/mle/commit/b3c7f5c), [`609231f`](https://github.com/adsr/mle/commit/609231f)) 20 | 21 | ### fixed 22 | 23 | - lint man page ([`8c1543e`](https://github.com/adsr/mle/commit/8c1543e)) (Raf Czlonka) 24 | 25 | ## [1.5.0] - 2022-05-27 26 | 27 | ### changed 28 | 29 | - switch from termbox to termbox2 ([`f1c2293`](https://github.com/adsr/mle/commit/f1c2293)) 30 | - upgrade to lua 5.4 ([`fed1717`](https://github.com/adsr/mle/commit/fed1717)) 31 | - improve editor perf in large files ([`dd28398`](https://github.com/adsr/mle/commit/dd28398)) 32 | 33 | ### added 34 | 35 | - add support for ctrl, alt, shift modifiers ([`5f876bb`](https://github.com/adsr/mle/commit/5f876bb)) 36 | - add coarse undo ([`7e05653`](https://github.com/adsr/mle/commit/7e05653)) 37 | - add ability to sigstop and resume editor ([`a19ea77`](https://github.com/adsr/mle/commit/a19ea77)) 38 | - add `snapcraft.yaml` ([`ef2a38a`](https://github.com/adsr/mle/commit/ef2a38a)) 39 | - add `cmd_align_cursors` ([`891d2ed`](https://github.com/adsr/mle/commit/891d2ed)) 40 | - add `cmd_blist` for navigating buffers ([`9853838`](https://github.com/adsr/mle/commit/9853838)) 41 | - add `cmd_insert_newline_below` ([`b016957`](https://github.com/adsr/mle/commit/b016957)) 42 | - add `cmd_repeat` ([`eb37d13`](https://github.com/adsr/mle/commit/eb37d13)) 43 | - add `cmd_swap_anchor` ([`171f52b`](https://github.com/adsr/mle/commit/171f52b)) 44 | - add cursor_select_by "all" ([`2d11ddb`](https://github.com/adsr/mle/commit/2d11ddb)) 45 | 46 | ### fixed 47 | 48 | - fix kmap fallthru bug ([`66cd599`](https://github.com/adsr/mle/commit/66cd599)) 49 | - fix viewport use-after-free bugs ([`8fb367d`](https://github.com/adsr/mle/commit/8fb367d), [`2273cde`](https://github.com/adsr/mle/commit/2273cde)) 50 | - fix handling of tabs and wide chars in soft wrap mode ([`6b3a954`](https://github.com/adsr/mle/commit/6b3a954)) 51 | - fix mystery infinite loop ([`3a154dd`](https://github.com/adsr/mle/commit/3a154dd)) 52 | - fix leak in syntax highlighting code ([`1c49728`](https://github.com/adsr/mle/commit/1c49728)) (Francois Berder) 53 | - permit zero-width chars ([`75136ae`](https://github.com/adsr/mle/commit/75136ae)) 54 | - fix macos compat bugs (Kevin Sjöberg) 55 | 56 | ## [1.4.3] - 2020-02-13 57 | 58 | ### added 59 | 60 | - fallback to malloc if mmap fails ([`dd966bb`](https://github.com/adsr/mle/commit/dd966bb)) 61 | 62 | ## [1.4.2] - 2019-09-25 63 | 64 | ### fixed 65 | 66 | - fix keybind bug ([`31e89a6`](https://github.com/adsr/mle/commit/31e89a6)) 67 | 68 | ## [1.4.1] - 2019-09-20 69 | 70 | ### fixed 71 | 72 | - fix keybind bug ([`af0444b`](https://github.com/adsr/mle/commit/af0444b)) 73 | 74 | ## [1.4.0] - 2019-09-14 75 | 76 | ### changed 77 | 78 | - change to k&r pointer code style ([`02a5565`](https://github.com/adsr/mle/commit/02a5565)) 79 | - switch to semver 80 | 81 | ### added 82 | 83 | - add `cmd_anchor_by` ([`5b42593`](https://github.com/adsr/mle/commit/5b42593)) 84 | 85 | ## [1.3] - 2018-12-21 86 | 87 | ### changed 88 | 89 | - reorganize source tree ([`6dba4fa`](https://github.com/adsr/mle/commit/6dba4fa)) 90 | 91 | ### added 92 | 93 | - add man page ([`2a25d51`](https://github.com/adsr/mle/commit/2a25d51)) 94 | - add `cmd_move_bracket_toggle` for bracket matching ([`21b2deb`](https://github.com/adsr/mle/commit/21b2deb)) 95 | 96 | ## [1.2] - 2018-08-11 97 | 98 | ### added 99 | 100 | - add lua scripting support ([`76f1c7a`](https://github.com/adsr/mle/commit/76f1c7a)) 101 | - add functional test suite ([`af56bea`](https://github.com/adsr/mle/commit/af56bea)) 102 | - add `cmd_jump` for two-letter cursor jumping ([`abd239c`](https://github.com/adsr/mle/commit/abd239c)) 103 | - add `cmd_perl` for executing perl ([`abd239c`](https://github.com/adsr/mle/commit/abd239c)) 104 | - add auto indent ([`9b84541`](https://github.com/adsr/mle/commit/9b84541)) 105 | - add ctag support ([`f708148`](https://github.com/adsr/mle/commit/f708148)) 106 | - add copy paste between buffers ([`5789313`](https://github.com/adsr/mle/commit/5789313)) 107 | 108 | ### removed 109 | 110 | - remove lel edit language ([`8fa4b7d`](https://github.com/adsr/mle/commit/8fa4b7d)) 111 | - remove stdio extensibility ([`a560633`](https://github.com/adsr/mle/commit/a560633)) 112 | 113 | ### fixed 114 | 115 | - fix getopt bug in bsd ([`4a49979`](https://github.com/adsr/mle/commit/4a49979)) (Leonardo Cecchi) 116 | 117 | ## [1.1] - 2017-03-29 118 | 119 | _[initial release](https://lists.suckless.org/dev/1703/31240.html)_ 120 | 121 | [1.7.2]: https://github.com/adsr/mle/releases/tag/v1.7.2 122 | [1.5.0]: https://github.com/adsr/mle/releases/tag/v1.5.0 123 | [1.4.3]: https://github.com/adsr/mle/releases/tag/v1.4.3 124 | [1.4.2]: https://github.com/adsr/mle/releases/tag/v1.4.2 125 | [1.4.1]: https://github.com/adsr/mle/releases/tag/v1.4.1 126 | [1.4.0]: https://github.com/adsr/mle/releases/tag/v1.4.0 127 | [1.3]: https://github.com/adsr/mle/releases/tag/v1.3 128 | [1.2]: https://github.com/adsr/mle/releases/tag/v1.2 129 | [1.1]: https://github.com/adsr/mle/releases/tag/v1.1 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prefix?=/usr/local 2 | 3 | mle_cflags:=-std=c99 -Wall -Wextra -pedantic -Wno-pointer-arith -Wno-unused-result -Wno-unused-parameter -g -O0 -D_GNU_SOURCE -DPCRE2_CODE_UNIT_WIDTH=8 -I. $(CFLAGS) $(CPPFLAGS) 4 | mle_ldflags:=$(LDFLAGS) 5 | mle_dynamic_libs:=-lpcre2-8 -llua5.4 6 | mle_static_libs:=vendor/pcre2/libpcre2-8.a vendor/lua/liblua5.4.a 7 | mle_ldlibs:=-lm $(LDLIBS) 8 | mle_objects:=$(patsubst %.c,%.o,$(filter-out %.inc.c,$(wildcard *.c))) 9 | mle_objects_no_main:=$(filter-out main.o,$(mle_objects)) 10 | mle_func_tests:=$(wildcard tests/func/test_*.sh)) 11 | mle_unit_tests:=$(patsubst %.c,%,$(wildcard tests/unit/test_*.c)) 12 | mle_unit_test_objects:=$(patsubst %.c,%.o,$(wildcard tests/unit/test_*.c)) 13 | mle_unit_test_all:=tests/unit/test 14 | mle_vendor_deps:= 15 | mle_static_var:= 16 | mle_git_sha:=$(shell test -d .git && command -v git >/dev/null && git rev-parse --short HEAD) 17 | 18 | ifdef mle_static 19 | mle_static_var:=-static 20 | endif 21 | 22 | ifdef mle_vendor 23 | mle_ldlibs:=$(mle_static_libs) $(mle_ldlibs) 24 | mle_cflags:=-Ivendor/pcre2/src -Ivendor -Ivendor/uthash/src $(mle_cflags) 25 | mle_vendor_deps:=$(mle_static_libs) 26 | else 27 | mle_ldlibs:=$(mle_dynamic_libs) $(mle_ldlibs) 28 | endif 29 | 30 | ifneq ($(mle_git_sha),) 31 | mle_cflags:=-DMLE_VERSION_APPEND=-$(mle_git_sha) $(mle_cflags) 32 | endif 33 | 34 | all: mle 35 | 36 | mle: $(mle_vendor_deps) $(mle_objects) termbox2.h 37 | $(CC) $(mle_static_var) $(mle_cflags) $(mle_objects) $(mle_ldflags) $(mle_ldlibs) -o mle 38 | 39 | $(mle_objects): %.o: %.c 40 | $(CC) -c $(mle_cflags) $< -o $@ 41 | 42 | $(mle_vendor_deps): 43 | $(MAKE) -C vendor 44 | 45 | $(mle_unit_test_objects): %.o: %.c 46 | $(CC) -DTEST_NAME=$(basename $(notdir $<)) -c $(mle_cflags) $< -o $@ 47 | 48 | $(mle_unit_test_all): $(mle_objects_no_main) $(mle_unit_test_objects) $(mle_unit_test_all).c $(mle_vendor_deps) 49 | $(CC) $(mle_cflags) -rdynamic $(mle_unit_test_all).c $(mle_objects_no_main) $(mle_unit_test_objects) $(mle_ldflags) $(mle_ldlibs) -ldl -o $@ 50 | 51 | $(mle_unit_tests): %: $(mle_unit_test_all) 52 | { echo "#!/bin/sh"; echo "$(abspath $(mle_unit_test_all)) $(notdir $@)"; } >$@ && chmod +x $@ 53 | 54 | test: mle $(mle_unit_tests) 55 | ./mle -v && export MLE=$$(pwd)/mle && $(MAKE) -C tests 56 | 57 | sloc: 58 | find . -maxdepth 1 \ 59 | '(' -name '*.c' -or -name '*.h' ')' \ 60 | -not -name 'termbox2.h' \ 61 | -exec cat {} ';' | wc -l 62 | 63 | install: mle 64 | install -v -d $(DESTDIR)$(prefix)/bin 65 | install -v -m 755 mle $(DESTDIR)$(prefix)/bin/mle 66 | 67 | uninstall: 68 | rm -v $(DESTDIR)$(prefix)/bin/mle 69 | 70 | uscript: 71 | php uscript.inc.php >uscript.inc.c 72 | 73 | clean_quick: 74 | rm -f mle $(mle_objects) $(mle_unit_test_objects) $(mle_unit_tests) $(mle_unit_test_all) 75 | 76 | clean: 77 | rm -f mle $(mle_objects) $(mle_vendor_deps) $(mle_unit_test_objects) $(mle_unit_tests) $(mle_unit_test_all) 78 | $(MAKE) -C vendor clean 79 | 80 | .NOTPARALLEL: 81 | 82 | .PHONY: all test sloc install uscript clean 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mle 2 | 3 | mle is a small, flexible, terminal-based text editor written in C. 4 | 5 | Runs on Linux, Windows (Cygwin or WSL), FreeBSD, macOS, and more. 6 | 7 | [![Build Status](https://travis-ci.org/adsr/mle.svg?branch=master)](https://travis-ci.org/adsr/mle) 8 | 9 | ### Demos 10 | 11 | [![asciicast](https://i.imgur.com/PZocaOT.png)](https://asciinema.org/a/162536) 12 | 13 | * [AceJump-like movement](https://i.imgur.com/atS11HX.gif) 14 | * [Large file benchmark](http://i.imgur.com/VGGMmGg.gif) 15 | * [Older demos](http://imgur.com/a/ZBmmQ) 16 | 17 | ### Aims 18 | 19 | * Keep codebase small and hackable 20 | * Minimize build-time and run-time dependencies 21 | * Favor simplicity over correctness 22 | * Make extensible and configurable 23 | * Use shell commands to enhance functionality (e.g., grep, tree) 24 | 25 | ### Features 26 | 27 | * Small codebase (~12k SLOC) 28 | * Full UTF-8 support 29 | * Syntax highlighting 30 | * Stackable key maps (modes) 31 | * Extensible via [Lua](https://www.lua.org) 32 | * Scriptable rc file 33 | * Key macros 34 | * Window splitting 35 | * Regex search and replace 36 | * Large file support 37 | * Incremental search 38 | * Linear undo and redo 39 | * Multiple cursors 40 | * Auto indent 41 | * Headless mode 42 | * Navigation via [ctags](https://github.com/universal-ctags/ctags) 43 | * Movement via [less](https://www.gnu.org/software/less/) 44 | * Fuzzy file search via [fzf](https://github.com/junegunn/fzf) 45 | * File browsing via [tree](http://mama.indstate.edu/users/ice/tree/) 46 | * File grep via [grep](https://www.gnu.org/software/grep/) 47 | * String manip via [perl](https://www.perl.org/) 48 | 49 | ### Building 50 | 51 | $ sudo apt install git build-essential # install git, make, gcc, libc-dev 52 | $ 53 | $ git clone --recursive https://github.com/adsr/mle.git 54 | $ cd mle 55 | $ make mle_vendor=1 56 | 57 | To build a completely static binary, try `make mle_vendor=1 mle_static=1`. 58 | 59 | You can also run plain `make` to link against system libraries instead of 60 | `vendor/`. Note this requires the following packages to be installed: 61 | 62 | uthash-dev 63 | liblua5.4-dev 64 | libpcre2-dev 65 | 66 | To install to `/usr/local/bin`: 67 | 68 | $ make install 69 | 70 | To install to a custom directory, supply `prefix`, e.g.: 71 | 72 | $ make install prefix=/usr # /usr/bin/mle 73 | 74 | ### Installing from a repo 75 | 76 | mle may be available to install via your system's package manager. 77 | 78 | # apt install mle # Ubuntu and Debian-based distros 79 | # dnf install mle # CentOS, RHEL, Fedora-based distros 80 | # pkg install mle # FreeBSD 81 | # yay -S mle # Arch (via AUR) 82 | # snap install mle # all major Linux distros 83 | # nix-env -i mle # NixOS (via nixpkgs) 84 | # apk add mle # Alpine 85 | # xbps-install mle # Void 86 | # brew install mle # macOS (Homebrew) 87 | # port install mle # macOS (MacPorts) 88 | # setup-x86.exe -q -P mle # Cygwin 89 | 90 | [![Packaging status](https://repology.org/badge/vertical-allrepos/mle.svg)](https://repology.org/project/mle/versions) 91 | 92 | ### Basic usage 93 | 94 | $ mle # Open blank buffer 95 | $ mle one.c # Edit one.c 96 | $ mle one.c:100 # Edit one.c at line 100 97 | $ mle one.c two.c # Edit one.c and two.c 98 | $ mle -h # Show command line help 99 | 100 | The default key bindings are intuitive. Input text as normal, use directional 101 | keys to move around, use `Ctrl-S` to save, `Ctrl-O` to open, `Ctrl-X` to exit. 102 | 103 | Press `F2` for full help. 104 | 105 | ### Advanced usage: mlerc 106 | 107 | mle is customized via command line options. Run `mle -h` to view all cli 108 | options. 109 | 110 | To set default options, make an rc file named `~/.mlerc` (or `/etc/mlerc`). The 111 | contents of the rc file are any number of cli options separated by newlines. 112 | Lines that begin with a semi-colon are interpretted as comments. 113 | 114 | If `~/.mlerc` is executable, mle executes it and interprets the resulting stdout 115 | as described above. For example, consider the following snippet from an 116 | executable `~/.mlerc` bash(1) script: 117 | 118 | # Define 'test' kmap 119 | echo '-Ktest,,1' 120 | 121 | # M-q: replace grep with git grep if `.git` exists 122 | if [ -d ".git" ]; then 123 | echo '-kcmd_grep,M-q,git grep --color=never -P -i -I -n %s 2>/dev/null' 124 | fi 125 | 126 | # Set default kmap 127 | echo '-n test' 128 | 129 | This overrides the built-in grep command with `git grep` if `.git` exists in 130 | the current working directory. 131 | 132 | ### Shell command integration 133 | 134 | The following programs will enable or enhance certain features of mle if they 135 | exist in `PATH`. 136 | 137 | * [bash](https://www.gnu.org/software/bash/) (tab completion) 138 | * [fzf](https://github.com/junegunn/fzf) (fuzzy file search) 139 | * [grep](https://www.gnu.org/software/grep/) (file grep) 140 | * [less](https://www.gnu.org/software/less/) (less integration) 141 | * [perl](https://www.perl.org/) (perl 1-liners) 142 | * [readtags](https://github.com/universal-ctags/ctags) (ctags integration) 143 | * [tree](http://mama.indstate.edu/users/ice/tree/) (file browsing) 144 | 145 | Arbitrary shell commands can also be run via `cmd_shell` (`M-e` by default). If 146 | any text is selected, it is sent to stdin of the command. Any resulting stdout 147 | is inserted into the text buffer. 148 | 149 | ### Advanced usage: Headless mode 150 | 151 | mle provides support for non-interactive editing which may be useful for using 152 | the editor as a regular command line tool. In headless mode, mle reads stdin 153 | into a buffer, applies a startup macro if specified, and then writes the buffer 154 | contents to stdout. For example: 155 | 156 | $ echo -n hello | mle -M 'test C-e space w o r l d enter' -p test 157 | hello world 158 | 159 | If stdin is a pipe, mle goes into headless mode automatically. Headless mode can 160 | be explicitly enabled or disabled with the `-H` option. 161 | 162 | If stdin is a pipe and headless mode is disabled via `-H0`, mle reads stdin into 163 | a new buffer and then runs as normal in interactive mode. 164 | 165 | ### Advanced usage: Scripting 166 | 167 | mle is extensible via the [Lua](https://www.lua.org) programming language. 168 | Scripts are loaded via the `-x` cli option. Commands registered by scripts can 169 | be mapped to keys as normal via `-k`. See `uscript.lua` for a simple example. 170 | 171 | There is also a `wren` branch with [Wren](http://wren.io) scripting support. 172 | That work is on pause. 173 | 174 | ### Forks 175 | 176 | * [eon](https://github.com/tomas/eon) - mouse support and Notepad-like 177 | selections 178 | * [turbo-mle](https://github.com/magiblot/turbo-mle) - Turbo Vision port 179 | 180 | ### Acknowledgments 181 | 182 | mle makes extensive use of the following libraries. 183 | 184 | * [uthash](https://troydhanson.github.io/uthash) for hash maps and linked lists 185 | * [termbox2](https://github.com/termbox/termbox2) for TUI 186 | * [PCRE2](http://www.pcre.org/) for syntax highlighting and search 187 | -------------------------------------------------------------------------------- /cursor.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "mle.h" 4 | 5 | static int cursor_uncut_ex(cursor_t *cursor, int editor_wide); 6 | 7 | // Clone cursor 8 | int cursor_clone(cursor_t *cursor, int use_srules, cursor_t **ret_clone) { 9 | cursor_t *clone; 10 | bview_add_cursor(cursor->bview, cursor->mark->bline, cursor->mark->col, &clone); 11 | if (cursor->is_anchored) { 12 | cursor_toggle_anchor(clone, use_srules); 13 | mark_join(clone->anchor, cursor->anchor); 14 | } 15 | *ret_clone = clone; 16 | return MLE_OK; 17 | } 18 | 19 | // Remove cursor 20 | int cursor_destroy(cursor_t *cursor) { 21 | return bview_remove_cursor(cursor->bview, cursor); 22 | } 23 | 24 | // Select by mark 25 | int cursor_select_between(cursor_t *cursor, mark_t *a, mark_t *b, int use_srules) { 26 | cursor_drop_anchor(cursor, use_srules); 27 | if (mark_is_lt(a, b)) { 28 | mark_join(cursor->mark, a); 29 | mark_join(cursor->anchor, b); 30 | } else { 31 | mark_join(cursor->mark, b); 32 | mark_join(cursor->anchor, a); 33 | } 34 | return MLE_OK; 35 | } 36 | 37 | // Toggle cursor anchor 38 | int cursor_toggle_anchor(cursor_t *cursor, int use_srules) { 39 | if (!cursor->is_anchored && !cursor->is_temp_anchored) { 40 | mark_clone(cursor->mark, &(cursor->anchor)); 41 | if (use_srules) { 42 | cursor->sel_rule = srule_new_range(cursor->mark, cursor->anchor, 0, TB_REVERSE); 43 | buffer_add_srule(cursor->bview->buffer, cursor->sel_rule); 44 | } 45 | cursor->is_anchored = 1; 46 | } else { 47 | if (use_srules && cursor->sel_rule) { 48 | buffer_remove_srule(cursor->bview->buffer, cursor->sel_rule); 49 | srule_destroy(cursor->sel_rule); 50 | cursor->sel_rule = NULL; 51 | } 52 | mark_destroy(cursor->anchor); 53 | cursor->is_anchored = 0; 54 | cursor->is_temp_anchored = 0; 55 | } 56 | return MLE_OK; 57 | } 58 | 59 | // Drop cursor anchor 60 | int cursor_drop_anchor(cursor_t *cursor, int use_srules) { 61 | if (cursor->is_anchored) return MLE_OK; 62 | return cursor_toggle_anchor(cursor, use_srules); 63 | } 64 | 65 | // Lift cursor anchor 66 | int cursor_lift_anchor(cursor_t *cursor) { 67 | if (!cursor->is_anchored) return MLE_OK; 68 | return cursor_toggle_anchor(cursor, cursor->sel_rule ? 1 : 0); 69 | } 70 | 71 | // Get lo and hi marks in a is_anchored=1 cursor 72 | int cursor_get_lo_hi(cursor_t *cursor, mark_t **ret_lo, mark_t **ret_hi) { 73 | if (!cursor->is_anchored) { 74 | return MLE_ERR; 75 | } 76 | if (mark_is_gt(cursor->anchor, cursor->mark)) { 77 | *ret_lo = cursor->mark; 78 | *ret_hi = cursor->anchor; 79 | } else { 80 | *ret_lo = cursor->anchor; 81 | *ret_hi = cursor->mark; 82 | } 83 | return MLE_OK; 84 | } 85 | 86 | // Get mark 87 | int cursor_get_mark(cursor_t *cursor, mark_t **ret_mark) { 88 | *ret_mark = cursor->mark; 89 | return MLE_OK; 90 | } 91 | 92 | // Get anchor if anchored 93 | int cursor_get_anchor(cursor_t *cursor, mark_t **ret_anchor) { 94 | *ret_anchor = cursor->is_anchored ? cursor->anchor : NULL; 95 | return MLE_OK; 96 | } 97 | 98 | // Make selection by strat 99 | int cursor_select_by(cursor_t *cursor, const char *strat, int use_srules) { 100 | if (cursor->is_anchored) { 101 | return MLE_ERR; 102 | } 103 | if (strcmp(strat, "bracket") == 0) { 104 | return cursor_select_by_bracket(cursor, use_srules); 105 | } else if (strcmp(strat, "word") == 0) { 106 | return cursor_select_by_word(cursor, use_srules); 107 | } else if (strcmp(strat, "word_back") == 0) { 108 | return cursor_select_by_word_back(cursor, use_srules); 109 | } else if (strcmp(strat, "word_forward") == 0) { 110 | return cursor_select_by_word_forward(cursor, use_srules); 111 | } else if (strcmp(strat, "eol") == 0) { 112 | cursor_toggle_anchor(cursor, use_srules); 113 | mark_move_eol(cursor->anchor); 114 | } else if (strcmp(strat, "bol") == 0) { 115 | cursor_toggle_anchor(cursor, use_srules); 116 | mark_move_bol(cursor->anchor); 117 | } else if (strcmp(strat, "string") == 0) { 118 | return cursor_select_by_string(cursor, use_srules); 119 | } else if (strcmp(strat, "all") == 0) { 120 | mark_move_beginning(cursor->mark); 121 | cursor_toggle_anchor(cursor, use_srules); 122 | mark_move_end(cursor->mark); 123 | } else { 124 | MLE_RETURN_ERR(cursor->bview->editor, "Unrecognized cursor_select_by strat '%s'", strat); 125 | } 126 | return MLE_OK; 127 | } 128 | 129 | // Select by bracket 130 | int cursor_select_by_bracket(cursor_t *cursor, int use_srules) { 131 | mark_t *orig; 132 | mark_clone(cursor->mark, &orig); 133 | if (mark_move_bracket_top(cursor->mark, MLE_BRACKET_PAIR_MAX_SEARCH) != MLBUF_OK) { 134 | mark_destroy(orig); 135 | return MLE_ERR; 136 | } 137 | cursor_toggle_anchor(cursor, use_srules); 138 | if (mark_move_bracket_pair(cursor->anchor, MLE_BRACKET_PAIR_MAX_SEARCH) != MLBUF_OK) { 139 | cursor_toggle_anchor(cursor, use_srules); 140 | mark_join(cursor->mark, orig); 141 | mark_destroy(orig); 142 | return MLE_ERR; 143 | } 144 | mark_move_by(cursor->mark, 1); 145 | mark_destroy(orig); 146 | return MLE_OK; 147 | } 148 | 149 | // Select by word-back 150 | int cursor_select_by_word_back(cursor_t *cursor, int use_srules) { 151 | if (mark_is_at_word_bound(cursor->mark, -1)) return MLE_ERR; 152 | cursor_toggle_anchor(cursor, use_srules); 153 | mark_move_prev_re(cursor->mark, MLE_RE_WORD_BACK, sizeof(MLE_RE_WORD_BACK)-1); 154 | return MLE_OK; 155 | } 156 | 157 | // Select by word-forward 158 | int cursor_select_by_word_forward(cursor_t *cursor, int use_srules) { 159 | if (mark_is_at_word_bound(cursor->mark, 1)) return MLE_ERR; 160 | cursor_toggle_anchor(cursor, use_srules); 161 | mark_move_next_re(cursor->mark, MLE_RE_WORD_FORWARD, sizeof(MLE_RE_WORD_FORWARD)-1); 162 | return MLE_OK; 163 | } 164 | 165 | // Select by string 166 | int cursor_select_by_string(cursor_t *cursor, int use_srules) { 167 | mark_t *orig; 168 | uint32_t qchar; 169 | char *qre1 = "(?mark, &orig); 174 | 175 | for (left_right = 0; left_right <= 1; left_right++) { 176 | mark_join(cursor->mark, orig); 177 | 178 | // left_right==0 initially look left for quote 179 | // left_right==1 initially look right for quote 180 | rv = (left_right == 0 ? mark_move_prev_re : mark_move_next_re) 181 | (cursor->mark, qre1, strlen(qre1)); 182 | if (rv != MLBUF_OK) continue; 183 | 184 | // Get quote char and make qre2 185 | mark_get_char_after(cursor->mark, &qchar); 186 | snprintf(qre2, sizeof(qre2), "(?mark, 1); 190 | cursor_drop_anchor(cursor, use_srules); 191 | 192 | // left_right==0 look right for quote pair 193 | // left_right==1 look left for quote pair 194 | rv = (left_right == 0 ? mark_move_next_re : mark_move_prev_re) 195 | (cursor->anchor, qre2, strlen(qre2)); 196 | if (rv != MLBUF_OK) continue; 197 | 198 | // Selected! 199 | if (left_right == 1) mark_move_by(cursor->anchor, 1); 200 | mark_destroy(orig); 201 | return MLE_OK; 202 | } 203 | 204 | cursor_lift_anchor(cursor); 205 | mark_join(cursor->mark, orig); 206 | mark_destroy(orig); 207 | return MLE_ERR; 208 | } 209 | 210 | // Select by word 211 | int cursor_select_by_word(cursor_t *cursor, int use_srules) { 212 | uint32_t after; 213 | if (mark_is_at_eol(cursor->mark)) return MLE_ERR; 214 | mark_get_char_after(cursor->mark, &after); 215 | if (!isalnum((char)after) && (char)after != '_') return MLE_ERR; 216 | if (!mark_is_at_word_bound(cursor->mark, -1)) { 217 | mark_move_prev_re(cursor->mark, MLE_RE_WORD_BACK, sizeof(MLE_RE_WORD_BACK)-1); 218 | } 219 | cursor_toggle_anchor(cursor, use_srules); 220 | mark_move_next_re(cursor->mark, MLE_RE_WORD_FORWARD, sizeof(MLE_RE_WORD_FORWARD)-1); 221 | return MLE_OK; 222 | } 223 | 224 | // Cut or copy text 225 | int cursor_cut_copy(cursor_t *cursor, int is_cut, int use_srules, int append) { 226 | char *cutbuf; 227 | bint_t cutbuf_len; 228 | bint_t cur_len; 229 | if (!append && cursor->cut_buffer) { 230 | free(cursor->cut_buffer); 231 | cursor->cut_buffer = NULL; 232 | } 233 | if (!cursor->is_anchored) { 234 | use_srules = 0; 235 | cursor_toggle_anchor(cursor, use_srules); 236 | mark_move_bol(cursor->mark); 237 | mark_move_eol(cursor->anchor); 238 | if (!cursor->is_block) mark_move_by(cursor->anchor, 1); 239 | } 240 | if (cursor->is_block) { 241 | mark_block_get_between(cursor->mark, cursor->anchor, &cutbuf, &cutbuf_len); 242 | } else { 243 | mark_get_between(cursor->mark, cursor->anchor, &cutbuf, &cutbuf_len); 244 | } 245 | if (append && cursor->cut_buffer) { 246 | cur_len = strlen(cursor->cut_buffer); 247 | cursor->cut_buffer = realloc(cursor->cut_buffer, cur_len + cutbuf_len + 1); 248 | strncat(cursor->cut_buffer, cutbuf, cutbuf_len); 249 | free(cutbuf); 250 | } else { 251 | cursor->cut_buffer = cutbuf; 252 | } 253 | if (cursor->bview->editor->cut_buffer) free(cursor->bview->editor->cut_buffer); 254 | cursor->bview->editor->cut_buffer = strdup(cursor->cut_buffer); 255 | if (is_cut) { 256 | if (cursor->is_block) { 257 | mark_block_delete_between(cursor->mark, cursor->anchor); 258 | } else { 259 | mark_delete_between(cursor->mark, cursor->anchor); 260 | } 261 | } 262 | cursor_toggle_anchor(cursor, use_srules); 263 | return MLE_OK; 264 | } 265 | 266 | // Uncut from cursor cut_buffer 267 | int cursor_uncut(cursor_t *cursor) { 268 | return cursor_uncut_ex(cursor, 0); 269 | } 270 | 271 | // Uncut editor-wide cut_buffer 272 | int cursor_uncut_last(cursor_t *cursor) { 273 | return cursor_uncut_ex(cursor, 1); 274 | } 275 | 276 | // Regex search and replace 277 | int cursor_replace(cursor_t *cursor, int interactive, char *opt_regex, char *opt_replacement) { 278 | return cursor_replace_ex(cursor, interactive, opt_regex, opt_replacement, NULL, NULL, NULL, NULL); 279 | } 280 | 281 | // Regex search and replace (extended params) 282 | int cursor_replace_ex(cursor_t *cursor, int interactive, char *opt_regex, char *opt_replacement, char *opt_cmd_name, int *inout_all, int *optret_num_replacements, int *optret_cancelled) { 283 | char *regex; 284 | char *replacement; 285 | int wrapped; 286 | int all; 287 | char *yn; 288 | mark_t *lo_mark; 289 | mark_t *hi_mark; 290 | mark_t *orig_mark; 291 | mark_t *search_mark; 292 | mark_t *search_mark_end; 293 | int anchored_before; 294 | srule_t *highlight; 295 | bline_t *bline; 296 | bint_t col; 297 | bint_t char_count; 298 | bint_t orig_viewport_y; 299 | int pcre_rc; 300 | PCRE2_SIZE pcre_ovector[30]; 301 | str_t repl_backref = {0}; 302 | int num_replacements; 303 | char *cmd_name; 304 | 305 | if (!interactive && (!opt_regex || !opt_replacement)) { 306 | return MLE_ERR; 307 | } else if ((opt_regex && !opt_replacement) || (!opt_regex && opt_replacement)) { 308 | return MLE_ERR; 309 | } 310 | 311 | regex = NULL; 312 | replacement = NULL; 313 | wrapped = 0; 314 | lo_mark = NULL; 315 | hi_mark = NULL; 316 | orig_mark = NULL; 317 | search_mark = NULL; 318 | search_mark_end = NULL; 319 | anchored_before = 0; 320 | all = interactive ? (inout_all ? *inout_all : 0) : 1; 321 | num_replacements = 0; 322 | mark_set_pcre_capture(&pcre_rc, pcre_ovector, 30); 323 | orig_viewport_y = -1; 324 | cmd_name = opt_cmd_name ? opt_cmd_name : "replace"; 325 | 326 | do { 327 | if (!interactive || (opt_regex && opt_replacement)) { 328 | regex = strdup(opt_regex); 329 | replacement = strdup(opt_replacement); 330 | } else { 331 | editor_prompt_fmt(cursor->bview->editor, NULL, ®ex, "%s: Search regex?", cmd_name); 332 | if (!regex) break; 333 | editor_prompt_fmt(cursor->bview->editor, NULL, &replacement, "%s: Replacement string?", cmd_name); 334 | if (!replacement) break; 335 | } 336 | orig_mark = buffer_add_mark(cursor->bview->buffer, NULL, 0); 337 | lo_mark = buffer_add_mark(cursor->bview->buffer, NULL, 0); 338 | hi_mark = buffer_add_mark(cursor->bview->buffer, NULL, 0); 339 | search_mark = buffer_add_mark(cursor->bview->buffer, NULL, 0); 340 | search_mark_end = buffer_add_mark(cursor->bview->buffer, NULL, 0); 341 | mark_join(search_mark, cursor->mark); 342 | mark_join(orig_mark, cursor->mark); 343 | orig_viewport_y = cursor->bview->viewport_mark->bline->line_index; 344 | orig_mark->lefty = 1; // lefty==1 avoids moving when text is inserted at mark 345 | lo_mark->lefty = 1; 346 | if (cursor->is_anchored) { 347 | anchored_before = mark_is_gt(cursor->mark, cursor->anchor); 348 | mark_join(lo_mark, !anchored_before ? cursor->mark : cursor->anchor); 349 | mark_join(hi_mark, anchored_before ? cursor->mark : cursor->anchor); 350 | } else { 351 | mark_move_beginning(lo_mark); 352 | mark_move_end(hi_mark); 353 | } 354 | while (1) { 355 | pcre_rc = 0; 356 | // TODO compile regex 357 | if (mark_find_next_re(search_mark, regex, strlen(regex), &bline, &col, &char_count) == MLBUF_OK 358 | && (mark_move_to(search_mark, bline->line_index, col) == MLBUF_OK) 359 | && (mark_is_gte(search_mark, lo_mark)) 360 | && (mark_is_lt(search_mark, hi_mark)) 361 | && (!wrapped || mark_is_lt(search_mark, orig_mark)) 362 | ) { 363 | mark_move_to(search_mark_end, bline->line_index, col + char_count); 364 | mark_join(cursor->mark, search_mark); 365 | yn = NULL; 366 | if (all) { 367 | yn = MLE_PROMPT_YES; 368 | } else if (interactive) { 369 | highlight = srule_new_range(search_mark, search_mark_end, 0, TB_REVERSE); 370 | buffer_add_srule(cursor->bview->buffer, highlight); 371 | bview_rectify_viewport(cursor->bview); 372 | bview_draw(cursor->bview); 373 | editor_prompt_fmt(cursor->bview->editor, 374 | &(editor_prompt_params_t) { .kmap = cursor->bview->editor->kmap_prompt_yna }, &yn, 375 | "%s: OK to replace? (y=yes, n=no, a=all, C-c=stop)", cmd_name 376 | ); 377 | buffer_remove_srule(cursor->bview->buffer, highlight); 378 | srule_destroy(highlight); 379 | bview_draw(cursor->bview); 380 | } 381 | // TODO can be more efficient 382 | if (!yn) { 383 | if (optret_cancelled) *optret_cancelled = 1; 384 | break; 385 | } else if (0 == strcmp(yn, MLE_PROMPT_YES) || 0 == strcmp(yn, MLE_PROMPT_ALL)) { 386 | str_append_replace_with_backrefs(&repl_backref, search_mark->bline->data, replacement, pcre_rc, pcre_ovector, 30); 387 | if (mark_is_eq(search_mark, search_mark_end) && repl_backref.len <= 0) { 388 | mark_move_by(search_mark, 1); 389 | } else { 390 | mark_replace_between(search_mark, search_mark_end, repl_backref.data, repl_backref.len); 391 | num_replacements += 1; 392 | } 393 | str_free(&repl_backref); 394 | if (0 == strcmp(yn, MLE_PROMPT_ALL)) all = 1; 395 | } else { 396 | mark_move_by(search_mark, 1); 397 | } 398 | } else if (!wrapped) { 399 | mark_join(search_mark, lo_mark); 400 | wrapped = 1; 401 | } else { 402 | break; 403 | } 404 | } 405 | } while (0); 406 | 407 | if (cursor->is_anchored && lo_mark && hi_mark) { 408 | mark_join(cursor->mark, anchored_before ? hi_mark : lo_mark); 409 | mark_join(cursor->anchor, anchored_before ? lo_mark : hi_mark); 410 | } else if (orig_mark) { 411 | mark_join(cursor->mark, orig_mark); 412 | } 413 | 414 | mark_set_pcre_capture(NULL, NULL, 0); 415 | if (regex) free(regex); 416 | if (replacement) free(replacement); 417 | if (lo_mark) mark_destroy(lo_mark); 418 | if (hi_mark) mark_destroy(hi_mark); 419 | if (orig_mark) mark_destroy(orig_mark); 420 | if (search_mark) mark_destroy(search_mark); 421 | if (search_mark_end) mark_destroy(search_mark_end); 422 | 423 | if (interactive) { 424 | if (optret_num_replacements) { 425 | *optret_num_replacements = num_replacements; 426 | } else { 427 | MLE_SET_INFO(cursor->bview->editor, "%s: Replaced %d instance(s)", cmd_name, num_replacements); 428 | } 429 | if (orig_viewport_y >= 0) { 430 | bview_set_viewport_y(cursor->bview, orig_viewport_y, 1); 431 | } else { 432 | bview_rectify_viewport(cursor->bview); 433 | } 434 | bview_draw(cursor->bview); 435 | } 436 | 437 | if (inout_all) *inout_all = all; 438 | 439 | return MLE_OK; 440 | } 441 | 442 | // Uncut (paste) text 443 | static int cursor_uncut_ex(cursor_t *cursor, int editor_wide) { 444 | char *cut_buffer; 445 | cut_buffer = cursor->cut_buffer && !editor_wide ? cursor->cut_buffer : cursor->bview->editor->cut_buffer; 446 | if (!cut_buffer) return MLE_ERR; 447 | if (cursor->is_block) { 448 | mark_block_insert_before(cursor->mark, cut_buffer, strlen(cut_buffer)); 449 | } else { 450 | mark_insert_before(cursor->mark, cut_buffer, strlen(cut_buffer)); 451 | } 452 | return MLE_OK; 453 | } 454 | -------------------------------------------------------------------------------- /keys.h: -------------------------------------------------------------------------------- 1 | // kname modmin modadd ch key 2 | MLE_KEY_DEF("space", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_TILDE) 3 | MLE_KEY_DEF("~", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_TILDE) 4 | MLE_KEY_DEF("2", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_TILDE) 5 | MLE_KEY_DEF("a", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_A) 6 | MLE_KEY_DEF("b", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_B) 7 | MLE_KEY_DEF("c", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_C) 8 | MLE_KEY_DEF("d", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_D) 9 | MLE_KEY_DEF("e", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_E) 10 | MLE_KEY_DEF("f", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_F) 11 | MLE_KEY_DEF("g", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_G) 12 | MLE_KEY_DEF("h", TB_MOD_CTRL, 0, 0, TB_KEY_BACKSPACE) 13 | MLE_KEY_DEF("tab", 0, TB_MOD_CTRL, 0, TB_KEY_TAB) 14 | MLE_KEY_DEF("i", TB_MOD_CTRL, 0, 0, TB_KEY_TAB) 15 | MLE_KEY_DEF("j", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_J) 16 | MLE_KEY_DEF("k", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_K) 17 | MLE_KEY_DEF("l", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_L) 18 | MLE_KEY_DEF("enter", 0, TB_MOD_CTRL, 0, TB_KEY_ENTER) 19 | MLE_KEY_DEF("m", TB_MOD_CTRL, 0, 0, TB_KEY_ENTER) 20 | MLE_KEY_DEF("n", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_N) 21 | MLE_KEY_DEF("o", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_O) 22 | MLE_KEY_DEF("p", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_P) 23 | MLE_KEY_DEF("q", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_Q) 24 | MLE_KEY_DEF("r", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_R) 25 | MLE_KEY_DEF("s", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_S) 26 | MLE_KEY_DEF("t", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_T) 27 | MLE_KEY_DEF("u", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_U) 28 | MLE_KEY_DEF("v", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_V) 29 | MLE_KEY_DEF("w", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_W) 30 | MLE_KEY_DEF("x", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_X) 31 | MLE_KEY_DEF("y", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_Y) 32 | MLE_KEY_DEF("z", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_Z) 33 | MLE_KEY_DEF("4", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_4) 34 | MLE_KEY_DEF("\\", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_4) 35 | MLE_KEY_DEF("5", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_5) 36 | MLE_KEY_DEF("6", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_6) 37 | MLE_KEY_DEF("7", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_7) 38 | MLE_KEY_DEF("/", TB_MOD_CTRL, 0, 0, TB_KEY_CTRL_SLASH) 39 | MLE_KEY_DEF("backspace", 0, TB_MOD_CTRL, 0, TB_KEY_CTRL_8) 40 | MLE_KEY_DEF("escape", 0, 0, 0, TB_KEY_ESC) 41 | MLE_KEY_DEF("space", 0, 0, 32, 0) 42 | MLE_KEY_DEF("comma", 0, 0, 44, 0) 43 | MLE_KEY_DEF("up", 0, 0, 0, TB_KEY_ARROW_UP) 44 | MLE_KEY_DEF("down", 0, 0, 0, TB_KEY_ARROW_DOWN) 45 | MLE_KEY_DEF("left", 0, 0, 0, TB_KEY_ARROW_LEFT) 46 | MLE_KEY_DEF("right", 0, 0, 0, TB_KEY_ARROW_RIGHT) 47 | MLE_KEY_DEF("insert", 0, 0, 0, TB_KEY_INSERT) 48 | MLE_KEY_DEF("delete", 0, 0, 0, TB_KEY_DELETE) 49 | MLE_KEY_DEF("home", 0, 0, 0, TB_KEY_HOME) 50 | MLE_KEY_DEF("end", 0, 0, 0, TB_KEY_END) 51 | MLE_KEY_DEF("pgup", 0, 0, 0, TB_KEY_PGUP) 52 | MLE_KEY_DEF("pgdn", 0, 0, 0, TB_KEY_PGDN) 53 | MLE_KEY_DEF("backtab", 0, 0, 0, TB_KEY_BACK_TAB) 54 | MLE_KEY_DEF("f1", 0, 0, 0, TB_KEY_F1) 55 | MLE_KEY_DEF("f2", 0, 0, 0, TB_KEY_F2) 56 | MLE_KEY_DEF("f3", 0, 0, 0, TB_KEY_F3) 57 | MLE_KEY_DEF("f4", 0, 0, 0, TB_KEY_F4) 58 | MLE_KEY_DEF("f5", 0, 0, 0, TB_KEY_F5) 59 | MLE_KEY_DEF("f6", 0, 0, 0, TB_KEY_F6) 60 | MLE_KEY_DEF("f7", 0, 0, 0, TB_KEY_F7) 61 | MLE_KEY_DEF("f8", 0, 0, 0, TB_KEY_F8) 62 | MLE_KEY_DEF("f9", 0, 0, 0, TB_KEY_F9) 63 | MLE_KEY_DEF("f10", 0, 0, 0, TB_KEY_F10) 64 | MLE_KEY_DEF("f11", 0, 0, 0, TB_KEY_F11) 65 | MLE_KEY_DEF("f12", 0, 0, 0, TB_KEY_F12) 66 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "termbox2.h" 7 | #include "mlbuf.h" 8 | #include "mle.h" 9 | 10 | editor_t _editor; 11 | 12 | int main(int argc, char **argv) { 13 | memset(&_editor, 0, sizeof(editor_t)); 14 | setlocale(LC_ALL, ""); 15 | if (editor_init(&_editor, argc, argv) == MLE_OK) { 16 | editor_run(&_editor); 17 | } 18 | editor_deinit(&_editor); 19 | return _editor.exit_code; 20 | } 21 | -------------------------------------------------------------------------------- /mlbuf.h: -------------------------------------------------------------------------------- 1 | #ifndef __MLBUF_H 2 | #define __MLBUF_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // Typedefs 13 | typedef struct buffer_s buffer_t; // A buffer of text (stored as a linked list of blines) 14 | typedef struct bline_s bline_t; // A line in a buffer 15 | typedef struct bline_char_s bline_char_t; // Metadata about a character in a bline 16 | typedef struct baction_s baction_t; // An insert or delete action (used for undo) 17 | typedef struct mark_s mark_t; // A mark in a buffer 18 | typedef struct srule_s srule_t; // A style rule 19 | typedef struct srule_node_s srule_node_t; // A node in a list of style rules 20 | typedef struct sblock_s sblock_t; // A style of a particular character 21 | typedef struct smemo_s smemo_t; // A memoization of pcre2_match 22 | typedef struct str_s str_t; // A dynamically resizeable string 23 | typedef void (*buffer_callback_t)(buffer_t *buffer, baction_t *action, void *udata); 24 | typedef intmax_t bint_t; 25 | 26 | // str_t 27 | struct str_s { 28 | char *data; 29 | size_t len; 30 | size_t cap; 31 | ssize_t inc; 32 | }; 33 | 34 | // buffer_t 35 | struct buffer_s { 36 | bline_t *first_line; 37 | bline_t *last_line; 38 | bint_t byte_count; 39 | bint_t line_count; 40 | srule_node_t *srules; 41 | srule_node_t *range_srules; 42 | baction_t *actions; 43 | baction_t *action_tail; 44 | baction_t *action_undone; 45 | str_t registers[26]; 46 | mark_t *lettered_marks[26]; 47 | char *path; 48 | struct stat st; 49 | int is_unsaved; 50 | char *data; 51 | bint_t data_len; 52 | int is_data_dirty; 53 | int ref_count; 54 | int tab_width; 55 | buffer_callback_t callback; 56 | void *callback_udata; 57 | int mmap_fd; 58 | char *mmap; 59 | size_t mmap_len; 60 | bline_char_t *slabbed_chars; 61 | bline_t *slabbed_blines; 62 | int *action_group; 63 | int num_applied_srules; 64 | int is_in_open; 65 | int is_in_callback; 66 | int is_style_disabled; 67 | int is_in_undo; 68 | int last_errno; 69 | }; 70 | 71 | // bline_t 72 | struct bline_s { 73 | buffer_t *buffer; 74 | char *data; 75 | bint_t data_len; 76 | bint_t data_cap; 77 | bint_t line_index; 78 | bint_t char_count; 79 | bint_t char_vwidth; 80 | bline_char_t *chars; 81 | bint_t chars_cap; 82 | mark_t *marks; 83 | srule_t *eol_rule; 84 | int is_chars_dirty; 85 | int is_slabbed; 86 | int is_data_slabbed; 87 | bline_t *next; 88 | bline_t *prev; 89 | }; 90 | 91 | // sblock_t 92 | struct sblock_s { 93 | uint16_t fg; 94 | uint16_t bg; 95 | }; 96 | 97 | // bline_char_t 98 | struct bline_char_s { 99 | uint32_t ch; 100 | int len; 101 | bint_t index; 102 | bint_t vcol; 103 | bint_t index_to_vcol; // accessed via >chars[index], not >chars[char] 104 | sblock_t style; 105 | }; 106 | 107 | // baction_t 108 | struct baction_s { 109 | int type; // MLBUF_BACTION_TYPE_* 110 | buffer_t *buffer; 111 | bline_t *start_line; 112 | bint_t start_line_index; 113 | bint_t start_col; 114 | bline_t *maybe_end_line; 115 | bint_t maybe_end_line_index; 116 | bint_t maybe_end_col; 117 | bint_t byte_delta; 118 | bint_t char_delta; 119 | bint_t line_delta; 120 | int action_group; 121 | char *data; 122 | bint_t data_len; 123 | baction_t *next; 124 | baction_t *prev; 125 | }; 126 | 127 | // mark_t 128 | struct mark_s { 129 | bline_t *bline; 130 | bint_t col; 131 | bint_t target_col; 132 | srule_t *range_srule; 133 | char letter; 134 | mark_t *next; 135 | mark_t *prev; 136 | int lefty; 137 | }; 138 | 139 | // smemo_t 140 | struct smemo_s { 141 | int looked; 142 | bint_t look_offset; 143 | int found; 144 | bint_t start; 145 | bint_t stop; 146 | }; 147 | 148 | // srule_t 149 | struct srule_s { 150 | int type; // MLBUF_SRULE_TYPE_* 151 | char *re; 152 | char *re_end; 153 | pcre2_code *cre; 154 | pcre2_code *cre_end; 155 | mark_t *range_a; 156 | mark_t *range_b; 157 | sblock_t style; 158 | smemo_t memo; 159 | smemo_t memo_end; 160 | }; 161 | 162 | // srule_node_t 163 | struct srule_node_s { 164 | srule_t *srule; 165 | srule_node_t *next; 166 | srule_node_t *prev; 167 | }; 168 | 169 | // buffer functions 170 | buffer_t *buffer_new(void); 171 | buffer_t *buffer_new_open(char *path, int *optret_errno); 172 | mark_t *buffer_add_mark(buffer_t *self, bline_t *maybe_line, bint_t maybe_col); 173 | mark_t *buffer_add_mark_ex(buffer_t *self, char letter, bline_t *maybe_line, bint_t maybe_col); 174 | int buffer_get_lettered_mark(buffer_t *self, char letter, mark_t **ret_mark); 175 | int buffer_destroy_mark(buffer_t *self, mark_t *mark); 176 | int buffer_open(buffer_t *self, char *path); 177 | int buffer_save(buffer_t *self); 178 | int buffer_save_as(buffer_t *self, char *path, bint_t *optret_nbytes); 179 | int buffer_write_to_file(buffer_t *self, FILE *fp, size_t *optret_nbytes); 180 | int buffer_write_to_fd(buffer_t *self, int fd, size_t *optret_nbytes); 181 | int buffer_get(buffer_t *self, char **ret_data, bint_t *ret_data_len); 182 | int buffer_clear(buffer_t *self); 183 | int buffer_set(buffer_t *self, char *data, bint_t data_len); 184 | int buffer_set_mmapped(buffer_t *self, char *data, bint_t data_len); 185 | int buffer_substr(buffer_t *self, bline_t *start_line, bint_t start_col, bline_t *end_line, bint_t end_col, char **ret_data, bint_t *ret_data_len, bint_t *ret_nchars); 186 | int buffer_insert(buffer_t *self, bint_t offset, char *data, bint_t data_len, bint_t *optret_num_chars); 187 | int buffer_delete(buffer_t *self, bint_t offset, bint_t num_chars); 188 | int buffer_replace(buffer_t *self, bint_t offset, bint_t num_chars, char *data, bint_t data_len); 189 | int buffer_insert_w_bline(buffer_t *self, bline_t *start_line, bint_t start_col, char *data, bint_t data_len, bint_t *optret_num_chars); 190 | int buffer_delete_w_bline(buffer_t *self, bline_t *start_line, bint_t start_col, bint_t num_chars); 191 | int buffer_replace_w_bline(buffer_t *self, bline_t *start_line, bint_t start_col, bint_t num_chars, char *data, bint_t data_len); 192 | int buffer_get_bline(buffer_t *self, bint_t line_index, bline_t **ret_bline); 193 | int buffer_get_bline_w_hint(buffer_t *self, bint_t line_index, bline_t *opt_hint, bline_t **ret_bline); 194 | int buffer_get_bline_col(buffer_t *self, bint_t offset, bline_t **ret_bline, bint_t *ret_col); 195 | int buffer_get_offset(buffer_t *self, bline_t *bline, bint_t col, bint_t *ret_offset); 196 | int buffer_undo(buffer_t *self); 197 | int buffer_redo(buffer_t *self); 198 | int buffer_undo_action_group(buffer_t *self); 199 | int buffer_redo_action_group(buffer_t *self); 200 | int buffer_add_srule(buffer_t *self, srule_t *srule); 201 | int buffer_remove_srule(buffer_t *self, srule_t *srule); 202 | int buffer_set_callback(buffer_t *self, buffer_callback_t fn_cb, void *udata); 203 | int buffer_set_action_group_ptr(buffer_t *self, int *action_group); 204 | int buffer_set_tab_width(buffer_t *self, int tab_width); 205 | int buffer_set_styles_enabled(buffer_t *self, int is_enabled); 206 | int buffer_apply_styles(buffer_t *self, bline_t *start_line, bint_t line_delta); 207 | int buffer_register_set(buffer_t *self, char reg, char *data, size_t data_len); 208 | int buffer_register_append(buffer_t *self, char reg, char *data, size_t data_len); 209 | int buffer_register_prepend(buffer_t *self, char reg, char *data, size_t data_len); 210 | int buffer_register_clear(buffer_t *self, char reg); 211 | int buffer_register_get(buffer_t *self, char reg, int dup, char **ret_data, size_t *ret_data_len); 212 | int buffer_destroy(buffer_t *self); 213 | 214 | // bline functions 215 | int bline_insert(bline_t *self, bint_t col, char *data, bint_t data_len, bint_t *ret_num_chars); 216 | int bline_delete(bline_t *self, bint_t col, bint_t num_chars); 217 | int bline_replace(bline_t *self, bint_t col, bint_t num_chars, char *data, bint_t data_len); 218 | int bline_get_col(bline_t *self, bint_t index, bint_t *ret_col); 219 | int bline_get_col_from_vcol(bline_t *self, bint_t vcol, bint_t *ret_col); 220 | int bline_index_to_col(bline_t *bline, bint_t index, bint_t *ret_col); 221 | int bline_count_chars(bline_t *bline); 222 | 223 | // mark functions 224 | int mark_block_delete_between(mark_t *self, mark_t *other); 225 | int mark_block_get_between(mark_t *self, mark_t *other, char **ret_str, bint_t *ret_str_len); 226 | int mark_block_get_top_left(mark_t *self, mark_t *other, bline_t **ret_bline, bint_t *ret_col); 227 | int mark_block_insert_before(mark_t *self, char *data, bint_t data_len); 228 | int mark_block_is_between(mark_t *self, mark_t *ma, mark_t *mb); 229 | int mark_clone(mark_t *self, mark_t **ret_mark); 230 | int mark_clone_w_letter(mark_t *self, char letter, mark_t **ret_mark); 231 | int mark_cmp(mark_t *a, mark_t *b, mark_t **optret_first, mark_t **optret_second); 232 | int mark_delete_after(mark_t *self, bint_t num_chars); 233 | int mark_delete_before(mark_t *self, bint_t num_chars); 234 | int mark_delete_between(mark_t *self, mark_t *other); 235 | int mark_destroy(mark_t *self); 236 | int mark_find_bracket_pair(mark_t *self, bint_t max_chars, bline_t **ret_line, bint_t *ret_col, bint_t *ret_brkt); 237 | int mark_find_bracket_top(mark_t *self, bint_t max_chars, bline_t **ret_line, bint_t *ret_col, bint_t *ret_brkt); 238 | int mark_find_next_cre(mark_t *self, pcre2_code *cre, bline_t **ret_line, bint_t *ret_col, bint_t *ret_num_chars); 239 | int mark_find_next_re(mark_t *self, char *re, bint_t re_len, bline_t **ret_line, bint_t *ret_col, bint_t *ret_num_chars); 240 | int mark_find_next_str(mark_t *self, char *str, bint_t str_len, bline_t **ret_line, bint_t *ret_col, bint_t *ret_num_chars); 241 | int mark_find_prev_cre(mark_t *self, pcre2_code *cre, bline_t **ret_line, bint_t *ret_col, bint_t *ret_num_chars); 242 | int mark_find_prev_re(mark_t *self, char *re, bint_t re_len, bline_t **ret_line, bint_t *ret_col, bint_t *ret_num_chars); 243 | int mark_find_prev_str(mark_t *self, char *str, bint_t str_len, bline_t **ret_line, bint_t *ret_col, bint_t *ret_num_chars); 244 | int mark_get_between(mark_t *self, mark_t *other, char **ret_str, bint_t *ret_str_len); 245 | int mark_get_char_after(mark_t *self, uint32_t *ret_char); 246 | int mark_get_char_before(mark_t *self, uint32_t *ret_char); 247 | int mark_get_nchars_between(mark_t *self, mark_t *other, bint_t *ret_nchars); 248 | int mark_get_offset(mark_t *self, bint_t *ret_offset); 249 | int mark_insert_after(mark_t *self, char *data, bint_t data_len); 250 | int mark_insert_before(mark_t *self, char *data, bint_t data_len); 251 | int mark_is_after_col_minus_lefties(mark_t *self, bint_t col); 252 | int mark_is_at_bol(mark_t *self); 253 | int mark_is_at_eol(mark_t *self); 254 | int mark_is_at_word_bound(mark_t *self, int side); 255 | int mark_is_eq(mark_t *self, mark_t *other); 256 | int mark_is_gte(mark_t *self, mark_t *other); 257 | int mark_is_gt(mark_t *self, mark_t *other); 258 | int mark_is_lte(mark_t *self, mark_t *other); 259 | int mark_is_lt(mark_t *self, mark_t *other); 260 | int mark_is_between(mark_t *self, mark_t *ma, mark_t *mb); 261 | int mark_join(mark_t *self, mark_t *other); 262 | int mark_move_beginning(mark_t *self); 263 | int mark_move_bol(mark_t *self); 264 | int mark_move_bracket_pair_ex(mark_t *self, bint_t max_chars, bline_t **optret_line, bint_t *optret_col, bint_t *optret_num_chars); 265 | int mark_move_bracket_pair(mark_t *self, bint_t max_chars); 266 | int mark_move_bracket_top_ex(mark_t *self, bint_t max_chars, bline_t **optret_line, bint_t *optret_col, bint_t *optret_num_chars); 267 | int mark_move_bracket_top(mark_t *self, bint_t max_chars); 268 | int mark_move_by(mark_t *self, bint_t char_delta); 269 | int mark_move_col(mark_t *self, bint_t col); 270 | int mark_move_end(mark_t *self); 271 | int mark_move_eol(mark_t *self); 272 | int mark_move_next_cre_ex(mark_t *self, pcre2_code *cre, bline_t **optret_line, bint_t *optret_col, bint_t *optret_num_chars); 273 | int mark_move_next_cre(mark_t *self, pcre2_code *cre); 274 | int mark_move_next_cre_nudge(mark_t *self, pcre2_code *cre); 275 | int mark_move_next_re_ex(mark_t *self, char *re, bint_t re_len, bline_t **optret_line, bint_t *optret_col, bint_t *optret_num_chars); 276 | int mark_move_next_re(mark_t *self, char *re, bint_t re_len); 277 | int mark_move_next_re_nudge(mark_t *self, char *re, bint_t re_len); 278 | int mark_move_next_str_ex(mark_t *self, char *str, bint_t str_len, bline_t **optret_line, bint_t *optret_col, bint_t *optret_num_chars); 279 | int mark_move_next_str(mark_t *self, char *str, bint_t str_len); 280 | int mark_move_next_str_nudge(mark_t *self, char *str, bint_t str_len); 281 | int mark_move_offset(mark_t *self, bint_t offset); 282 | int mark_move_prev_cre_ex(mark_t *self, pcre2_code *cre, bline_t **optret_line, bint_t *optret_col, bint_t *optret_num_chars); 283 | int mark_move_prev_cre(mark_t *self, pcre2_code *cre); 284 | int mark_move_prev_re_ex(mark_t *self, char *re, bint_t re_len, bline_t **optret_line, bint_t *optret_col, bint_t *optret_num_chars); 285 | int mark_move_prev_re(mark_t *self, char *re, bint_t re_len); 286 | int mark_move_prev_str_ex(mark_t *self, char *str, bint_t str_len, bline_t **optret_line, bint_t *optret_col, bint_t *optret_num_chars); 287 | int mark_move_prev_str(mark_t *self, char *str, bint_t str_len); 288 | int mark_move_to(mark_t *self, bint_t line_index, bint_t col); 289 | int mark_move_to_w_bline(mark_t *self, bline_t *bline, bint_t col); 290 | int mark_move_vert(mark_t *self, bint_t line_delta); 291 | int mark_replace_between(mark_t *self, mark_t *other, char *data, bint_t data_len); 292 | int mark_replace(mark_t *self, bint_t num_chars, char *data, bint_t data_len); 293 | int mark_set_pcre_capture(int *rc, PCRE2_SIZE *ovector, int ovector_size); 294 | int mark_swap(mark_t *self, mark_t *other); 295 | 296 | // srule functions 297 | srule_t *srule_new_single(char *re, bint_t re_len, int caseless, uint16_t fg, uint16_t bg); 298 | srule_t *srule_new_multi(char *re, bint_t re_len, char *re_end, bint_t re_end_len, uint16_t fg, uint16_t bg); 299 | srule_t *srule_new_range(mark_t *range_a, mark_t *range_b, uint16_t fg, uint16_t bg); 300 | int srule_destroy(srule_t *srule); 301 | 302 | // util functions 303 | void *recalloc(void *ptr, size_t orig_num, size_t new_num, size_t el_size); 304 | void _mark_mark_move_inner(mark_t *mark, bline_t *bline_target, bint_t col, int do_set_target); 305 | void str_append_stop(str_t *str, char *data, char *data_stop); 306 | void str_append(str_t *str, char *data); 307 | void str_append_char(str_t *str, char c); 308 | void str_append_len(str_t *str, char *data, size_t data_len); 309 | void str_prepend_stop(str_t *str, char *data, char *data_stop); 310 | void str_prepend(str_t *str, char *data); 311 | void str_prepend_len(str_t *str, char *data, size_t data_len); 312 | void str_set(str_t *str, char *data); 313 | void str_set_len(str_t *str, char *data, size_t data_len); 314 | void str_put_len(str_t *str, char *data, size_t data_len, int is_prepend); 315 | void str_ensure_cap(str_t *str, size_t cap); 316 | void str_clear(str_t *str); 317 | void str_free(str_t *str); 318 | void str_sprintf(str_t *str, const char *fmt, ...); 319 | void str_append_replace_with_backrefs(str_t *str, char *subj, char *repl, int pcre_rc, PCRE2_SIZE *pcre_ovector, int pcre_ovecsize); 320 | size_t utf8_str_length(char *data, size_t len); 321 | int utf8_char_to_unicode(uint32_t *out, const char *c, const char *stop); 322 | 323 | // Globals 324 | extern pcre2_match_data *pcre2_md; 325 | 326 | // Macros 327 | #define MLBUF_DEBUG 1 328 | 329 | // #define MLBUF_LARGE_FILE_SIZE 10485760 330 | #define MLBUF_LARGE_FILE_SIZE 0 331 | 332 | #define MLBUF_OK 0 333 | #define MLBUF_ERR 1 334 | 335 | #define MLBUF_BACTION_TYPE_INSERT 0 336 | #define MLBUF_BACTION_TYPE_DELETE 1 337 | 338 | #define MLBUF_SRULE_TYPE_SINGLE 0 339 | #define MLBUF_SRULE_TYPE_MULTI 1 340 | #define MLBUF_SRULE_TYPE_RANGE 2 341 | 342 | #define MLBUF_MIN(a,b) (((a)<(b)) ? (a) : (b)) 343 | #define MLBUF_MAX(a,b) (((a)>(b)) ? (a) : (b)) 344 | 345 | #define MLBUF_BLINE_DATA_STOP(bline) ((bline)->data + ((bline)->data_len)) 346 | 347 | #define MLBUF_DEBUG_PRINTF(fmt, ...) do { \ 348 | if (MLBUF_DEBUG) { \ 349 | fprintf(stderr, "%lu ", time(0)); \ 350 | fprintf(stderr, (fmt), __VA_ARGS__); \ 351 | fflush(stderr); \ 352 | } \ 353 | } while (0) 354 | 355 | // TODO replace is_chars_dirty with some sort of version check 356 | // (e.g., b->v != b->buffer->v) so that we can invalidate by 357 | // all blines by incrementing a single scalar (b->buffer->v) 358 | // in buffer_set_tab_width 359 | // TODO replace with inline function 360 | #define MLBUF_BLINE_ENSURE_CHARS(b) do { \ 361 | if ((b)->is_chars_dirty) { \ 362 | bline_count_chars(b); \ 363 | } \ 364 | } while (0) 365 | 366 | #define MLBUF_MAKE_GT_EQ0(v) if ((v) < 0) v = 0 367 | 368 | #define MLBUF_ENSURE_AZ(c) \ 369 | if ((c) < 'a' || (c) > 'z') return MLBUF_ERR 370 | 371 | #define MLBUF_REG_PTR(buf, lett) \ 372 | &((buf)->registers[(lett) - 'a']) 373 | 374 | #define MLBUF_LETT_MARK(buf, lett) \ 375 | (buf)->lettered_marks[(lett) - 'a'] 376 | 377 | #endif 378 | -------------------------------------------------------------------------------- /mle.1: -------------------------------------------------------------------------------- 1 | .Dd March 1, 2023 2 | .Dt MLE 1 3 | .Os 4 | .Sh NAME 5 | .Nm mle 6 | .Nd flexible terminal-based text editor 7 | .Sh SYNOPSIS 8 | .Nm mle 9 | .Op Fl abceHhiKklMmNnpSstuvwxyz 10 | .Op Ar file[:line] 11 | .Li ... 12 | .Sh DESCRIPTION 13 | .Nm 14 | is a small, flexible, terminal-based text editor written in C. 15 | It runs on Linux, Windows (Cygwin or WSL), FreeBSD, macOS, and more. 16 | .Ss Basic usage 17 | .Bd -literal 18 | $ mle # Open blank buffer 19 | $ mle one.c # Edit one.c 20 | $ mle one.c:100 # Edit one.c at line 100 21 | $ mle one.c two.c # Edit one.c and two.c 22 | $ mle -h # Show command line help 23 | .Ed 24 | .Pp 25 | The default key bindings are intuitive. 26 | Input text as normal, use directional keys to move around, use 27 | `Ctrl-S` to save, `Ctrl-O` to open, `Ctrl-X` to exit. 28 | .Pp 29 | Press `F2` for full help. 30 | .Ss Options 31 | .Bl -tag -width ".Fl foo barbaz" -offset indent 32 | .It Fl h 33 | Show help 34 | .It Fl a Aq 1|0 35 | Enable/disable tab to space (default: 1) 36 | .It Fl b Aq 1|0 37 | Enable/disable highlight bracket pairs (default: 1) 38 | .It Fl c Ar column 39 | Color column (default: -1, disabled) 40 | .It Fl e Aq 1|0 41 | Enable/disable mouse support (default: 0) 42 | .It Fl H Aq 1|0 43 | Enable/disable headless mode (default: 1 if no tty, else 0) 44 | .It Fl i Aq 1|0 45 | Enable/disable auto indent (default: 0) 46 | .It Fl K Ar kdef 47 | Make a kmap definition (use with -k). 48 | .Pp 49 | .Ar kdef 50 | is formatted as 51 | .Li `,,` , 52 | where 53 | .Ar name 54 | is the name of the kmap, 55 | .Ar default_cmd 56 | is the default command handler (can be empty), and 57 | .Ar allow_fallthru 58 | is a 0 or 1 specifying whether unhandled key input should 59 | be forwarded to the previous kmap on the stack or not. 60 | .It Fl k Ar kbind 61 | Add key binding to current kmap definition (use after -K). 62 | .Pp 63 | .Ar kbind 64 | is formatted as 65 | .Li `,,` , 66 | where 67 | .Ar cmd 68 | is a command name, 69 | .Ar key 70 | is a key name, and 71 | .Ar param 72 | is a static parameter passed to the command (can be empty). 73 | .It Fl l Ar ltype 74 | Set linenum type (default: 0, absolute). 75 | .Pp 76 | .Ar ltype 77 | can be 0 (absolute), 1 (relative), or 2 (both) 78 | .It Fl M Ar macro 79 | Add a macro. 80 | .Pp 81 | .Ar macro 82 | is formatted as 83 | .Li ` ... ` , 84 | where 85 | .Ar name 86 | is the name of the macro, and 87 | .Ar keyN 88 | are space-separated key names. 89 | .It Fl m Ar key 90 | Set macro toggle key (default: M-r). 91 | .Ar key 92 | is a key name. 93 | .It Fl N 94 | Skip reading of rc file 95 | .It Fl n Ar kmap 96 | Set init kmap (default: mle_normal). 97 | .Ar kmap 98 | is a kmap name. 99 | .It Fl p Ar macro 100 | Set startup macro. 101 | .Ar macro 102 | is a macro name. 103 | .It Fl S Ar syndef 104 | Make a syntax definition (use with -s). 105 | .Pp 106 | .Ar syndef 107 | is formatted as 108 | .Li `,,,` , 109 | where 110 | .Ar name 111 | is a syntax name, 112 | .Ar path_pattern 113 | is a path matching regex (PCRE), 114 | .Ar tab_width 115 | is the default tab width, 116 | .Ar tab_to_space 117 | is a 0 or 1 specifying whether to convert tabs to spaces or not. 118 | .It Fl s Ar synrule 119 | Add syntax rule to current syntax definition (use after -S). 120 | .Pp 121 | .Ar synrule 122 | is formatted as 123 | .Li `,,,` , 124 | where 125 | .Ar start 126 | and 127 | .Ar end 128 | are text matching regexes (PCRE), and 129 | .Ar fg 130 | and 131 | .Ar bg 132 | are attributes to apply to matching text. 133 | .Pp 134 | If both 135 | .Ar start 136 | and 137 | .Ar end 138 | are supplied, the rule applies to all text matched in 139 | between the regexes, potentially spanning multiple lines. 140 | If only 141 | .Ar start 142 | is specified, the rule applies to text matched by the regex 143 | on a single line. 144 | .Pp 145 | Attributes for 146 | .Ar fg 147 | and 148 | .Ar bg 149 | are as follows: 150 | .Bl -tag -width "####" -offset indent 151 | .It 0 152 | default 153 | .It 1 154 | black 155 | .It 2 156 | red 157 | .It 4 158 | yellow 159 | .It 5 160 | blue 161 | .It 6 162 | magenta 163 | .It 7 164 | cyan 165 | .It 8 166 | white 167 | .It 256 168 | bold 169 | .It 512 170 | underline 171 | .It 1024 172 | reverse 173 | .It 2048 174 | italic 175 | .El 176 | .It Fl t Ar size 177 | Set tab size (default: 4) 178 | .It Fl u Aq 1|0 179 | Enable/disable coarse undo/redo (default: 0) 180 | .It Fl v 181 | Print version and exit 182 | .It Fl w Aq 1|0 183 | Enable/disable soft word wrap (default: 0) 184 | .It Fl x Ar uscript 185 | Run a Lua user script (experimental) 186 | .It Fl y Ar syntax 187 | Set override syntax for files opened at start up. 188 | If '-' is specified, use the built-in generic syntax. 189 | .Ar syntax 190 | is any syntax name. 191 | .It Fl z Aq 1|0 192 | Enable/disable trimmed paste (default: 1) 193 | .El 194 | .Sh KEY NAMES 195 | Key names for -k, -M, and -m are formatted as `` or `-`. 196 | .Pp 197 | .Ar key 198 | is any character or one of the following: 199 | space, tab, enter, backspace, comma, up, down, left, right, insert, delete, 200 | home, end, pgup, pgdn, backtab, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, 201 | f12. 202 | .Pp 203 | .Ar mod 204 | is one of 205 | .Bl -tag -width "############" -offset indent 206 | .It S 207 | Shift 208 | .It M 209 | Alt (Meta) 210 | .It MS 211 | Alt-Shift 212 | .It C 213 | Ctrl 214 | .It CS 215 | Ctrl-Shift 216 | .It CM 217 | Ctrl-Alt 218 | .It CMS 219 | Ctrl-Alt-Shift 220 | .El 221 | .Pp 222 | Not all key names are valid or supported by all terminals. 223 | Run with `-Qk` to display key names for given input. 224 | .Sh ADVANCED USAGE 225 | Below are some advanced things you can do with mle. 226 | .Ss rc file 227 | To customize the editor, make an rc file named 228 | .Pa ~/.mlerc 229 | or 230 | .Pa /etc/mlerc . 231 | The contents of the rc file are any number of cli options separated 232 | by newlines. 233 | Lines that begin with a semi-colon are interpreted as comments. 234 | .Pp 235 | If the rc file is executable, mle executes it and interprets the 236 | resulting stdout as described above. 237 | For example, consider the following snippet from an executable 238 | .Ar ~/.mlerc 239 | .Xr bash 1 240 | script: 241 | .Bd -literal 242 | ... 243 | # Define 'test' kmap 244 | echo '-Ktest,,1' 245 | 246 | # M-q: replace grep with git grep if `.git` exists 247 | if [ -d ".git" ]; then 248 | echo '-kcmd_grep,M-q,git grep --color=never -P -i -I -n %s 2>/dev/null' 249 | fi 250 | 251 | # Set default kmap 252 | echo '-n test' 253 | ... 254 | .Ed 255 | .Pp 256 | This overrides the built-in grep command with `git grep` if 257 | .Pa .git 258 | exists in the current working directory. 259 | .Ss Shell command integration 260 | The following programs will enable or enhance certain features of 261 | mle if they exist in 262 | .Em PATH . 263 | .Bl -tag -width "############" -offset indent 264 | .It Xr bash 1 265 | file tab completion 266 | .It Xr fzf 1 267 | fuzzy file search 268 | .It Xr grep 1 269 | file grep 270 | .It Xr less 1 271 | less integration 272 | .It Xr perl 1 273 | perl 1-liners 274 | .It Xr readtags 1 275 | ctags integration 276 | .It Xr tree 1 277 | file browsing 278 | .El 279 | .Pp 280 | Arbitrary shell commands can also be run via `cmd_shell` 281 | (M-e by default). If any text is selected, it is sent to 282 | stdin of the command. 283 | Any resulting stdout is inserted into the text buffer. 284 | .Ss Headless mode 285 | mle provides support for non-interactive editing which may be useful 286 | for using the editor as a regular command line tool. 287 | In headless mode, mle reads stdin into a buffer, applies a startup 288 | macro if specified, and then writes the buffer contents to stdout. 289 | For example: 290 | .Bd -literal 291 | $ echo -n hello | mle -M 'test C-e space w o r l d enter' -p test 292 | hello world 293 | .Ed 294 | .Pp 295 | If stdin is a pipe, mle goes into headless mode automatically. 296 | Headless mode can be explicitly enabled or disabled with the `-H` 297 | option. 298 | .Pp 299 | If stdin is a pipe and headless mode is disabled via -H0, mle reads 300 | stdin into a new buffer and then runs as normal in interactive mode. 301 | .Ss Scripting (experimental) 302 | mle is extensible via the Lua programming language. 303 | Scripts are loaded via the `-x` cli option. 304 | Commands registered by scripts can be mapped to keys as normal via `-k`. 305 | See 306 | .Lk https://github.com/adsr/mle 307 | for more info. 308 | .Sh ACKNOWLEDGEMENTS 309 | mle makes extensive use of the following libraries. 310 | .Bl -tag -width "############" -offset indent 311 | .It Em uthash 312 | for hash maps and linked lists 313 | .It Em termbox2 314 | for TUI 315 | .It Em PCRE2 316 | for syntax highlighting and search 317 | .El 318 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: mle-editor 2 | summary: Flexible terminal-based editor. 3 | description: | 4 | mle is a small, flexible, terminal-based text editor written in C. 5 | Notable features include: full Unicode support, syntax highlighting, 6 | scriptable rc file, macros, search and replace (PCRE), window 7 | splitting, multiple cursors, and integration with various shell 8 | commands. 9 | version: 1.7.2 10 | license: Apache-2.0 11 | base: core22 12 | grade: stable 13 | confinement: strict 14 | 15 | apps: 16 | mle: 17 | command: usr/local/bin/mle 18 | 19 | parts: 20 | mle: 21 | plugin: make 22 | source: https://github.com/adsr/mle.git 23 | source-commit: 064147fc83e5587bf793cb8ccbfe51525c6b5489 24 | source-type: git 25 | make-parameters: ["-j1", "mle_vendor=1", "mle_static=1"] 26 | build-packages: [libtool, automake] 27 | stage-packages: [fzf, tree, less, perl, universal-ctags] 28 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | ./run.sh 3 | 4 | .PHONY: all 5 | -------------------------------------------------------------------------------- /tests/func/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | if [ -n "$skip" ]; then 3 | echo -e " \x1b[33mSKIP\x1b[0m $skip" 4 | unset skip 5 | else 6 | actual=$( 7 | $MLE \ 8 | -N \ 9 | -H1 \ 10 | -Qd \ 11 | -K test_kmap,,1 \ 12 | -k cmd_quit_without_saving,f12, \ 13 | -n test_kmap \ 14 | -M "test_macro $macro f12" \ 15 | -p test_macro \ 16 | "${extra_opts[@]}" \ 17 | 2>&1 >/dev/null 18 | ); 19 | exit_code=$? 20 | 21 | if [ "$exit_code" -ne 0 ]; then 22 | echo -e " \x1b[31mERR \x1b[0m nonzero_exit_code=$exit_code\n\n$actual" 23 | exit 1 24 | fi 25 | 26 | for assert_name in "${!expected[@]}"; do 27 | expected_re="${expected[$assert_name]}" 28 | if grep -Eq "$expected_re" <<<"$actual"; then 29 | echo -e " \x1b[32mOK \x1b[0m $assert_name" 30 | else 31 | echo -e " \x1b[31mERR \x1b[0m $assert_name expected=$expected_re\n\n$actual" 32 | exit 1 33 | fi 34 | done 35 | 36 | for assert_name in "${!not_expected[@]}"; do 37 | not_expected_re="${not_expected[$assert_name]}" 38 | if ! grep -Eq "$not_expected_re" <<<"$actual"; then 39 | echo -e " \x1b[32mOK \x1b[0m $assert_name" 40 | else 41 | echo -e " \x1b[31mERR \x1b[0m $assert_name not_expected=$not_expected_re\n\n$actual" 42 | exit 1 43 | fi 44 | done 45 | 46 | unset expected 47 | unset not_expected 48 | fi 49 | -------------------------------------------------------------------------------- /tests/func/test_blist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | extra_opts=(file42 file43) 4 | macro='C-\ M-\ C-f f i l e 4 3 enter enter M-c' 5 | declare -A expected 6 | expected[bview_count ]='^bview_count=2' 7 | expected[bview_file42 ]='^bview.[[:digit:]]+.buffer.path=file42$' 8 | expected[bview_headless]='^bview.[[:digit:]]+.buffer.path=$' 9 | source "test.sh" 10 | 11 | extra_opts=(apple banana carrot) 12 | macro='M-2 M-3 M-0 M-c M-1 M-c' 13 | declare -A expected 14 | expected[carrot_count ]='^bview_count=2' 15 | expected[carrot_file ]='^bview.[[:digit:]]+.buffer.path=carrot$' 16 | expected[carrot_headless]='^bview.[[:digit:]]+.buffer.path=$' 17 | source "test.sh" 18 | -------------------------------------------------------------------------------- /tests/func/test_block.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='a b c enter d e f enter g h i enter M-\ M-a insert j k down C-k C-u' 4 | declare -A expected 5 | expected[acut_line1]='^ c$' 6 | expected[acut_line2]='^ jk$' 7 | expected[acut_line3]='^ghde$' 8 | source 'test.sh' 9 | 10 | macro='a b c enter d e f enter g h i enter M-\ M-a insert right right down M-k C-u' 11 | declare -A expected 12 | expected[acopy_line1]='^abc$' 13 | expected[acopy_line2]='^deab$' 14 | expected[acopy_line3]='^ghde$' 15 | source 'test.sh' 16 | 17 | macro='space a b enter enter space c d M-a insert M-; a M-k C-e C-u' 18 | declare -A expected 19 | expected[aspace_line1]='^ abab$' 20 | expected[aspace_line2]='^ cdcd$' 21 | source 'test.sh' 22 | 23 | macro='a b c insert M-k C-u' 24 | declare -A expected 25 | expected[copy_line1]='^abc$' 26 | expected[copy_line2]='^$' 27 | expected[copy_count]='\.line_count=2$' 28 | source 'test.sh' 29 | 30 | macro='a b c insert C-k C-u' 31 | declare -A expected 32 | expected[cut_line1]='^ abc$' 33 | expected[cut_line2]='^ $' 34 | expected[cut_count]='\.line_count=2$' 35 | source 'test.sh' 36 | 37 | macro="a enter b enter c M-\ M-a insert M-/ C-/ ' x" 38 | declare -A expected 39 | expected[drop_line1]='^ax$' 40 | expected[drop_line2]='^bx$' 41 | expected[drop_line3]='^cx$' 42 | expected[drop_count]='\.line_count=3$' 43 | source 'test.sh' 44 | 45 | macro="a a enter b b enter c c M-\ M-a insert M-/ C-/ ' M-a C-a C-k C-u" 46 | declare -A expected 47 | expected[adrop_line1]='^ aa$' 48 | expected[adrop_line2]='^ bb$' 49 | expected[adrop_line3]='^ cc$' 50 | expected[adrop_line4]='^ $' 51 | expected[adrop_count]='\.line_count=4$' 52 | source 'test.sh' 53 | -------------------------------------------------------------------------------- /tests/func/test_browse.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # skip if tree is not available 4 | if ! command -v tree &>/dev/null; then 5 | skip='tree not in PATH' 6 | source 'test.sh' 7 | exit 0 8 | elif ! tree --help | grep -q charset; then 9 | skip='tree version looks incorrect' 10 | source 'test.sh' 11 | exit 0 12 | fi 13 | 14 | # make tmpdir and delete at exit 15 | this_dir=$(pwd) 16 | tmpdir=$(mktemp -d) 17 | cd $tmpdir 18 | finish() { cd $this_dir; rm -rf $tmpdir; } 19 | trap finish EXIT 20 | 21 | # setup tmpdir 22 | mkdir -p adir/bdir 23 | touch adir/bdir/hi 24 | 25 | # setup tmpdir with space in it (should be escaped) 26 | mkdir -p 'a sp/b sp' 27 | touch 'a sp/b sp/h i' 28 | 29 | # ensure that we can browse into adir, bdir, then select hi 30 | # ensure path is relative to where we started 31 | macro='C-b C-f a d i r enter enter C-f b d i r enter enter C-f h i enter enter' 32 | declare -A expected 33 | expected[hi]='^bview.[[:digit:]]+.buffer.path=adir/bdir/hi$' 34 | source "$this_dir/test.sh" 35 | 36 | # ensure that we can browse into 'a sp', 'b sp', then select 'h i' 37 | # ensure path is relative to where we started 38 | macro='C-b C-f a space s p enter enter C-f b space s p enter enter C-f h space i enter enter' 39 | declare -A expected 40 | expected[hi_space]='^bview.[[:digit:]]+.buffer.path=a sp/b sp/h i$' 41 | source "$this_dir/test.sh" 42 | -------------------------------------------------------------------------------- /tests/func/test_close.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='a M-h M-p M-c C-c b' 4 | declare -A expected 5 | expected[close_root_prompted]='^ab$' 6 | source 'test.sh' 7 | -------------------------------------------------------------------------------- /tests/func/test_coarse_undo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # skip if timeout is not available 4 | if ! command -v timeout &>/dev/null; then 5 | skip='timeout not in PATH' 6 | source 'test.sh' 7 | exit 0 8 | fi 9 | 10 | MLE_ORIG=$MLE 11 | export MLE="timeout -s KILL 2 $MLE_ORIG" 12 | 13 | macro='a C-z' 14 | extra_opts=(-u1) # coarse undo 15 | declare -A expected 16 | expected[no_inf_loop]='.' 17 | source 'test.sh' 18 | 19 | tmpf=$(mktemp) 20 | finish() { rm -f $tmpf; } 21 | trap finish EXIT 22 | cat >$tmpf <<'EOD' 23 | func apple() { 24 | // banana (ensure coarse undo works properly on very first user action) 25 | } // carrot 26 | EOD 27 | byte_count=$(cat $tmpf | wc -c) 28 | macro='C-e enter C-z' 29 | extra_opts=(-u1 -i1 $tmpf) # coarse undo, auto indent 30 | declare -A expected 31 | expected[coarse_undo_1st_1]="apple" 32 | expected[coarse_undo_1st_2]="banana" 33 | expected[coarse_undo_1st_3]="carrot" 34 | expected[coarse_undo_1st_c]="^bview.0.buffer.byte_count=$byte_count\$" 35 | source 'test.sh' 36 | 37 | export MLE="$MLE_ORIG" 38 | -------------------------------------------------------------------------------- /tests/func/test_cursor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='b e g i n space { enter l i n e 1 enter l i n e 2 enter } enter C-f l i n e 1 enter C-2 d' 4 | declare -A expected 5 | expected[sel_bracket_mark_line]='.mark.line_index=0$' 6 | expected[sel_bracket_mark_col]='.mark.col=7$' 7 | expected[sel_bracket_anchor_line]='.anchor.line_index=3$' 8 | expected[sel_bracket_anchor_col]='.anchor.col=0$' 9 | source 'test.sh' 10 | 11 | macro='a b c enter l i n e 2 enter l i n e 3 C-2 q' 12 | declare -A expected 13 | expected[sel_all_mark_line]='.mark.line_index=2$' 14 | expected[sel_all_mark_col]='.mark.col=5$' 15 | expected[sel_all_anchor_line]='.anchor.line_index=0$' 16 | expected[sel_all_anchor_col]='.anchor.col=0$' 17 | source 'test.sh' 18 | -------------------------------------------------------------------------------- /tests/func/test_cut_copy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='f o o C-k' 4 | declare -A expected 5 | expected[cut_data]='^$' 6 | source 'test.sh' 7 | 8 | macro='f o o C-k C-u' 9 | declare -A expected 10 | expected[uncut_data]='^foo$' 11 | source 'test.sh' 12 | 13 | macro='f o o M-k C-u' 14 | declare -A expected 15 | expected[copy_data]='^foofoo$' 16 | source 'test.sh' 17 | 18 | macro='f o o M-a left left C-k' 19 | declare -A expected 20 | expected[sel_cut_data]='^f$' 21 | source 'test.sh' 22 | 23 | macro='f o o M-a left left C-k C-u' 24 | declare -A expected 25 | expected[sel_uncut_data]='^foo$' 26 | source 'test.sh' 27 | 28 | macro='f o o M-a left left M-k C-u' 29 | declare -A expected 30 | expected[sel_copy_data]='^foooo$' 31 | source 'test.sh' 32 | 33 | macro='a b c { x y z } C-f x y z enter C-d d C-e C-u' 34 | declare -A expected 35 | expected[cut_by_bracket_data]='^abc\{\}xyz$' 36 | source 'test.sh' 37 | 38 | macro='a b c space x y z left C-d w C-a C-u' 39 | declare -A expected 40 | expected[cut_by_word_data]='^xyzabc $' 41 | source 'test.sh' 42 | 43 | macro='a b c space x y z C-d s C-a C-u' 44 | declare -A expected 45 | expected[cut_by_word_back_data]='^xyzabc $' 46 | source 'test.sh' 47 | 48 | macro='a b c space x y z C-a C-d f C-e C-u' 49 | declare -A expected 50 | expected[cut_by_word_forward_data]='^ xyzabc$' 51 | source 'test.sh' 52 | 53 | macro='a b c space x y z C-d a C-u C-u' 54 | declare -A expected 55 | expected[cut_by_bol_data]='^abc xyzabc xyz$' 56 | source 'test.sh' 57 | 58 | macro='a b c space x y z C-a C-d e C-u C-u' 59 | declare -A expected 60 | expected[cut_by_eol_data]='^abc xyzabc xyz$' 61 | source 'test.sh' 62 | 63 | macro='a space " q u o t e d " space s t r i n g C-f u o t e enter C-d c' 64 | declare -A expected 65 | expected[cut_by_str1_data]='^a "" string$' 66 | source 'test.sh' 67 | 68 | macro="\" a ' b \" C-f b enter C-d c" 69 | declare -A expected 70 | expected[cut_by_str2_data]='^""$' 71 | source 'test.sh' 72 | 73 | macro='o n e - a b c M-a left left left C-k C-n t w o - C-u' 74 | declare -A expected 75 | expected[global_cut_buffer_data]='^two-abc$' 76 | source 'test.sh' 77 | 78 | macro='a b c C-2 e' 79 | declare -A expected 80 | expected[mark]='^bview.0.cursor.0.mark.col=3$' 81 | expected[anchor]='^bview.0.cursor.0.anchor.col=3$' 82 | source 'test.sh' 83 | 84 | macro='a b c enter d e f M-\ M-a C-f f enter C-/ ; right C-k' 85 | declare -A expected 86 | expected[swap_anchor]='^af$' 87 | source 'test.sh' 88 | 89 | macro='a C-n b S-left M-k C-u M-p S-left M-k M-n CM-u' 90 | declare -A expected 91 | expected[buf1_data]='^a$' 92 | expected[buf2_data]='^bab$' 93 | source 'test.sh' 94 | -------------------------------------------------------------------------------- /tests/func/test_delete.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='f o o enter b a r backspace backspace backspace backspace' 4 | declare -A expected 5 | expected[bs_line1]='^foo$' 6 | expected[bs_byte_count]='^bview.0.buffer.byte_count=3$' 7 | expected[bs_line_count]='^bview.0.buffer.line_count=1$' 8 | source 'test.sh' 9 | unset expected 10 | 11 | macro='f o o b a r left left left delete delete delete' 12 | declare -A expected 13 | expected[del_line1]='^foo$' 14 | expected[del_byte_count]='^bview.0.buffer.byte_count=3$' 15 | expected[del_line_count]='^bview.0.buffer.line_count=1$' 16 | source 'test.sh' 17 | 18 | macro='f o o space b a r C-w' 19 | declare -A expected 20 | expected[delete_word_back_data]='^foo $' 21 | source 'test.sh' 22 | 23 | macro='f o o space b a r C-a M-d' 24 | declare -A expected 25 | expected[delete_word_forward_data]='^ bar$' 26 | source 'test.sh' 27 | -------------------------------------------------------------------------------- /tests/func/test_indent_outdent.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # cmd_indent 4 | macro='h e l l o M-.' 5 | declare -A expected 6 | expected[indent_data]='^ hello$' 7 | source 'test.sh' 8 | 9 | # cmd_outdent 10 | macro='space space space space h e l l o M-,' 11 | declare -A expected 12 | expected[outdent_data]='^hello$' 13 | source 'test.sh' 14 | 15 | # cmd_indent (sel) 16 | macro='h e l l o enter w o r l d M-\ M-a M-/ M-.' 17 | declare -A expected 18 | expected[sel_indent_data1]='^ hello$' 19 | expected[sel_indent_data2]='^ world$' 20 | source 'test.sh' 21 | 22 | # cmd_outdent (sel) 23 | macro='space space space space h e l l o enter space space space space w o r l d M-\ M-a M-/ M-,' 24 | declare -A expected 25 | expected[sel_outdent_data1]='^hello$' 26 | expected[sel_outdent_data2]='^world$' 27 | source 'test.sh' 28 | 29 | # _cmd_insert_auto_indent_* 30 | macro='M-o a 0 enter f o r space { enter i = 1 enter } enter' 31 | extra_opts=(-i 1) 32 | declare -A expected 33 | expected[auto_indent_1]='^for \{$' 34 | expected[auto_indent_2]='^'$'\t''i=1$' 35 | expected[auto_indent_3]='^}$' 36 | source 'test.sh' 37 | -------------------------------------------------------------------------------- /tests/func/test_insert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='h e l l o enter w o r l d enter' 4 | declare -A expected 5 | expected[a_line1 ]='^hello$' 6 | expected[a_line2 ]='^world$' 7 | expected[a_byte_count]='^bview.0.buffer.byte_count=12$' 8 | expected[a_line_count]='^bview.0.buffer.line_count=3$' 9 | source 'test.sh' 10 | 11 | macro='w o r l d M-i h e l l o' 12 | declare -A expected 13 | expected[b_line1 ]='^hello$' 14 | expected[b_line2 ]='^world$' 15 | expected[b_byte_count]='^bview.0.buffer.byte_count=11$' 16 | expected[b_line_count]='^bview.0.buffer.line_count=2$' 17 | source 'test.sh' 18 | 19 | macro='h e l l o C-a M-u right M-u C-e M-u' 20 | declare -A expected 21 | expected[c_line1 ]='^hello$' 22 | expected[c_lineb ]='^$' 23 | expected[c_line_count]='^bview.0.buffer.line_count=4$' 24 | source 'test.sh' 25 | -------------------------------------------------------------------------------- /tests/func/test_issue_51.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # make tmpf and delete at exit 4 | tmpf=$(mktemp) 5 | finish() { rm -f $tmpf; } 6 | trap finish EXIT 7 | 8 | # write >MLE_BRACKET_PAIR_MAX_SEARCH chars to tmpf 9 | echo '(' >>$tmpf 10 | seq 1 10000 >>$tmpf 11 | echo ')' >>$tmpf 12 | 13 | # ensure cursor.sel_rule closed 14 | macro='M-c right C-2 d' 15 | extra_opts=($tmpf) 16 | declare -A expected 17 | expected[srule_should_be_closed]='^bview.0.cursor.0.sel_rule=n$' 18 | source 'test.sh' 19 | -------------------------------------------------------------------------------- /tests/func/test_kmap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # ensure binding in prev kmap works with fallthru enabled 4 | extra_opts=( 5 | -K 'ka,,1' -k 'cmd_shell,a a,printf A' -k 'cmd_push_kmap,x,kb' -k 'cmd_quit_without_saving,f12,' 6 | -K 'kb,,1' -k 'cmd_shell,a b,printf B' -k 'cmd_pop_kmap,x,' 7 | -n 'ka' 8 | ) 9 | macro='a a a b x a b a a x a z' 10 | declare -A expected 11 | expected[data_a]='^AabBAaz$' 12 | source 'test.sh' 13 | 14 | # ensure input trail resets on non-matching input w/ fallthru disabled 15 | extra_opts=( 16 | -K 'ka,,0' -k 'cmd_shell,a,printf A' -k 'cmd_quit_without_saving,f12,' 17 | -n 'ka' 18 | ) 19 | macro='b a' 20 | declare -A expected 21 | expected[data_b]='^A$' 22 | source 'test.sh' 23 | -------------------------------------------------------------------------------- /tests/func/test_lua.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | on_exit() { [ -n "$lua_script" ] && rm -f $lua_script; } 4 | trap on_exit EXIT 5 | lua_script=$(mktemp 'mle.test_lua.XXXXXXXXXX') 6 | extra_opts=(-x $lua_script -K lua_kmap,,1 -k cmd_lua_test,f11, -k cmd_quit_without_saving,f12, -n lua_kmap) 7 | 8 | # mle.mark_insert_before 9 | macro='f11' 10 | cat >$lua_script <<"EOD" 11 | mle.editor_register_cmd("cmd_lua_test", function (ctx) 12 | mle.mark_insert_before(ctx["mark"], "hello from lua\n", 15) 13 | end) 14 | EOD 15 | declare -A expected 16 | expected[simple_data]='^hello from lua$' 17 | source 'test.sh' 18 | 19 | # mle.editor_open_bview 20 | macro='f11' 21 | cat >$lua_script <<"EOD" 22 | mle.editor_register_cmd("cmd_lua_test", function (ctx) 23 | print "hi1" 24 | mle.editor_open_bview(ctx["editor"], nil, 0, nil, 0, 1, 0, 0, nil) 25 | print "hi2" 26 | end) 27 | EOD 28 | declare -A expected 29 | expected[open_data1 ]='^hi1$' 30 | expected[open_data2 ]='^hi2$' 31 | expected[open_bview_count]='^bview_count=2$' 32 | source 'test.sh' 33 | 34 | # mle.editor_prompt 35 | macro='f11 t e s t enter . . . f11 C-c' 36 | cat >$lua_script <<"EOD" 37 | mle.editor_register_cmd("cmd_lua_test", function (ctx) 38 | rv = mle.editor_prompt(ctx["editor"], "input?") 39 | if rv then 40 | str = "hello " .. rv .. " from lua" 41 | else 42 | str = "you hit ctrl-c" 43 | end 44 | mle.mark_insert_before(ctx["mark"], str, string.len(str)) 45 | end) 46 | EOD 47 | declare -A expected 48 | expected[prompt_data]='^hello test from lua...you hit ctrl-c$' 49 | source 'test.sh' 50 | 51 | # mle.editor_register_observer 52 | macro='f11' 53 | cat >$lua_script <<"EOD" 54 | mle.editor_register_cmd("cmd_lua_test", function (ctx) 55 | print "ell" 56 | end) 57 | mle.editor_register_observer("cmd:cmd_lua_test:before", function (ctx) 58 | print "h" 59 | end) 60 | mle.editor_register_observer("cmd:cmd_lua_test:after", function (ctx) 61 | print "o" 62 | end) 63 | EOD 64 | declare -A expected 65 | expected[observer_data]='^hello$' 66 | source 'test.sh' 67 | 68 | # mle.editor_register_observer 69 | macro='f11 h i enter M-e s e q space 1 space 5 | p a s t e space - s d , space - enter backspace backspace' 70 | cat >$lua_script <<"EOD" 71 | mark = nil 72 | in_callback = false 73 | mle.editor_register_cmd("cmd_lua_test", function (ctx) 74 | mark = ctx["mark"] 75 | bview = mle.editor_open_bview(ctx["editor"], nil, 0, nil, 0, 1, 0, 0, nil) 76 | mle.editor_set_active(ctx["editor"], bview["optret_bview"]) 77 | end) 78 | mle.editor_register_observer("buffer:baction", function (baction) 79 | if in_callback or not mark then return end 80 | in_callback = true 81 | str = "buffer=" .. baction["buffer"] .. " byte_delta=" .. baction["byte_delta"] .. "\n" 82 | mle.mark_insert_before(mark, str, string.len(str)) 83 | in_callback = false 84 | end) 85 | EOD 86 | declare -A expected 87 | expected[observer_data1 ]='^hi$' 88 | expected[observer_data2 ]='^1,2,3,4,$' 89 | expected[observer_output1]='^buffer=[^ ]+ byte_delta=1$' # typing 90 | expected[observer_output2]='^buffer=[^ ]+ byte_delta=10$' # output from `seq 1 5 | paste -sd, -` 91 | expected[observer_output3]='^buffer=[^ ]+ byte_delta=-1$' # backspacing 92 | source 'test.sh' 93 | -------------------------------------------------------------------------------- /tests/func/test_macro.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | extra_opts=(-M 'printable a A S-a M-a M-A MS-a C-a CS-a CM-a CMS-a enter comma tab f1' ) 4 | macro='M-< p r i n t a b l e enter' 5 | declare -A expected 6 | expected[printed_macro]='^a A S-a M-a M-A MS-a C-a CS-a CM-a CMS-a C-enter comma C-tab f1 $' 7 | source 'test.sh' 8 | -------------------------------------------------------------------------------- /tests/func/test_move.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # cmd_move_(left|right|up|down) 4 | macro='h e l l o enter w o r l d left up right down' 5 | declare -A expected 6 | expected[dpad_cursor_line]='^bview.0.cursor.0.mark.line_index=1$' 7 | expected[dpad_cursor_col ]='^bview.0.cursor.0.mark.col=5$' 8 | source 'test.sh' 9 | 10 | # cmd_move_(bol) 11 | macro='h e l l o enter w o r l d C-a' 12 | declare -A expected 13 | expected[bol_cursor_line]='^bview.0.cursor.0.mark.line_index=1$' 14 | expected[bol_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 15 | source 'test.sh' 16 | 17 | # cmd_move_(eol) 18 | macro='h e l l o enter w o r l d M-\ C-e' 19 | declare -A expected 20 | expected[eol_cursor_line]='^bview.0.cursor.0.mark.line_index=0$' 21 | expected[eol_cursor_col ]='^bview.0.cursor.0.mark.col=5$' 22 | source 'test.sh' 23 | 24 | # cmd_move_(beginning|end) 25 | macro='h e l l o enter w o r l d M-\ M-/' 26 | declare -A expected 27 | expected[beg_end_cursor_line]='^bview.0.cursor.0.mark.line_index=1$' 28 | expected[beg_end_cursor_col ]='^bview.0.cursor.0.mark.col=5$' 29 | source 'test.sh' 30 | 31 | # cmd_move_page_(up|down) 32 | macro='1 enter 2 enter 3 enter 4 enter 5 enter 6 enter C-pgup a C-pgdn b' 33 | expected[pgup5]='^a2$' 34 | expected[pgdn5]='^b$' 35 | source 'test.sh' 36 | 37 | # cmd_move_(to_line) 38 | macro='0 enter 1 enter 2 enter 3 M-g 2 enter' 39 | declare -A expected 40 | expected[to_line_cursor_col]='^bview.0.cursor.0.mark.line_index=1$' 41 | source 'test.sh' 42 | 43 | # cmd_move_(to_offset) 44 | macro='a a enter b b enter c c enter M-G 5 enter' 45 | declare -A expected 46 | expected[to_offset_cursor_line]='^bview.0.cursor.0.mark.line_index=1$' 47 | expected[to_offset_cursor_col ]='^bview.0.cursor.0.mark.col=2$' 48 | source 'test.sh' 49 | 50 | # cmd_move_(to_relative) 51 | macro='0 enter 1 enter 2 enter 3 enter M-y 2 u M-y 1 d' 52 | declare -A expected 53 | expected[relative_cursor_col]='^bview.0.cursor.0.mark.line_index=3$' 54 | source 'test.sh' 55 | 56 | # cmd_move_(until_forward|until_back) 57 | macro="b a n a n a M-; b M-' a" 58 | declare -A expected 59 | expected[until_cursor_col]='^bview.0.cursor.0.mark.col=1$' 60 | source 'test.sh' 61 | 62 | # cmd_move_until_forward (nudge) 63 | macro="a a C-a M-' a" 64 | declare -A expected 65 | expected[nudge_cursor_col]='^bview.0.cursor.0.mark.col=1$' 66 | source 'test.sh' 67 | 68 | # cmd_move_(word_forward|word_back) 69 | macro='a p p l e space b a n a n a space c r a n M-b M-b M-f' 70 | declare -A expected 71 | expected[word_cursor_col]='^bview.0.cursor.0.mark.col=12$' 72 | source 'test.sh' 73 | 74 | # cmd_move_(bracket_forward|bracket_back) 75 | macro='t e s t space { space { space { M-left M-left M-left M-right' 76 | declare -A expected 77 | expected[bracket_cursor_col]='^bview.0.cursor.0.mark.col=7$' 78 | source 'test.sh' 79 | 80 | # cmd_move_bracket_toggle 81 | macro='t e s t 1 { enter t e s t 2 [ enter ] enter } M-= a M-= b' 82 | declare -A expected 83 | expected[bracket_toggle_line]='^bview.0.cursor.0.mark.line_index=2$' 84 | expected[bracket_toggle_col ]='^bview.0.cursor.0.mark.col=1$' 85 | expected[bracket_toggle_a ]='^test2a\[$' 86 | expected[bracket_toggle_b ]='^b\]$' 87 | source 'test.sh' 88 | 89 | # cmd_jump 90 | macro='a n t space b a t space c a t space d o g M-j a c' 91 | declare -A expected 92 | expected[jump_cursor_col]='^bview.0.cursor.0.mark.col=8$' 93 | source 'test.sh' 94 | 95 | # cmd_(drop|goto)_lettered_mark 96 | macro='a enter b enter c M-m M-\ a M-m c up b M-z z M-m c M-z z b M-m c' 97 | declare -A expected 98 | expected[lettmark_a]='^aa$' 99 | expected[lettmark_b]='^bbb$' 100 | expected[lettmark_c]='^cccc$' 101 | source 'test.sh' 102 | -------------------------------------------------------------------------------- /tests/func/test_multi_cursor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='1 0 space 2 0 space 3 0 C-r \ d + C-/ y e s C-/ /' 4 | declare -A expected 5 | expected[isearch_data ]='^yes10 yes20 yes30$' 6 | expected[isearch_cursor_count]='^bview.0.cursor_count=1$' 7 | source 'test.sh' 8 | 9 | macro='h i space C-/ . C-a C-/ a X X' 10 | declare -A expected 11 | expected[drop_wake_data ]='^XXhi XX$' 12 | expected[drop_wake_cursor_count]='^bview.0.cursor_count=2$' 13 | source 'test.sh' 14 | 15 | macro="o n e enter t w o enter t h r e e M-a M-\ C-/ ' C-e space a n d" 16 | declare -A expected 17 | expected[column_data1 ]='^one and$' 18 | expected[column_data2 ]='^two and$' 19 | expected[column_data3 ]='^three and$' 20 | expected[column_cursor_count]='^bview.0.cursor_count=3$' 21 | source 'test.sh' 22 | 23 | macro="a enter space b enter space space c enter C-r \ S + C-/ C-/ l" 24 | declare -A expected 25 | expected[align_line_1]='^ a$' 26 | expected[align_line_2]='^ b$' 27 | expected[align_line_3]='^ c$' 28 | source 'test.sh' 29 | 30 | macro="a b enter c d enter e f enter C-r \ S + C-/ M-a C-e C-/ ; x" 31 | declare -A expected 32 | expected[swap_line_1]='^xab$' 33 | expected[swap_line_2]='^xcd$' 34 | expected[swap_line_3]='^xef$' 35 | source 'test.sh' 36 | 37 | macro="a b enter c d enter C-r \ S + C-/ M-a C-e M-." 38 | declare -A expected 39 | expected[indent_line_1]='^ ab$' 40 | expected[indent_line_2]='^ cd$' 41 | source 'test.sh' 42 | 43 | macro="a enter b c enter d e f enter C-r \ S + C-/ M-a C-e M-e w c space - c enter" 44 | declare -A expected 45 | expected[shell_line_1]='^\s*1$' 46 | expected[shell_line_2]='^\s*2$' 47 | expected[shell_line_3]='^\s*3$' 48 | source 'test.sh' 49 | -------------------------------------------------------------------------------- /tests/func/test_open.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='' 4 | extra_opts=(test_file_w_suffix test_file) 5 | declare -A expected 6 | expected[path_1]='^bview.[[:digit:]]+.buffer.path=test_file_w_suffix$' 7 | expected[path_2]='^bview.[[:digit:]]+.buffer.path=test_file$' 8 | source 'test.sh' 9 | -------------------------------------------------------------------------------- /tests/func/test_repeat.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro="b a n a n a home M-' a f5 f5" 4 | declare -A expected 5 | expected[cursor]='^bview.0.cursor.0.mark.col=5$' 6 | source 'test.sh' 7 | -------------------------------------------------------------------------------- /tests/func/test_search_replace.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # cmd_search 4 | macro='h e l l o enter w o r l d C-f h e l l o enter' 5 | declare -A expected 6 | expected[search_cursor_line]='^bview.0.cursor.0.mark.line_index=0$' 7 | expected[search_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 8 | source 'test.sh' 9 | 10 | # cmd_rsearch 11 | macro='h i enter h i enter h i enter CM-f h i enter' 12 | declare -A expected 13 | expected[rsearch_cursor_line]='^bview.0.cursor.0.mark.line_index=2$' 14 | expected[rsearch_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 15 | source 'test.sh' 16 | 17 | # cmd_search_next 18 | macro='h i enter h i enter h i enter C-f h i enter C-g' 19 | declare -A expected 20 | expected[next_cursor_line]='^bview.0.cursor.0.mark.line_index=1$' 21 | expected[next_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 22 | source 'test.sh' 23 | 24 | # cmd_search_prev (wrap) 25 | macro='h i enter h i enter h i enter C-f h i enter CM-g' 26 | declare -A expected 27 | expected[prev_wrap_cursor_line]='^bview.0.cursor.0.mark.line_index=2$' 28 | expected[prev_wrap_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 29 | source 'test.sh' 30 | 31 | # cmd_search_next (wrap) 32 | macro='h e l l o enter h e l l o C-f h e l l o enter C-g C-g' 33 | declare -A expected 34 | expected[next_wrap_cursor_line]='^bview.0.cursor.0.mark.line_index=0$' 35 | expected[next_wrap_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 36 | source 'test.sh' 37 | 38 | # cmd_find_word 39 | macro='h e l l o enter h e l l o left C-v' 40 | declare -A expected 41 | expected[find_cursor_line]='^bview.0.cursor.0.mark.line_index=0$' 42 | expected[find_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 43 | source 'test.sh' 44 | 45 | # cmd_find_word (wrap) 46 | macro='h e l l o enter h e l l o left C-v C-v' 47 | declare -A expected 48 | expected[find_wrap_cursor_line]='^bview.0.cursor.0.mark.line_index=1$' 49 | expected[find_wrap_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 50 | source 'test.sh' 51 | 52 | # cmd_rfind_word 53 | macro='h i enter h i enter h i C-a CM-v' 54 | declare -A expected 55 | expected[rfind_cursor_line]='^bview.0.cursor.0.mark.line_index=1$' 56 | expected[rfind_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 57 | source 'test.sh' 58 | 59 | # cmd_isearch 1 60 | macro='a c t o r enter a p p l e enter a p p e t i t e enter a z u r e M-\ C-r a p p e enter' 61 | declare -A expected 62 | expected[isearch_cursor_line]='^bview.0.cursor.0.mark.line_index=2$' 63 | expected[iesarch_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 64 | source 'test.sh' 65 | 66 | # cmd_isearch 2 67 | macro='i enter a enter b enter a enter b M-\ C-r a enter C-r b enter C-r C-n C-n' 68 | declare -A expected 69 | expected[isearch_cursor_line]='^bview.0.cursor.0.mark.line_index=3$' 70 | expected[iesarch_cursor_col ]='^bview.0.cursor.0.mark.col=0$' 71 | source 'test.sh' 72 | 73 | # cmd_replace 1 74 | macro='a 1 space b 2 space c 3 space d 4 C-t \ d + enter x enter y n a' 75 | declare -A expected 76 | expected[replace1_data]='^ax b2 cx dx$' 77 | source 'test.sh' 78 | 79 | # cmd_replace 2 80 | macro='a 1 space b 2 space c 3 space d 4 C-t \ d + enter x enter y n C-c' 81 | declare -A expected 82 | expected[replace2_data]='^ax b2 c3 d4$' 83 | source 'test.sh' 84 | 85 | # cmd_replace 3 86 | macro='a b c 1 2 3 C-t ( . . . ) ( . . . ) enter A $ 2 $ n B $ 1 $ n $ x 4 3 enter a' 87 | declare -A expected 88 | expected[replace3_data1]='^A123$' 89 | expected[replace3_data2]='^Babc$' 90 | expected[replace3_data3]='^C$' 91 | source 'test.sh' 92 | 93 | # cmd_search history 94 | macro='h e l l o enter h e l l o C-f h e l l o enter C-f up enter' 95 | declare -A expected 96 | expected[history_line]='^bview.0.cursor.0.mark.line_index=1$' 97 | expected[history_col ]='^bview.0.cursor.0.mark.col=0$' 98 | source 'test.sh' 99 | 100 | # cmd_replace_all (all) 101 | macro='1 a C-n 2 b C-n 3 c CM-t ( \ d ) ( \ w ) enter $ 2 $ 1 enter a' 102 | declare -A expected 103 | expected[replace_all_1_1]='^a1$' 104 | expected[replace_all_1_2]='^b2$' 105 | expected[replace_all_1_3]='^c3$' 106 | source 'test.sh' 107 | 108 | # cmd_replace_all (no, yes, cancel) 109 | macro='1 a C-n 2 b C-n 3 c CM-t ( \ d ) ( \ w ) enter $ 2 $ 1 enter n y C-c' 110 | declare -A expected 111 | expected[replace_all_2_1]='^1a$' 112 | expected[replace_all_2_2]='^b2$' 113 | expected[replace_all_2_3]='^3c$' 114 | source 'test.sh' 115 | 116 | # cmd_replace_all (avoid infinite replacement) 117 | macro='a enter C-t ^ enter enter a' 118 | declare -A expected 119 | expected[replace_no_inf_loop]='^a$' 120 | source 'test.sh' 121 | -------------------------------------------------------------------------------- /tests/func/test_shell.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='M-e e c h o space h e l l o 4 2 enter' 4 | declare -A expected 5 | expected[shell_data]='^hello42$' 6 | source 'test.sh' 7 | -------------------------------------------------------------------------------- /tests/func/test_show_help.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | macro='f2' 4 | declare -A expected 5 | expected[help]='mle command help' 6 | source 'test.sh' 7 | -------------------------------------------------------------------------------- /tests/func/test_syntax.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | tmpn=test_syntax_file 3 | extra_opts=(-S syn_test,$tmpn,4,0 -s foo,,258,3 $tmpn) 4 | macro='M-c f o o' 5 | declare -A expected 6 | expected[style_fg]='\bfg=258\b' 7 | expected[style_bg]='\bbg=3\b' 8 | source 'test.sh' 9 | 10 | tmpn=test_syntax_file 11 | extra_opts=(-S syn_test,$tmpn,4,0 -s foo,,1,2 $tmpn) 12 | macro='M-c { enter f o o ; enter } enter M-\ M-a M-/ M-. M-a' 13 | declare -A expected 14 | expected[outdent_style]='' 15 | source 'test.sh' 16 | 17 | on_exit() { [ -n "$tmpf" ] && rm -f $tmpf; } 18 | trap on_exit EXIT 19 | tmpf=$(mktemp 'mle.test_syntax.XXXXXXXXXX') 20 | 21 | cat >$tmpf <<"EOD" 22 | "aaa" 23 | "aaa\\" 24 | "aaa \" aaa" 25 | "aaa" bbb "aaa" 26 | "aaa \" aaa" bbb "aaa \" aaa" 27 | "aaa \" aaa\\" bbb "aaa \" aaa\\" 28 | "aaa" bbb 29 | "aaa\\" bbb 30 | "aaa \" aaa" bbb 31 | "aaa" bbb "aaa" bbb 32 | "aaa \" aaa" bbb "aaa \" aaa" bbb 33 | "aaa \" aaa\\" bbb "aaa \" aaa\\" bbb 34 | bbb "aaa" bbb 35 | bbb "aaa\\" bbb 36 | bbb "aaa \" aaa" bbb 37 | bbb "aaa" bbb "aaa" bbb 38 | bbb "aaa \" aaa" bbb "aaa \" aaa" bbb 39 | bbb "aaa \" aaa\\" bbb "aaa \" aaa\\" bbb 40 | EOD 41 | extra_opts=(-yy $tmpf) 42 | macro='' 43 | declare -A expected 44 | declare -A not_expected 45 | # a==97 b==98 46 | expected[style_dq_a]='$tmpf <<"EOD" 53 | "aa" /*bb*/ 54 | "aa/*aa" /*bb*/ 55 | "aa/*aa*/aa" /*bb*/ 56 | "aa" /*bb*/ 57 | "aa/*aa" /*bb"bb*/ 58 | "aa/*aa*/aa" /*bb"bb"bb*/ 59 | /*bb*/ "aa" 60 | /*bb*/ "aa/*aa" 61 | /*bb*/ "aa/*aa*/aa" 62 | /*bb*/ "aa" 63 | /*bb"bb*/ "aa/*aa" 64 | /*bb"bb"bb*/ "aa/*aa*/aa" 65 | EOD 66 | extra_opts=(-yy $tmpf) 67 | macro='' 68 | declare -A expected 69 | declare -A not_expected 70 | # a==97 b==98 71 | expected[style_sc_a]='$tmpf <<"EOD" 78 | # ccc 79 | # 80 | aaa "bbb" // ccc "ccc" 81 | aaa "bbb" # ccc "ccc" 82 | EOD 83 | extra_opts=(-yy $tmpf) 84 | macro='' 85 | declare -A expected 86 | declare -A not_expected 87 | # a==97 b==98 c==99 #=35 88 | expected[style_a]='/dev/null; echo TEST $tshort; tput sgr0 2>/dev/null 15 | pushd $tdir &>/dev/null 16 | ./$tshort 17 | ec=$? 18 | popd &>/dev/null 19 | echo 20 | [ $ec -eq 0 ] && pass=$((pass+1)) 21 | total=$((total+1)) 22 | done 23 | printf "Passed %d out of %d tests\n" $pass $total 24 | [ $pass -eq $total ] || exit 1 25 | -------------------------------------------------------------------------------- /tests/unit/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*.c 3 | -------------------------------------------------------------------------------- /tests/unit/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "mle.h" 8 | #include "mlbuf.h" 9 | 10 | editor_t _editor; // satisfies extern in mle.h 11 | 12 | int main(int argc, char **argv) { 13 | buffer_t *buf; 14 | mark_t *cur; 15 | void *self; 16 | void (*symfn)(buffer_t *, mark_t *); 17 | char **symstr; 18 | char symbuf[1024]; 19 | 20 | if (argc < 2) return EXIT_FAILURE; 21 | 22 | self = dlopen(NULL, RTLD_NOW); 23 | if (!self) return EXIT_FAILURE; 24 | 25 | *(void **)(&symfn) = dlsym(self, argv[1]); 26 | sprintf(symbuf, "%s_str", argv[1]); 27 | symstr = (char**)dlsym(self, symbuf); 28 | if (!symfn || !symstr) return EXIT_FAILURE; 29 | 30 | setlocale(LC_ALL, ""); 31 | 32 | pcre2_md = pcre2_match_data_create(10, NULL); // Normally initialized in editor_init 33 | 34 | memset(&_editor, 0, sizeof(editor_t)); 35 | buf = buffer_new(); 36 | buffer_insert(buf, 0, *symstr, (bint_t)strlen(*symstr), NULL); 37 | cur = buffer_add_mark(buf, NULL, 0); 38 | 39 | symfn(buf, cur); 40 | 41 | buffer_destroy(buf); 42 | 43 | pcre2_match_data_free(pcre2_md); 44 | 45 | dlclose(self); 46 | 47 | return EXIT_SUCCESS; 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/test.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "mle.h" 8 | #include "mlbuf.h" 9 | 10 | #define concat1(a, b) a ## b 11 | #define concat2(a, b) concat1(a, b) 12 | #define str concat2(TEST_NAME, _str) 13 | #define test TEST_NAME 14 | 15 | extern char *str; 16 | extern void test(buffer_t *buf, mark_t *cur); 17 | 18 | // TODO run each test with buffer_set_slabbed + buffer_insert 19 | 20 | #define ASSERT(testname, expected, observed) do { \ 21 | if ((expected) == (observed)) { \ 22 | printf(" \x1b[32mOK \x1b[0m %s\n", (testname)); \ 23 | } else { \ 24 | printf(" \x1b[31mERR \x1b[0m %s expected=%" PRIdPTR " observed=%" PRIdPTR "\n", (testname), (intptr_t)(expected), (intptr_t)(observed)); \ 25 | exit(EXIT_FAILURE); \ 26 | } \ 27 | } while (0); 28 | -------------------------------------------------------------------------------- /tests/unit/test_bline_delete.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | 9 | bline_delete(buf->first_line, 0, 1); 10 | buffer_get(buf, &data, &data_len); 11 | ASSERT("bol", 0, strncmp(data, "ello\nworld", data_len)); 12 | 13 | bline_delete(buf->first_line, 0, 0); 14 | buffer_get(buf, &data, &data_len); 15 | ASSERT("noop", 0, strncmp(data, "ello\nworld", data_len)); 16 | 17 | bline_delete(buf->first_line->next, 4, 1); 18 | buffer_get(buf, &data, &data_len); 19 | ASSERT("eol", 0, strncmp(data, "ello\nworl", data_len)); 20 | 21 | bline_delete(buf->first_line, 1, 2); 22 | buffer_get(buf, &data, &data_len); 23 | ASSERT("mid", 0, strncmp(data, "eo\nworl", data_len)); 24 | 25 | bline_delete(buf->first_line, 4, 1); 26 | buffer_get(buf, &data, &data_len); 27 | ASSERT("oob", 0, strncmp(data, "eo\nwrl", data_len)); 28 | 29 | bline_delete(buf->first_line, 2, 3); 30 | buffer_get(buf, &data, &data_len); 31 | ASSERT("eatnl", 0, strncmp(data, "eol", data_len)); 32 | } 33 | -------------------------------------------------------------------------------- /tests/unit/test_bline_get_col.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "h\xc3\x85llo \xe4\xb8\x96\xe7\x95\x8c"; // "hallo ww" 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | // 01 2 34567 8 9 10 11 12 7 | // 01 23456 7 8 | bint_t col; 9 | MLBUF_BLINE_ENSURE_CHARS(buf->first_line); 10 | ASSERT("len", 8, buf->first_line->char_count); 11 | 12 | bline_get_col(buf->first_line, 0, &col); 13 | ASSERT("0", 0, col); 14 | 15 | bline_get_col(buf->first_line, 1, &col); 16 | ASSERT("1", 1, col); 17 | 18 | bline_get_col(buf->first_line, 2, &col); 19 | ASSERT("2", 1, col); 20 | 21 | bline_get_col(buf->first_line, 3, &col); 22 | ASSERT("3", 2, col); 23 | 24 | bline_get_col(buf->first_line, 7, &col); 25 | ASSERT("7", 6, col); 26 | 27 | bline_get_col(buf->first_line, 9, &col); 28 | ASSERT("9", 6, col); 29 | 30 | bline_get_col(buf->first_line, 10, &col); 31 | ASSERT("10", 7, col); 32 | 33 | bline_get_col(buf->first_line, 11, &col); 34 | ASSERT("11", 7, col); 35 | 36 | bline_get_col(buf->first_line, 13, &col); 37 | ASSERT("13", 8, col); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /tests/unit/test_bline_insert.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | 9 | bline_insert(buf->first_line, 5, "\n", 1, NULL); 10 | buffer_get(buf, &data, &data_len); 11 | ASSERT("nl", 0, strncmp(data, "hello\n\nworld", data_len)); 12 | 13 | bline_insert(buf->first_line->next, 0, "my", 2, NULL); 14 | buffer_get(buf, &data, &data_len); 15 | ASSERT("my", 0, strncmp(data, "hello\nmy\nworld", data_len)); 16 | 17 | bline_insert(buf->first_line->next, 0, "a", 1, NULL); 18 | buffer_get(buf, &data, &data_len); 19 | ASSERT("bol", 0, strncmp(data, "hello\namy\nworld", data_len)); 20 | 21 | bline_insert(buf->first_line->next, 3, "'s", 2, NULL); 22 | buffer_get(buf, &data, &data_len); 23 | ASSERT("eol", 0, strncmp(data, "hello\namy's\nworld", data_len)); 24 | 25 | bline_insert(buf->first_line->next, 6, " ", 1, NULL); 26 | buffer_get(buf, &data, &data_len); 27 | ASSERT("oob", 0, strncmp(data, "hello\namy's\n world", data_len)); 28 | 29 | bline_insert(buf->last_line, buf->last_line->char_count, "\nno\xe2\x80\x8bspace", 11, NULL); 30 | // The following assertion relies on a UTF-8 locale. `nl_langinfo` may not 31 | // be available, so let's comment it out for now. (In practice, if locale 32 | // is, e.g., C, the zero-width space will show up as "?" with vwidth==1.) 33 | // 34 | // codeset = nl_langinfo(CODESET); 35 | // if (codeset && strcmp(codeset, "UTF-8") == 0) { 36 | // ASSERT("0wv", 7, buf->last_line->char_vwidth); 37 | // } 38 | ASSERT("0wc", 8, buf->last_line->char_count); 39 | ASSERT("0wd", 10, buf->last_line->data_len); 40 | 41 | bline_insert(buf->last_line, buf->last_line->char_count, "\nnull\x00", 6, NULL); 42 | ASSERT("nulv", 5, buf->last_line->char_vwidth); 43 | ASSERT("nulc", 5, buf->last_line->char_count); 44 | ASSERT("nuld", 5, buf->last_line->data_len); 45 | } 46 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_add_mark.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *mark; 7 | 8 | mark = buffer_add_mark(buf, NULL, 0); 9 | ASSERT("mark0bline", 1, mark->bline == buf->first_line ? 1 : 0); 10 | ASSERT("mark0col", 0, mark->col); 11 | 12 | mark = buffer_add_mark(buf, buf->first_line->next, 5); 13 | ASSERT("mark1bline", 1, mark->bline == buf->first_line->next ? 1 : 0); 14 | ASSERT("mark1col", 5, mark->col); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_add_srule.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | bint_t i; 7 | srule_t *srule1; 8 | srule_t *srule2; 9 | 10 | srule1 = srule_new_single("world", sizeof("world")-1, 0, 1, 2); 11 | srule2 = srule_new_multi("lo", sizeof("lo")-1, "wo", sizeof("wo")-1, 3, 4); 12 | 13 | buffer_add_srule(buf, srule1); 14 | for (i = 0; i < buf->first_line->char_count; i++) { 15 | ASSERT("line1fg", 0, buf->first_line->chars[i].style.fg); 16 | ASSERT("line1bg", 0, buf->first_line->chars[i].style.bg); 17 | } 18 | for (i = 0; i < buf->first_line->next->char_count; i++) { 19 | ASSERT("line2fg", 1, buf->first_line->next->chars[i].style.fg); 20 | ASSERT("line2bg", 2, buf->first_line->next->chars[i].style.bg); 21 | } 22 | 23 | buffer_remove_srule(buf, srule1); 24 | buffer_add_srule(buf, srule2); 25 | for (i = 0; i < buf->first_line->char_count; i++) { 26 | ASSERT("line1fg_m", (i == 3 || i == 4 ? 3 : 0), buf->first_line->chars[i].style.fg); 27 | ASSERT("line1bg_m", (i == 3 || i == 4 ? 4 : 0), buf->first_line->chars[i].style.bg); 28 | } 29 | for (i = 0; i < buf->first_line->next->char_count; i++) { 30 | ASSERT("line2fg_m", (i == 0 || i == 1 ? 3 : 0), buf->first_line->next->chars[i].style.fg); 31 | ASSERT("line2bg_m", (i == 0 || i == 1 ? 4 : 0), buf->first_line->next->chars[i].style.bg); 32 | } 33 | buffer_remove_srule(buf, srule2); 34 | 35 | srule_destroy(srule1); 36 | srule_destroy(srule2); 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_delete.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | 9 | buffer_delete(buf, 0, 1); 10 | buffer_get(buf, &data, &data_len); 11 | ASSERT("bol", 0, strncmp(data, "ello\nworld", data_len)); 12 | 13 | buffer_delete(buf, 4, 1); 14 | buffer_get(buf, &data, &data_len); 15 | ASSERT("nl", 0, strncmp(data, "elloworld", data_len)); 16 | 17 | buffer_delete(buf, 7, 3); 18 | buffer_get(buf, &data, &data_len); 19 | ASSERT("oob", 0, strncmp(data, "ellowor", data_len)); 20 | 21 | buffer_delete(buf, 0, 7); 22 | buffer_get(buf, &data, &data_len); 23 | ASSERT("all", 0, strncmp(data, "", data_len)); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_destroy.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | ASSERT("yes", 1, 1); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_get.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | 9 | buffer_get(buf, &data, &data_len); 10 | ASSERT("len", 11, data_len); 11 | ASSERT("get", 0, strncmp(data, "hello\nworld", data_len)); 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_get_bline.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "0\n1\n2\n3\n4\n"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | bint_t i; 7 | bline_t *line; 8 | bline_t *line2; 9 | for (line = buf->first_line, i = 0; line; line = line->next, i += 1) { 10 | buffer_get_bline(buf, i, &line2); 11 | ASSERT("line", line, line2); 12 | } 13 | for (line = buf->first_line, i = 0; line; line = line->next, i += 1) { 14 | buffer_get_bline_w_hint(buf, i, buf->last_line->prev, &line2); 15 | ASSERT("line", line, line2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_get_bline_col.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | bline_t *line; 7 | bint_t col; 8 | 9 | buffer_get_bline_col(buf, 0, &line, &col); 10 | ASSERT("0line", buf->first_line, line); 11 | ASSERT("0col", 0, col); 12 | 13 | buffer_get_bline_col(buf, 1, &line, &col); 14 | ASSERT("1line", buf->first_line, line); 15 | ASSERT("1col", 1, col); 16 | 17 | buffer_get_bline_col(buf, 5, &line, &col); 18 | ASSERT("5line", buf->first_line, line); 19 | ASSERT("5col", 5, col); 20 | 21 | buffer_get_bline_col(buf, 6, &line, &col); 22 | ASSERT("6line", buf->first_line->next, line); 23 | ASSERT("6col", 0, col); 24 | 25 | buffer_get_bline_col(buf, 99, &line, &col); 26 | ASSERT("oobline", buf->first_line->next, line); 27 | ASSERT("oobcol", 5, col); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_get_offset.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | bint_t offset; 7 | 8 | buffer_get_offset(buf, buf->first_line, 0, &offset); 9 | ASSERT("f0", 0, offset); 10 | 11 | buffer_get_offset(buf, buf->first_line, 1, &offset); 12 | ASSERT("f1", 1, offset); 13 | 14 | buffer_get_offset(buf, buf->first_line, 5, &offset); 15 | ASSERT("f5", 5, offset); 16 | 17 | buffer_get_offset(buf, buf->first_line->next, 0, &offset); 18 | ASSERT("n0", 6, offset); 19 | 20 | buffer_get_offset(buf, buf->first_line->next, 6, &offset); 21 | ASSERT("noob", 11, offset); 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_insert.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | bint_t nchars; 9 | 10 | buffer_insert(buf, 0, "\xe4\xb8\x96\xe7\x95\x8c\n", 7, &nchars); 11 | buffer_get(buf, &data, &data_len); 12 | ASSERT("bol", 0, strncmp(data, "\xe4\xb8\x96\xe7\x95\x8c\nhello\nworld", data_len)); 13 | ASSERT("bolnchars", 3, nchars); 14 | 15 | buffer_insert(buf, 3, "s", 1, &nchars); 16 | buffer_get(buf, &data, &data_len); 17 | ASSERT("3", 0, strncmp(data, "\xe4\xb8\x96\xe7\x95\x8c\nshello\nworld", data_len)); 18 | ASSERT("3nchars", 1, nchars); 19 | 20 | buffer_insert(buf, 7, "\n\n", 2, &nchars); 21 | buffer_get(buf, &data, &data_len); 22 | ASSERT("nlnl", 0, strncmp(data, "\xe4\xb8\x96\xe7\x95\x8c\nshel\n\nlo\nworld", data_len)); 23 | ASSERT("nlnlnchars", 2, nchars); 24 | 25 | buffer_insert(buf, 20, "!", 1, NULL); 26 | buffer_get(buf, &data, &data_len); 27 | ASSERT("oob", 0, strncmp(data, "\xe4\xb8\x96\xe7\x95\x8c\nshel\n\nlo\nworld!", data_len)); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_new.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | ASSERT("yes", 1, 1); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_redo.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hi"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | int action_group; 9 | 10 | buffer_delete(buf, 0, 1); 11 | buffer_undo(buf); 12 | buffer_redo(buf); 13 | buffer_get(buf, &data, &data_len); 14 | ASSERT("del", 0, strncmp(data, "i", data_len)); 15 | 16 | buffer_insert(buf, 0, "y", 1, NULL); 17 | buffer_undo(buf); 18 | buffer_redo(buf); 19 | buffer_get(buf, &data, &data_len); 20 | ASSERT("ins", 0, strncmp(data, "yi", data_len)); 21 | 22 | action_group = 0; 23 | buffer_set_action_group_ptr(buf, &action_group); 24 | 25 | buffer_insert(buf, 2, "e", 1, NULL); 26 | buffer_insert(buf, 3, "ld", 2, NULL); 27 | action_group += 1; 28 | buffer_undo_action_group(buf); 29 | buffer_redo_action_group(buf); 30 | buffer_get(buf, &data, &data_len); 31 | ASSERT("ins_group", 0, strncmp(data, "yield", data_len)); 32 | } 33 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_register.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hi"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *reg; 7 | size_t reg_len; 8 | 9 | buffer_register_set(buf, 'a', "yo", 2); 10 | buffer_register_get(buf, 'a', 0, ®, ®_len); 11 | ASSERT("set", 0, strncmp(reg, "yo", reg_len)) 12 | 13 | buffer_register_prepend(buf, 'a', "g", 1); 14 | buffer_register_get(buf, 'a', 0, ®, ®_len); 15 | ASSERT("pre", 0, strncmp(reg, "gyo", reg_len)) 16 | 17 | buffer_register_append(buf, 'a', "!!!", 3); 18 | buffer_register_get(buf, 'a', 0, ®, ®_len); 19 | ASSERT("app", 0, strncmp(reg, "gyo!!!", reg_len)) 20 | 21 | buffer_register_clear(buf, 'a'); 22 | buffer_register_get(buf, 'a', 0, ®, ®_len); 23 | ASSERT("clr1", 0, reg_len); 24 | ASSERT("clr2", 1, reg != NULL ? 1 : 0); 25 | } 26 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_remove_srule.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | bint_t i; 7 | srule_t *srule; 8 | srule = srule_new_single("world", sizeof("world")-1, 0, 1, 2); 9 | buffer_add_srule(buf, srule); 10 | buffer_remove_srule(buf, srule); 11 | for (i = 0; i < buf->first_line->char_count; i++) { 12 | ASSERT("line1fg", 0, buf->first_line->chars[i].style.fg); 13 | ASSERT("line1bg", 0, buf->first_line->chars[i].style.bg); 14 | } 15 | for (i = 0; i < buf->first_line->next->char_count; i++) { 16 | ASSERT("line2fg", 0, buf->first_line->next->chars[i].style.fg); 17 | ASSERT("line2bg", 0, buf->first_line->next->chars[i].style.bg); 18 | } 19 | srule_destroy(srule); 20 | } 21 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_replace.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "lineA\n\nline2\nline3\n"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | 9 | buffer_replace(buf, 0, 0, "b", 1); 10 | buffer_get(buf, &data, &data_len); 11 | ASSERT("rpl1", 0, strncmp(data, "blineA\n\nline2\nline3\n", data_len)); 12 | ASSERT("r1bc", data_len, buf->byte_count); 13 | ASSERT("r1lc", 5, buf->line_count); 14 | 15 | buffer_replace(buf, 3, 3, "xe0", 3); 16 | buffer_get(buf, &data, &data_len); 17 | ASSERT("rpl2", 0, strncmp(data, "blixe0\n\nline2\nline3\n", data_len)); 18 | ASSERT("r2bc", data_len, buf->byte_count); 19 | ASSERT("r2lc", 5, buf->line_count); 20 | 21 | buffer_replace(buf, 10, 7, "N", 1); 22 | buffer_get(buf, &data, &data_len); 23 | ASSERT("rpl3", 0, strncmp(data, "blixe0\n\nliNe3\n", data_len)); 24 | ASSERT("r3bc", data_len, buf->byte_count); 25 | ASSERT("r3lc", 4, buf->line_count); 26 | 27 | buffer_replace(buf, 5, 4, "jerk\nstuff", 10); 28 | buffer_get(buf, &data, &data_len); 29 | ASSERT("rpl4", 0, strncmp(data, "blixejerk\nstuffiNe3\n", data_len)); 30 | ASSERT("r4bc", data_len, buf->byte_count); 31 | ASSERT("r4lc", 3, buf->line_count); 32 | 33 | buffer_replace(buf, 9, 99, "X", 1); 34 | buffer_get(buf, &data, &data_len); 35 | ASSERT("rpl5", 0, strncmp(data, "blixejerkX", data_len)); 36 | ASSERT("r5bc", data_len, buf->byte_count); 37 | ASSERT("r5lc", 1, buf->line_count); 38 | 39 | buffer_replace(buf, 5, 0, "y\nb", 3); 40 | buffer_get(buf, &data, &data_len); 41 | ASSERT("rpl6", 0, strncmp(data, "blixey\nbjerkX", data_len)); 42 | ASSERT("r6bc", data_len, buf->byte_count); 43 | ASSERT("r6lc", 2, buf->line_count); 44 | 45 | buffer_replace(buf, 0, 0, "\n", 1); 46 | buffer_get(buf, &data, &data_len); 47 | ASSERT("rpl7", 0, strncmp(data, "\nblixey\nbjerkX", data_len)); 48 | ASSERT("r7bc", data_len, buf->byte_count); 49 | ASSERT("r7lc", 3, buf->line_count); 50 | 51 | buffer_replace(buf, 6, 3, NULL, 0); 52 | buffer_get(buf, &data, &data_len); 53 | ASSERT("rpl8", 0, strncmp(data, "\nblixejerkX", data_len)); 54 | ASSERT("r8bc", data_len, buf->byte_count); 55 | ASSERT("r8lc", 2, buf->line_count); 56 | 57 | buffer_replace(buf, 0, 11, "1\n2\n3\n4\n", 8); 58 | buffer_get(buf, &data, &data_len); 59 | ASSERT("rpl9", 0, strncmp(data, "1\n2\n3\n4\n", data_len)); 60 | ASSERT("r9bc", data_len, buf->byte_count); 61 | ASSERT("r9lc", 5, buf->line_count); 62 | 63 | buffer_replace(buf, 2, 6, "five\nsix\nseven\neight\nnine", 25); 64 | buffer_get(buf, &data, &data_len); 65 | ASSERT("rpla", 0, strncmp(data, "1\nfive\nsix\nseven\neight\nnine", data_len)); 66 | ASSERT("rabc", data_len, buf->byte_count); 67 | ASSERT("ralc", 6, buf->line_count); 68 | 69 | buffer_undo(buf); 70 | buffer_undo(buf); 71 | buffer_undo(buf); 72 | buffer_undo(buf); 73 | buffer_undo(buf); 74 | buffer_undo(buf); 75 | buffer_undo(buf); 76 | buffer_undo(buf); 77 | buffer_undo(buf); 78 | buffer_get(buf, &data, &data_len); 79 | ASSERT("undo", 0, strncmp(data, "\nblixey\nbjerkX", data_len)); 80 | } 81 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_set.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | buffer_set(buf, "goodbye\nvoid", 12); 9 | buffer_get(buf, &data, &data_len); 10 | ASSERT("set", 0, strncmp("goodbye\nvoid", data, data_len)); 11 | } 12 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_set_callback.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | buffer_t *global_buf; 4 | int *global_udata; 5 | 6 | static void callback_fn(buffer_t *buf, baction_t *bac, void *udata) { 7 | ASSERT("bufp", global_buf, buf); 8 | ASSERT("udata", (void*)global_udata, udata); 9 | ASSERT("bac_type", MLBUF_BACTION_TYPE_INSERT, bac->type); 10 | ASSERT("bac_buf", buf, bac->buffer); 11 | ASSERT("bac_start_line", buf->first_line, bac->start_line); 12 | ASSERT("bac_start_line_index", 0, bac->start_line_index); 13 | ASSERT("bac_start_col", 0, bac->start_col); 14 | ASSERT("bac_maybe_end_line", buf->first_line->next, bac->maybe_end_line); 15 | ASSERT("bac_maybe_end_line_index", 1, bac->maybe_end_line_index); 16 | ASSERT("bac_maybe_end_col", 2, bac->maybe_end_col); 17 | ASSERT("bac_byte_delta", 5, bac->byte_delta); 18 | ASSERT("bac_char_delta", 5, bac->char_delta); 19 | ASSERT("bac_line_delta", 1, bac->line_delta); 20 | ASSERT("bac_data", 0, strncmp("te\nst", bac->data, bac->data_len)); 21 | } 22 | 23 | char *str = "hello\nworld"; 24 | 25 | void test(buffer_t *buf, mark_t *cur) { 26 | int udata; 27 | udata = 42; 28 | global_buf = buf; 29 | global_udata = &udata; 30 | buffer_set_callback(buf, callback_fn, (void*)global_udata); 31 | buffer_insert(buf, 0, "te\nst", 5, NULL); 32 | } 33 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_set_tab_width.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | #define comma , 4 | 5 | char *str = "he\tllo\t\t"; 6 | 7 | void test(buffer_t *buf, mark_t *cur) { 8 | bint_t i; 9 | bint_t char_vcols_4[8] = {0 comma 1 comma 2 comma 4 comma 5 comma 6 comma 7 comma 8}; 10 | bint_t char_vcols_2a[8] = {0 comma 1 comma 2 comma 4 comma 5 comma 6 comma 7 comma 8}; 11 | bint_t char_vcols_2b[9] = {0 comma 1 comma 2 comma 4 comma 5 comma 6 comma 7 comma 8 comma 10}; 12 | 13 | buffer_set_tab_width(buf, 4); 14 | // [he llo ] // char_vcol 15 | // [ t tt ] // tabs 16 | ASSERT("count4", 8, buf->first_line->char_count); 17 | ASSERT("width4", 12, buf->first_line->char_vwidth); 18 | for (i = 0; i < buf->first_line->char_count; i++) { 19 | ASSERT("vcol4", char_vcols_4[i], buf->first_line->chars[i].vcol); 20 | } 21 | 22 | buffer_set_tab_width(buf, 2); 23 | // [he llo ] // char_vcol 24 | // [ t tt ] // tabs 25 | ASSERT("count2a", 8, buf->first_line->char_count); 26 | ASSERT("width2a", 10, buf->first_line->char_vwidth); 27 | for (i = 0; i < buf->first_line->char_count; i++) { 28 | ASSERT("vcol2a", char_vcols_2a[i], buf->first_line->chars[i].vcol); 29 | } 30 | 31 | bline_insert(buf->first_line, 4, "\t", 1, NULL); 32 | // [he l lo ] // char_vcol 33 | // [ t t t t ] // tabs 34 | ASSERT("count2b", 9, buf->first_line->char_count); 35 | ASSERT("width2b", 12, buf->first_line->char_vwidth); 36 | for (i = 0; i < buf->first_line->char_count; i++) { 37 | ASSERT("vcol2b", char_vcols_2b[i], buf->first_line->chars[i].vcol); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_srule_overlap.c.todo: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "[\n * comment\n ]/\nsel"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | bint_t i; 7 | srule_t* multi; 8 | srule_t* range; 9 | mark_t* ma; 10 | mark_t* mb; 11 | 12 | ma = buffer_add_mark(buf, buf->first_line->next, 0); 13 | mb = buffer_add_mark(buf, buf->first_line->next->next->next, 3); 14 | multi = srule_new_multi("\\[", 2, "\\]", 2, 1, 2); 15 | range = srule_new_range(ma, mb, 3, 4); 16 | buffer_add_srule(buf, multi); 17 | buffer_add_srule(buf, range); 18 | 19 | for (i = 0; i < buf->first_line->next->char_count; i++) { 20 | ASSERT("line1fg", 1, buf->first_line->next->chars[i].style.fg); 21 | ASSERT("line1bg", 2, buf->first_line->next->chars[i].style.bg); 22 | } 23 | for (i = 0; i < buf->first_line->next->next->next->char_count; i++) { 24 | ASSERT("line2fg", 3, buf->first_line->next->next->next->chars[i].style.fg); 25 | ASSERT("line2bg", 4, buf->first_line->next->next->next->chars[i].style.bg); 26 | } 27 | 28 | buffer_remove_srule(buf, multi); 29 | buffer_remove_srule(buf, range); 30 | srule_destroy(multi); 31 | srule_destroy(range); 32 | buffer_destroy_mark(buf, ma); 33 | buffer_destroy_mark(buf, mb); 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_substr.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | bint_t nchars; 9 | 10 | buffer_substr(buf, buf->first_line, 4, buf->first_line->next, 1, &data, &data_len, &nchars); 11 | ASSERT("datalen", 3, data_len); 12 | ASSERT("nchars", 3, nchars); 13 | ASSERT("substr", 0, strncmp("o\nw", data, data_len)); 14 | free(data); 15 | } 16 | -------------------------------------------------------------------------------- /tests/unit/test_buffer_undo.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hi"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | int action_group; 9 | 10 | buffer_delete(buf, 0, 1); 11 | buffer_undo(buf); 12 | buffer_get(buf, &data, &data_len); 13 | ASSERT("del", 0, strncmp(data, "hi", data_len)); 14 | 15 | buffer_insert(buf, 0, "c", 1, NULL); 16 | buffer_undo(buf); 17 | buffer_get(buf, &data, &data_len); 18 | ASSERT("ins", 0, strncmp(data, "hi", data_len)); 19 | 20 | action_group = 0; 21 | buffer_set_action_group_ptr(buf, &action_group); 22 | 23 | buffer_insert(buf, 2, "t", 1, NULL); 24 | buffer_insert(buf, 3, "!", 1, NULL); 25 | action_group += 1; 26 | buffer_undo_action_group(buf); 27 | buffer_get(buf, &data, &data_len); 28 | ASSERT("ins_group", 0, strncmp(data, "hi", data_len)); 29 | } 30 | -------------------------------------------------------------------------------- /tests/unit/test_kinput.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | #define NUM_KINPUTS 256 4 | #define OFFSETOF(PTR, ELEMENT) ((ptrdiff_t)&(((PTR))->ELEMENT) - (ptrdiff_t)(PTR)) 5 | 6 | char *str = ""; 7 | 8 | void test(buffer_t *buf, mark_t *cur) { 9 | kinput_t weird_input[NUM_KINPUTS]; 10 | kinput_t input; 11 | int result; 12 | size_t i, j; 13 | size_t offsetof_mod, offsetof_ch, offsetof_key; 14 | kinput_t *input_ptr = &input; 15 | 16 | offsetof_mod = OFFSETOF(input_ptr, mod); 17 | offsetof_ch = OFFSETOF(input_ptr, ch); 18 | offsetof_key = OFFSETOF(input_ptr, key); 19 | 20 | // Fill weird_input values 21 | for (i = 0; i < sizeof(kinput_t) * NUM_KINPUTS; i++) { 22 | *(((uint8_t*)weird_input) + i) = (uint8_t)i; 23 | } 24 | 25 | // Ensure MLE_KINPUT_COPY *preserves* padding bytes 26 | result = 0; 27 | for (i = 0; i < NUM_KINPUTS; i++) { 28 | MLE_KINPUT_COPY(input, weird_input[i]); 29 | result |= memcmp(&input, &weird_input[i], sizeof(kinput_t)); 30 | } 31 | ASSERT("cmp_kinput_assign", 0, result); 32 | 33 | // Ensure MLE_KINPUT_SET *zeroes* padding bytes 34 | result = 0; 35 | for (i = 0; i < NUM_KINPUTS; i++) { 36 | // Fill input with non-sense; 42 is a good choice 37 | memset(&input, 42, sizeof(input)); 38 | 39 | // Set input to weird_input[i] 40 | MLE_KINPUT_SET(input, weird_input[i].mod, weird_input[i].ch, weird_input[i].key); 41 | 42 | // Ensure all fields are equal 43 | result |= memcmp(&input.mod, &weird_input[i].mod, sizeof(input.mod)); 44 | result |= memcmp(&input.ch, &weird_input[i].ch, sizeof(input.ch)); 45 | result |= memcmp(&input.key, &weird_input[i].key, sizeof(input.key)); 46 | 47 | // Ensure bytes between mod and ch are zero 48 | for (j = offsetof_mod + sizeof(input.mod); j < offsetof_ch; j++) { 49 | result |= *(((uint8_t*)&input) + j) == 0x00 ? 0 : 1; 50 | } 51 | 52 | // Ensure bytes between ch and key are zero 53 | for (j = offsetof_ch + sizeof(input.ch); j < offsetof_key; j++) { 54 | result |= *(((uint8_t*)&input) + j) == 0x00 ? 0 : 1; 55 | } 56 | 57 | // Ensure bytes between key and end are zero 58 | for (j = offsetof_key + sizeof(input.key); j < sizeof(kinput_t); j++) { 59 | result |= *(((uint8_t*)&input) + j) == 0x00 ? 0 : 1; 60 | } 61 | } 62 | ASSERT("cmp_kinput_set", 0, result); 63 | } 64 | -------------------------------------------------------------------------------- /tests/unit/test_mark_clone.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *other; 7 | mark_clone(cur, &other); 8 | ASSERT("neq", 1, other != cur ? 1 : 0); 9 | ASSERT("next", 1, other == cur->next ? 1 : 0); 10 | ASSERT("line", cur->bline, other->bline); 11 | ASSERT("col", cur->col, other->col); 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/test_mark_cmp.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *other, *first, *second; 7 | int cmp; 8 | other = buffer_add_mark(buf, NULL, 0); 9 | 10 | mark_move_beginning(cur); 11 | mark_move_end(other); 12 | cmp = mark_cmp(cur, other, &first, &second); 13 | ASSERT("a_cmp", -1, cmp); 14 | ASSERT("a_first", cur, first); 15 | ASSERT("a_second", other, second); 16 | 17 | mark_move_end(cur); 18 | mark_move_beginning(other); 19 | cmp = mark_cmp(cur, other, &first, &second); 20 | ASSERT("b_cmp", 1, cmp); 21 | ASSERT("b_first", other, first); 22 | ASSERT("b_second", cur, second); 23 | 24 | mark_move_beginning(cur); 25 | mark_move_beginning(other); 26 | cmp = mark_cmp(cur, other, &first, &second); 27 | ASSERT("c_cmp", 0, cmp); 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/test_mark_delete_after.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | mark_move_beginning(cur); 9 | mark_delete_after(cur, 1); 10 | buffer_get(buf, &data, &data_len); 11 | ASSERT("dela", 0, strncmp("ello\nworld", data, data_len)); 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/test_mark_delete_before.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | mark_move_end(cur); 9 | mark_delete_before(cur, 1); 10 | buffer_get(buf, &data, &data_len); 11 | ASSERT("delb", 0, strncmp("hello\nworl", data, data_len)); 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/test_mark_delete_between_mark.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *other; 7 | other = buffer_add_mark(buf, NULL, 0); 8 | mark_move_beginning(cur); 9 | mark_move_end(other); 10 | mark_delete_between(cur, other); 11 | ASSERT("count", 0, buf->byte_count); 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/test_mark_delete_between_mark_2.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = " hello {\n world\n }"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *other; 7 | other = buffer_add_mark(buf, NULL, 0); 8 | mark_move_next_str(cur, "{", 1); 9 | mark_move_by(cur, 1); 10 | mark_move_next_str(other, "}", 1); 11 | mark_delete_between(cur, other); 12 | ASSERT("lnct", 1, buf->line_count); 13 | ASSERT("data", 0, strncmp(" hello {}", buf->first_line->data, buf->first_line->data_len)); 14 | ASSERT("clin", 0, cur->bline->line_index); 15 | ASSERT("ccol", 9, cur->col); 16 | ASSERT("olin", 0, cur->bline->line_index); 17 | ASSERT("ocol", 9, cur->col); 18 | mark_destroy(other); 19 | } 20 | -------------------------------------------------------------------------------- /tests/unit/test_mark_find_bracket_pair.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "[ bracket [ test ] ] xyz"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); 7 | mark_move_bracket_pair(cur, 1024); 8 | ASSERT("col1", 19, cur->col); 9 | mark_move_by(cur, -2); 10 | mark_move_bracket_pair(cur, 1024); 11 | ASSERT("col2", 10, cur->col); 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/test_mark_get_between_mark.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *str; 7 | bint_t str_len; 8 | mark_t *other; 9 | other = buffer_add_mark(buf, NULL, 0); 10 | mark_move_beginning(cur); 11 | mark_move_end(other); 12 | mark_get_between(cur, other, &str, &str_len); 13 | ASSERT("len", 11, str_len); 14 | ASSERT("eq", 0, strncmp(str, "hello\nworld", str_len)); 15 | free(str); 16 | } 17 | -------------------------------------------------------------------------------- /tests/unit/test_mark_get_nchars_between.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *other; 7 | bint_t nchars; 8 | other = buffer_add_mark(buf, NULL, 0); 9 | mark_move_beginning(cur); 10 | mark_move_end(other); 11 | nchars = 0; 12 | mark_get_nchars_between(cur, other, &nchars); 13 | ASSERT("nchars", 11, nchars); 14 | } 15 | -------------------------------------------------------------------------------- /tests/unit/test_mark_get_offset.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | bint_t offset; 7 | 8 | mark_move_end(cur); 9 | mark_get_offset(cur, &offset); 10 | ASSERT("offset", 11, offset); 11 | 12 | mark_move_beginning(cur); 13 | mark_get_offset(cur, &offset); 14 | ASSERT("offset", 0, offset); 15 | } 16 | -------------------------------------------------------------------------------- /tests/unit/test_mark_insert_after.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | mark_move_beginning(cur); 9 | mark_insert_after(cur, "s", 1); 10 | buffer_get(buf, &data, &data_len); 11 | ASSERT("insa", 0, strncmp("shello\nworld", data, data_len)); 12 | ASSERT("col", 0, cur->col); 13 | } 14 | -------------------------------------------------------------------------------- /tests/unit/test_mark_insert_before.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | char *data; 7 | bint_t data_len; 8 | mark_move_beginning(cur); 9 | mark_insert_before(cur, "s", 1); 10 | buffer_get(buf, &data, &data_len); 11 | ASSERT("insb", 0, strncmp("shello\nworld", data, data_len)); 12 | ASSERT("col", 1, cur->col); 13 | } 14 | -------------------------------------------------------------------------------- /tests/unit/test_mark_is_at_word_bound.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "obj->method() yes bob"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); // |obj->method() yes bob 7 | ASSERT("l_bol", 1, mark_is_at_word_bound(cur, -1)); 8 | ASSERT("x_bol", 1, mark_is_at_word_bound(cur, 0)); 9 | ASSERT("r_bol", 0, mark_is_at_word_bound(cur, 1)); 10 | 11 | mark_move_by(cur, 1); // o|bj->method() yes bob 12 | ASSERT("l_mid", 0, mark_is_at_word_bound(cur, -1)); 13 | ASSERT("x_mid", 0, mark_is_at_word_bound(cur, 0)); 14 | ASSERT("r_mid", 0, mark_is_at_word_bound(cur, 1)); 15 | 16 | mark_move_by(cur, 2); // obj|->method() yes bob 17 | ASSERT("l_eow", 0, mark_is_at_word_bound(cur, -1)); 18 | ASSERT("x_eow", 1, mark_is_at_word_bound(cur, 0)); 19 | ASSERT("r_eow", 1, mark_is_at_word_bound(cur, 1)); 20 | 21 | mark_move_by(cur, 1); // obj-|>method() yes bob 22 | ASSERT("l_sym", 0, mark_is_at_word_bound(cur, -1)); 23 | ASSERT("x_sym", 0, mark_is_at_word_bound(cur, 0)); 24 | ASSERT("r_sym", 0, mark_is_at_word_bound(cur, 1)); 25 | 26 | mark_move_by(cur, 8); // obj->method(|) yes bob 27 | ASSERT("l_sym2", 0, mark_is_at_word_bound(cur, -1)); 28 | ASSERT("x_sym2", 0, mark_is_at_word_bound(cur, 0)); 29 | ASSERT("r_sym2", 0, mark_is_at_word_bound(cur, 1)); 30 | 31 | mark_move_by(cur, 1); // obj->method()| yes bob 32 | ASSERT("l_spa1", 0, mark_is_at_word_bound(cur, -1)); 33 | ASSERT("x_spa1", 0, mark_is_at_word_bound(cur, 0)); 34 | ASSERT("r_spa1", 0, mark_is_at_word_bound(cur, 1)); 35 | 36 | mark_move_by(cur, 1); // obj->method() |yes bob 37 | ASSERT("l_sow", 1, mark_is_at_word_bound(cur, -1)); 38 | ASSERT("x_sow", 1, mark_is_at_word_bound(cur, 0)); 39 | ASSERT("r_sow", 0, mark_is_at_word_bound(cur, 1)); 40 | 41 | mark_move_by(cur, 3); // obj->method() yes| bob 42 | ASSERT("l_eow2", 0, mark_is_at_word_bound(cur, -1)); 43 | ASSERT("x_eow2", 1, mark_is_at_word_bound(cur, 0)); 44 | ASSERT("r_eow2", 1, mark_is_at_word_bound(cur, 1)); 45 | 46 | mark_move_eol(cur); // obj->method() yes bob| 47 | ASSERT("l_eow2", 0, mark_is_at_word_bound(cur, -1)); 48 | ASSERT("x_eow2", 1, mark_is_at_word_bound(cur, 0)); 49 | ASSERT("r_eow2", 1, mark_is_at_word_bound(cur, 1)); 50 | } 51 | -------------------------------------------------------------------------------- /tests/unit/test_mark_is_gt.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *other; 7 | other = buffer_add_mark(buf, NULL, 0); 8 | 9 | mark_move_beginning(cur); 10 | mark_move_end(other); 11 | ASSERT("gt", 1, mark_is_gt(other, cur)); 12 | ASSERT("ngt1", 0, mark_is_gt(cur, other)); 13 | 14 | mark_move_beginning(other); 15 | ASSERT("ngt2", 0, mark_is_gt(other, cur)); 16 | ASSERT("ngt3", 0, mark_is_gt(cur, other)); 17 | } 18 | -------------------------------------------------------------------------------- /tests/unit/test_mark_lettered.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *lett1; 7 | mark_t *lett2; 8 | mark_t *lett3; 9 | 10 | mark_clone_w_letter(cur, 'a', &lett1); 11 | ASSERT("neq1", 1, lett1 != cur ? 1 : 0); 12 | ASSERT("let", '\0', cur->letter); 13 | ASSERT("let", 'a', lett1->letter); 14 | 15 | mark_clone_w_letter(cur, 'a', &lett2); // lett1 destroyed 16 | ASSERT("neq2", 1, lett1 != lett2 ? 1 : 0); 17 | 18 | mark_clone_w_letter(cur, '?', &lett3); 19 | ASSERT("null", 1, lett3 == NULL ? 1 : 0); 20 | 21 | mark_destroy(lett2); 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_bol.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_end(cur); 7 | mark_move_bol(cur); 8 | ASSERT("line", buf->first_line->next, cur->bline); 9 | ASSERT("col", 0, cur->col); 10 | } 11 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_bracket_pair.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "[ bracket [ test ] ] xyz"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); 7 | mark_move_bracket_pair(cur, 1024); 8 | ASSERT("col1", 19, cur->col); 9 | mark_move_by(cur, -2); 10 | mark_move_bracket_pair(cur, 1024); 11 | ASSERT("col2", 10, cur->col); 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_by.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); 7 | 8 | mark_move_by(cur, 1); 9 | ASSERT("col1", 1, cur->col); 10 | 11 | mark_move_by(cur, 4); 12 | ASSERT("col2", 5, cur->col); 13 | 14 | mark_move_by(cur, 1); 15 | ASSERT("col3", 0, cur->col); 16 | ASSERT("line3", buf->first_line->next, cur->bline); 17 | 18 | mark_move_by(cur, -1); 19 | ASSERT("col4", 5, cur->col); 20 | ASSERT("line4", buf->first_line, cur->bline); 21 | } 22 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_col.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); 7 | 8 | mark_move_col(cur, 5); 9 | ASSERT("col1", 5, cur->col); 10 | 11 | mark_move_col(cur, 6); 12 | ASSERT("oob", 5, cur->col); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_eol.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); 7 | mark_move_eol(cur); 8 | ASSERT("line", buf->first_line, cur->bline); 9 | ASSERT("col", 5, cur->col); 10 | } 11 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_next_re.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hi\nanna\nbanana"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); 7 | 8 | mark_move_next_re(cur, "an+a", strlen("an+a")); 9 | ASSERT("line1", buf->first_line->next, cur->bline); 10 | ASSERT("col1", 0, cur->col); 11 | 12 | mark_move_next_re(cur, "an+a", strlen("an+a")); 13 | ASSERT("line1again", buf->first_line->next, cur->bline); 14 | ASSERT("col1again", 0, cur->col); 15 | 16 | mark_move_next_re_nudge(cur, "an+a", strlen("an+a")); 17 | ASSERT("line2", buf->first_line->next->next, cur->bline); 18 | ASSERT("col2", 1, cur->col); 19 | 20 | mark_move_next_re_nudge(cur, "an+a", strlen("an+a")); 21 | ASSERT("line3", buf->first_line->next->next, cur->bline); 22 | ASSERT("col3", 3, cur->col); 23 | 24 | mark_move_beginning(cur); 25 | mark_move_next_re(cur, "hi", strlen("hi")); 26 | ASSERT("line4", buf->first_line, cur->bline); 27 | ASSERT("col4", 0, cur->col); 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_next_str.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hi\nana\nbanana"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); 7 | 8 | mark_move_next_str(cur, "ana", strlen("ana")); 9 | ASSERT("line1", buf->first_line->next, cur->bline); 10 | ASSERT("col1", 0, cur->col); 11 | 12 | mark_move_next_str(cur, "ana", strlen("ana")); 13 | ASSERT("line1again", buf->first_line->next, cur->bline); 14 | ASSERT("col1again", 0, cur->col); 15 | 16 | mark_move_next_str_nudge(cur, "ana", strlen("ana")); 17 | ASSERT("line2", buf->first_line->next->next, cur->bline); 18 | ASSERT("col2", 1, cur->col); 19 | 20 | mark_move_next_str_nudge(cur, "ana", strlen("ana")); 21 | ASSERT("line3", buf->first_line->next->next, cur->bline); 22 | ASSERT("col3", 3, cur->col); 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_offset.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_offset(cur, 11); 7 | ASSERT("col1", 5, cur->col); 8 | ASSERT("line1", buf->first_line->next, cur->bline); 9 | 10 | mark_move_offset(cur, 6); 11 | ASSERT("col2", 0, cur->col); 12 | ASSERT("line2", buf->first_line->next, cur->bline); 13 | 14 | mark_move_offset(cur, 12); 15 | ASSERT("oobcol", 5, cur->col); 16 | ASSERT("oobline", buf->first_line->next, cur->bline); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_prev_re.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hi\nanna\nbanana"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_end(cur); 7 | 8 | mark_move_prev_re(cur, "an+a", strlen("an+a")); 9 | ASSERT("line3", buf->first_line->next->next, cur->bline); 10 | ASSERT("col3", 3, cur->col); 11 | 12 | mark_move_prev_re(cur, "an+a", strlen("an+a")); 13 | ASSERT("line2", buf->first_line->next->next, cur->bline); 14 | ASSERT("col2", 1, cur->col); 15 | 16 | mark_move_prev_re(cur, "an+a", strlen("an+a")); 17 | ASSERT("line1", buf->first_line->next, cur->bline); 18 | ASSERT("col1", 0, cur->col); 19 | 20 | mark_move_end(cur); 21 | mark_move_prev_re(cur, "^", strlen("^")); 22 | ASSERT("cflex_line", buf->first_line->next->next, cur->bline); 23 | ASSERT("cflex_col", 0, cur->col); 24 | } 25 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_prev_str.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hi\nana\nbanana"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_end(cur); 7 | 8 | mark_move_prev_str(cur, "ana", strlen("ana")); 9 | ASSERT("line3", buf->first_line->next->next, cur->bline); 10 | ASSERT("col3", 3, cur->col); 11 | 12 | mark_move_prev_str(cur, "ana", strlen("ana")); 13 | ASSERT("line2", buf->first_line->next->next, cur->bline); 14 | ASSERT("col2", 1, cur->col); 15 | 16 | mark_move_prev_str(cur, "ana", strlen("ana")); 17 | ASSERT("line1", buf->first_line->next, cur->bline); 18 | ASSERT("col1", 0, cur->col); 19 | } 20 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_to.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_beginning(cur); 7 | 8 | mark_move_to(cur, 0, 0); 9 | ASSERT("lin1", buf->first_line, cur->bline); 10 | ASSERT("col1", 0, cur->col); 11 | 12 | mark_move_to(cur, 0, 5); 13 | ASSERT("lin2", buf->first_line, cur->bline); 14 | ASSERT("col2", 5, cur->col); 15 | 16 | mark_move_to(cur, 1, 0); 17 | ASSERT("lin3", buf->first_line->next, cur->bline); 18 | ASSERT("col3", 0, cur->col); 19 | 20 | mark_move_to(cur, 1, 3); 21 | ASSERT("lin4", buf->first_line->next, cur->bline); 22 | ASSERT("col4", 3, cur->col); 23 | 24 | mark_move_to(cur, 1, 6); 25 | ASSERT("oob1lin", buf->first_line->next, cur->bline); 26 | ASSERT("oob1col", 5, cur->col); 27 | 28 | mark_move_to(cur, 2, 0); 29 | ASSERT("oob2lin", buf->first_line->next, cur->bline); 30 | ASSERT("oob2col", 0, cur->col); 31 | 32 | mark_move_to(cur, 2, 1); 33 | ASSERT("oob3lin", buf->first_line->next, cur->bline); 34 | ASSERT("oob3col", 1, cur->col); 35 | } 36 | -------------------------------------------------------------------------------- /tests/unit/test_mark_move_vert.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hi\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_move_end(cur); 7 | 8 | mark_move_vert(cur, 1); 9 | ASSERT("oob", buf->first_line->next, cur->bline); 10 | 11 | mark_move_vert(cur, 0); 12 | ASSERT("noop", buf->first_line->next, cur->bline); 13 | 14 | mark_move_vert(cur, -1); 15 | ASSERT("noop", buf->first_line, cur->bline); 16 | ASSERT("col", 2, cur->col); 17 | ASSERT("tcol", 5, cur->target_col); 18 | } 19 | -------------------------------------------------------------------------------- /tests/unit/test_mark_set_pcre_ovector.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "Ervin won gold at age 35!"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | PCRE2_SIZE ovector[6] = {0}; 7 | int rc = 0; 8 | bline_t *bline; 9 | bint_t col; 10 | bint_t nchars; 11 | 12 | mark_move_beginning(cur); 13 | 14 | ASSERT("set", MLBUF_OK, mark_set_pcre_capture(&rc, ovector, 6)); 15 | 16 | mark_find_next_re(cur, "age ([0-9]+)", strlen("age ([0-9]+)"), &bline, &col, &nchars); 17 | ASSERT("bline", cur->bline, bline); 18 | ASSERT("col", 18, col); 19 | ASSERT("nchars", 6, nchars); 20 | ASSERT("rc", 2, rc); 21 | ASSERT("vec1a", 18, ovector[0]); 22 | ASSERT("vec1b", 24, ovector[1]); 23 | ASSERT("vec2a", 22, ovector[2]); 24 | ASSERT("vec2b", 24, ovector[3]); 25 | } 26 | -------------------------------------------------------------------------------- /tests/unit/test_mark_swap_with_mark.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | char *str = "hello\nworld"; 4 | 5 | void test(buffer_t *buf, mark_t *cur) { 6 | mark_t *other; 7 | other = buffer_add_mark(buf, NULL, 0); 8 | mark_move_beginning(cur); 9 | mark_move_end(other); 10 | mark_swap(cur, other); 11 | ASSERT("end", buf->first_line->next, cur->bline); 12 | ASSERT("endcol", 5, cur->col); 13 | ASSERT("beg", buf->first_line, other->bline); 14 | ASSERT("begcol", 0, other->col); 15 | } 16 | -------------------------------------------------------------------------------- /tests/unit/test_recalloc.c: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | typedef struct { 4 | int a; 5 | int b; 6 | } thing_t; 7 | 8 | char *str = ""; 9 | 10 | void test(buffer_t *buf, mark_t *cur) { 11 | int i; 12 | int is_all_zero; 13 | thing_t *things; 14 | int nsize = 1024; 15 | 16 | things = calloc(1, sizeof(thing_t)); 17 | things->a = 1; 18 | things->b = 2; 19 | 20 | things = recalloc(things, 1, nsize, sizeof(thing_t)); 21 | ASSERT("preserve_a", 1, things->a); 22 | ASSERT("preserve_b", 2, things->b); 23 | 24 | is_all_zero = 1; 25 | for (i = 1; i < nsize; i++) { 26 | if (things[i].a != 0 || things[i].b != 0) { 27 | is_all_zero = 0; 28 | break; 29 | } 30 | } 31 | ASSERT("zerod", 1, is_all_zero); 32 | 33 | free(things); 34 | } 35 | -------------------------------------------------------------------------------- /uscript.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "mle.h" 3 | 4 | #define MLE_USCRIPT_KEY "_uscript" 5 | 6 | #define MLE_USCRIPT_GET(pl, pu) do { \ 7 | lua_getglobal((pl), MLE_USCRIPT_KEY); \ 8 | (pu) = luaL_optpointer((pl), -1, NULL); \ 9 | if (!(pu)) return 0; \ 10 | } while (0) 11 | 12 | #define luaL_pushkey(pL, pt, pk, pv) do { \ 13 | lua_pushstring((pL), (pk)); \ 14 | (lua_push ## pt) ((pL), (pv)); \ 15 | lua_settable((pL), -3); \ 16 | } while (0) 17 | 18 | #define luaL_pushkey2(pL, pt, pk, pv1, pv2) do { \ 19 | lua_pushstring((pL), (pk)); \ 20 | (lua_push ## pt) ((pL), (pv1), (pv2)); \ 21 | lua_settable((pL), -3); \ 22 | } while (0) 23 | 24 | static int _uscript_panic(lua_State *L); 25 | static int _uscript_cmd_cb(cmd_context_t *ctx); 26 | static int _uscript_observer_cb(char *event_name, void *event_data, void *udata); 27 | static int _uscript_write(lua_State *L); 28 | static void _uscript_push_event_map(uscript_t *uscript, char *event_name, void *event_data); 29 | static void _uscript_push_cmd_map(lua_State *L, cmd_context_t *ctx); 30 | static void _uscript_push_baction_map(lua_State *L, baction_t *baction); 31 | static int _uscript_func_editor_prompt(lua_State *L); 32 | static int _uscript_func_editor_register_cmd(lua_State *L); 33 | static int _uscript_func_editor_register_observer(lua_State *L); 34 | static int _uscript_func_util_escape_shell_arg(lua_State *L); 35 | static int _uscript_func_util_shell_exec(lua_State *L); 36 | static int _uscript_func_editor_get_input(lua_State *L); 37 | static int _uscript_func_editor_menu(lua_State *L); 38 | static int _uscript_func_bview_pop_kmap(lua_State *L); 39 | static int _uscript_func_bview_push_kmap(lua_State *L); 40 | static int _uscript_func_buffer_set_callback(lua_State *L); 41 | static int _uscript_func_buffer_add_srule(lua_State *L); 42 | static int _uscript_func_buffer_remove_srule(lua_State *L); 43 | static int _uscript_func_buffer_write_to_file(lua_State *L); 44 | static void *luaL_checkpointer(lua_State *L, int arg); 45 | static void *luaL_optpointer(lua_State *L, int arg, void *def); 46 | static void lua_pushpointer(lua_State *L, void *ptr); 47 | static int luaL_checkfunction(lua_State *L, int arg); 48 | static int luaopen_mle(lua_State *L); 49 | 50 | #include "uscript.inc.c" 51 | 52 | // Run uscript 53 | uscript_t *uscript_run(editor_t *editor, char *path) { 54 | lua_State *L; 55 | uscript_t *uscript; 56 | L = luaL_newstate(); 57 | luaL_openlibs(L); 58 | luaL_requiref(L, "mle", luaopen_mle, 1); 59 | 60 | uscript = calloc(1, sizeof(uscript_t)); 61 | uscript->editor = editor; 62 | uscript->L = L; 63 | 64 | lua_pushpointer(L, (void*)uscript); 65 | lua_setglobal(L, MLE_USCRIPT_KEY); 66 | lua_pop(L, 1); 67 | 68 | lua_getglobal(L, "_G"); 69 | lua_pushcfunction(L, _uscript_write); 70 | lua_setfield(L, -2, "print"); 71 | lua_pop(L, 1); 72 | 73 | lua_atpanic(L, _uscript_panic); 74 | 75 | 76 | luaL_loadfile(L, path); // TODO err 77 | 78 | lua_pcall(L, 0, 0, 0); 79 | return uscript; 80 | } 81 | 82 | // Destroy uscript 83 | int uscript_destroy(uscript_t *uscript) { 84 | lua_close(uscript->L); 85 | return MLE_OK; 86 | } 87 | 88 | static int _uscript_panic(lua_State *L) { 89 | MLE_SET_ERR(&_editor, "uscript panic: %s", lua_tostring(L, -1)); 90 | return 0; 91 | } 92 | 93 | // Invoke cmd in uscript 94 | static int _uscript_cmd_cb(cmd_context_t *ctx) { 95 | int rv; 96 | lua_State *L; 97 | uhandle_t *uhandle; 98 | int top; 99 | 100 | uhandle = (uhandle_t*)(ctx->cmd->udata); 101 | L = uhandle->uscript->L; 102 | top = lua_gettop(L); 103 | lua_rawgeti(L, LUA_REGISTRYINDEX, uhandle->callback_ref); 104 | _uscript_push_cmd_map(L, ctx); 105 | 106 | if (lua_pcall(L, 1, 1, 0) != LUA_OK) { 107 | printf("err[%s]\n", luaL_checkstring(L, -1)); 108 | rv = MLE_ERR; 109 | } else if (lua_isboolean(L, -1) && !lua_toboolean(L, -1)) { 110 | rv = MLE_ERR; 111 | } else { 112 | rv = MLE_OK; 113 | } 114 | 115 | lua_settop(L, top); 116 | return rv; 117 | } 118 | 119 | static int _uscript_observer_cb(char *event_name, void *event_data, void *udata) { 120 | int rv; 121 | lua_State *L; 122 | uhandle_t *uhandle; 123 | uhandle = (uhandle_t*)(udata); 124 | L = uhandle->uscript->L; 125 | 126 | lua_rawgeti(L, LUA_REGISTRYINDEX, uhandle->callback_ref); 127 | _uscript_push_event_map(uhandle->uscript, event_name, event_data); 128 | 129 | if (lua_pcall(L, 1, 1, 0) != LUA_OK) { 130 | printf("err[%s]\n", luaL_checkstring(L, -1)); 131 | rv = MLE_ERR; 132 | } else if (lua_isboolean(L, -1) && !lua_toboolean(L, -1)) { 133 | rv = MLE_ERR; 134 | } else { 135 | rv = MLE_OK; 136 | } 137 | 138 | return rv; 139 | } 140 | 141 | // Handle write from uscript 142 | static int _uscript_write(lua_State *L) { 143 | uscript_t *uscript; 144 | char *str; 145 | mark_t *mark; 146 | int i, nargs; 147 | nargs = lua_gettop(L); 148 | MLE_USCRIPT_GET(L, uscript); 149 | if (!uscript->editor->active_edit 150 | || !uscript->editor->active_edit->active_cursor 151 | ) { 152 | return 0; 153 | } 154 | mark = uscript->editor->active_edit->active_cursor->mark; 155 | for (i = 1; i <= nargs; i++) { 156 | str = (char*)lua_tostring(L, i); 157 | mark_insert_before(mark, str, strlen(str)); 158 | } 159 | return 0; 160 | } 161 | 162 | static void _uscript_push_event_map(uscript_t *uscript, char *event_name, void *event_data) { 163 | lua_State *L; 164 | L = uscript->L; 165 | if (strcmp(event_name, "buffer:baction") == 0) { 166 | _uscript_push_baction_map(L, (baction_t*)event_data); 167 | return; 168 | } else if (strcmp(event_name, "buffer:save") == 0) { 169 | lua_createtable(L, 0, 1); 170 | luaL_pushkey(L, pointer, "bview", event_data); 171 | return; 172 | } else if (strncmp(event_name, "cmd:", 4) == 0) { 173 | _uscript_push_cmd_map(L, (cmd_context_t*)event_data); 174 | return; 175 | } 176 | lua_pushnil(uscript->L); // TODO 177 | } 178 | 179 | static void _uscript_push_cmd_map(lua_State *L, cmd_context_t *ctx) { 180 | lua_createtable(L, 0, 1); 181 | luaL_pushkey(L, pointer, "editor", (void*)ctx->editor); 182 | luaL_pushkey(L, pointer, "loop_ctx", (void*)ctx->loop_ctx); 183 | luaL_pushkey(L, pointer, "cmd", (void*)ctx->cmd); 184 | luaL_pushkey(L, pointer, "buffer", (void*)ctx->buffer); 185 | luaL_pushkey(L, pointer, "bview", (void*)ctx->bview); 186 | luaL_pushkey(L, pointer, "cursor", (void*)ctx->cursor); 187 | luaL_pushkey(L, pointer, "mark", (void*)ctx->cursor->mark); 188 | luaL_pushkey(L, string, "static_param", (const char*)ctx->static_param); 189 | } 190 | 191 | static void _uscript_push_baction_map(lua_State *L, baction_t *baction) { 192 | lua_createtable(L, 0, 1); 193 | luaL_pushkey(L, integer, "type", baction->type); 194 | luaL_pushkey(L, pointer, "buffer", (void*)baction->buffer); 195 | luaL_pushkey(L, integer, "start_line_index", baction->start_line_index); 196 | luaL_pushkey(L, integer, "start_col", baction->start_col); 197 | luaL_pushkey(L, integer, "maybe_end_line_index", baction->maybe_end_line_index); 198 | luaL_pushkey(L, integer, "maybe_end_col", baction->maybe_end_col); 199 | luaL_pushkey(L, integer, "byte_delta", baction->byte_delta); 200 | luaL_pushkey(L, integer, "char_delta", baction->char_delta); 201 | luaL_pushkey(L, integer, "line_delta", baction->line_delta); 202 | luaL_pushkey2(L, lstring, "data", (const char*)baction->data, baction->data_len); 203 | } 204 | 205 | // foreign static string _uscript_func_editor_prompt(prompt) 206 | static int _uscript_func_editor_prompt(lua_State *L) { 207 | uscript_t *uscript; 208 | char *prompt; 209 | char *answer = NULL; 210 | MLE_USCRIPT_GET(L, uscript); 211 | prompt = (char*)luaL_checkstring(L, 1); 212 | if (editor_prompt(uscript->editor, prompt, NULL, &answer) == MLE_OK && answer) { 213 | lua_pushstring(L, answer); 214 | free(answer); 215 | } else { 216 | lua_pushnil(L); 217 | } 218 | return 1; 219 | } 220 | 221 | int editor_register_cmd(editor_t *editor, cmd_t *cmd); 222 | 223 | // foreign static int _uscript_func_editor_register_cmd(cmd_name, fn_callback) 224 | static int _uscript_func_editor_register_cmd(lua_State *L) { 225 | uscript_t *uscript; 226 | uhandle_t *uhandle; 227 | int rv; 228 | char *cmd_name; 229 | int fn_callback; 230 | cmd_t cmd = {0}; 231 | MLE_USCRIPT_GET(L, uscript); 232 | 233 | cmd_name = (char*)luaL_checkstring(L, 1); // strdup'd by editor_register_cmd 234 | fn_callback = luaL_checkfunction(L, 2); 235 | 236 | uhandle = calloc(1, sizeof(uhandle_t)); 237 | uhandle->uscript = uscript; 238 | uhandle->callback_ref = fn_callback; 239 | DL_APPEND(uscript->uhandles, uhandle); 240 | 241 | cmd.name = cmd_name; 242 | cmd.func = _uscript_cmd_cb; 243 | cmd.udata = (void*)uhandle; 244 | rv = editor_register_cmd(uscript->editor, &cmd); 245 | 246 | lua_createtable(L, 0, 1); 247 | luaL_pushkey(L, integer, "rv", rv); 248 | lua_pushvalue(L, -1); 249 | return 1; 250 | } 251 | 252 | // foreign static int _uscript_func_editor_register_observer(event_name, fn_callback) 253 | static int _uscript_func_editor_register_observer(lua_State *L) { 254 | int rv; 255 | char *event_name; 256 | int fn_callback; 257 | uscript_t *uscript; 258 | uhandle_t *uhandle; 259 | MLE_USCRIPT_GET(L, uscript); 260 | 261 | event_name = (char*)luaL_checkstring(L, 1); 262 | fn_callback = luaL_checkfunction(L, 2); 263 | 264 | uhandle = calloc(1, sizeof(uhandle_t)); 265 | uhandle->uscript = uscript; 266 | uhandle->callback_ref = fn_callback; 267 | DL_APPEND(uscript->uhandles, uhandle); 268 | 269 | rv = editor_register_observer(uscript->editor, event_name, (void*)uhandle, _uscript_observer_cb, NULL); 270 | 271 | lua_createtable(L, 0, 1); 272 | luaL_pushkey(L, integer, "rv", rv); 273 | lua_pushvalue(L, -1); 274 | return 1; 275 | } 276 | 277 | // foreign static int _uscript_func_util_escape_shell_arg(arg) 278 | static int _uscript_func_util_escape_shell_arg(lua_State *L) { 279 | char *arg, *arg_escaped; 280 | uscript_t *uscript; 281 | MLE_USCRIPT_GET(L, uscript); 282 | 283 | arg = (char*)luaL_checkstring(L, 1); 284 | arg_escaped = util_escape_shell_arg(arg, strlen(arg)); 285 | 286 | lua_createtable(L, 0, 1); 287 | luaL_pushkey(L, integer, "rv", 0); 288 | luaL_pushkey2(L, lstring, "output", arg_escaped, strlen(arg_escaped)); 289 | lua_pushvalue(L, -1); 290 | free(arg_escaped); 291 | return 1; 292 | } 293 | 294 | // foreign static int _uscript_func_util_shell_exec(cmd, timeout_s) 295 | static int _uscript_func_util_shell_exec(lua_State *L) { 296 | int rv; 297 | char *cmd; 298 | long timeout_s; 299 | uscript_t *uscript; 300 | char *output; 301 | size_t output_len; 302 | int exit_code; 303 | MLE_USCRIPT_GET(L, uscript); 304 | 305 | cmd = (char*)luaL_checkstring(L, 1); 306 | timeout_s = (long)luaL_checkinteger(L, 2); 307 | output = NULL; 308 | output_len = 0; 309 | exit_code = -1; 310 | rv = util_shell_exec(uscript->editor, cmd, timeout_s, NULL, 0, 0, NULL, &output, &output_len, &exit_code); 311 | 312 | lua_createtable(L, 0, 1); 313 | luaL_pushkey(L, integer, "rv", rv); 314 | luaL_pushkey(L, integer, "exit_code", exit_code); 315 | luaL_pushkey2(L, lstring, "output", (output ? output : ""), output_len); 316 | lua_pushvalue(L, -1); 317 | if (output) free(output); 318 | return 1; 319 | } 320 | 321 | // foreign static int _uscript_func_editor_get_input(x, y, z) 322 | static int _uscript_func_editor_get_input(lua_State *L) { 323 | // TODO 324 | (void)L; 325 | return 0; 326 | } 327 | 328 | // foreign static int _uscript_func_editor_menu(x, y, z) 329 | static int _uscript_func_editor_menu(lua_State *L) { 330 | // TODO 331 | (void)L; 332 | return 0; 333 | } 334 | 335 | // foreign static int _uscript_func_bview_pop_kmap(x, y, z) 336 | static int _uscript_func_bview_pop_kmap(lua_State *L) { 337 | // TODO 338 | (void)L; 339 | return 0; 340 | } 341 | 342 | // foreign static int _uscript_func_bview_push_kmap(x, y, z) 343 | static int _uscript_func_bview_push_kmap(lua_State *L) { 344 | // TODO 345 | (void)L; 346 | return 0; 347 | } 348 | 349 | // foreign static int _uscript_func_buffer_set_callback(x, y, z) 350 | static int _uscript_func_buffer_set_callback(lua_State *L) { 351 | // TODO 352 | (void)L; 353 | return 0; 354 | } 355 | 356 | // foreign static int _uscript_func_buffer_add_srule(x, y, z) 357 | static int _uscript_func_buffer_add_srule(lua_State *L) { 358 | // TODO 359 | (void)L; 360 | return 0; 361 | } 362 | 363 | // foreign static int _uscript_func_buffer_remove_srule(x, y, z) 364 | static int _uscript_func_buffer_remove_srule(lua_State *L) { 365 | // TODO 366 | (void)L; 367 | return 0; 368 | } 369 | 370 | // foreign static int _uscript_func_buffer_write_to_file(x, y, z) 371 | static int _uscript_func_buffer_write_to_file(lua_State *L) { 372 | // TODO 373 | (void)L; 374 | return 0; 375 | } 376 | 377 | static void *luaL_checkpointer(lua_State *L, int arg) { 378 | luaL_checktype(L, arg, LUA_TSTRING); 379 | return luaL_optpointer(L, arg, NULL); 380 | } 381 | 382 | static void *luaL_optpointer(lua_State *L, int arg, void *def) { 383 | const char *ptr; 384 | uintptr_t ptr_as_int; 385 | ptr = luaL_optstring(L, arg, NULL); 386 | if (ptr && strlen(ptr) > 0) { 387 | ptr_as_int = (uintptr_t)strtoull(ptr, NULL, 16); 388 | return (void*)ptr_as_int; 389 | } 390 | return def; 391 | } 392 | 393 | static void lua_pushpointer(lua_State *L, void *ptr) { 394 | char ptrbuf[32]; 395 | if (ptr == NULL) { 396 | lua_pushnil(L); 397 | } else { 398 | snprintf(ptrbuf, 32, "%" PRIxPTR, (uintptr_t)ptr); 399 | lua_pushstring(L, ptrbuf); 400 | } 401 | } 402 | 403 | static int luaL_checkfunction(lua_State *L, int arg) { 404 | luaL_checktype(L, arg, LUA_TFUNCTION); 405 | lua_pushvalue(L, arg); 406 | return luaL_ref(L, LUA_REGISTRYINDEX); 407 | } 408 | 409 | static int luaopen_mle(lua_State *L) { 410 | luaL_newlib(L, mle_lib); 411 | return 1; 412 | } 413 | -------------------------------------------------------------------------------- /uscript.inc.php: -------------------------------------------------------------------------------- 1 | getProtoMap(); 9 | $hardcoded = $this->getHardcodedProtoMap(); 10 | $protos = array_merge($protos, $hardcoded); 11 | usort($protos, function ($a, $b) { 12 | return strcmp($a->name, $b->name); 13 | }); 14 | $this->printFuncs($protos); 15 | $this->printLibTable($protos); 16 | } 17 | 18 | function getProtoMap() { 19 | $mlbuf_h = __DIR__ . '/mlbuf.h'; 20 | $mle_h = __DIR__ . '/mle.h'; 21 | $grep_re = '^\S+ \*?(editor|bview|buffer|cursor|mark)_.*\);$'; 22 | $grep_cmd = sprintf( 23 | 'grep -hP %s %s %s', 24 | escapeshellarg($grep_re), 25 | escapeshellarg($mlbuf_h), 26 | escapeshellarg($mle_h) 27 | ); 28 | $proto_strs = explode("\n", shell_exec($grep_cmd)); 29 | $proto_strs = array_filter($proto_strs, function($proto_str) { 30 | if (strlen(trim($proto_str)) <= 0) { 31 | return false; 32 | } 33 | if (preg_match($this->blacklist_re, $proto_str)) { 34 | return false; 35 | } 36 | return true; 37 | }); 38 | $protos = array_map(function($proto_str) { 39 | return new Proto($proto_str); 40 | }, $proto_strs); 41 | return array_combine( 42 | array_column($protos, 'name'), 43 | $protos 44 | ); 45 | } 46 | 47 | function getHardcodedProtoMap() { 48 | $uscript_c = __DIR__ . '/uscript.c'; 49 | $grep_re = '^// foreign static (?\S+) _uscript_func_(?[^\(]+)\((?[^\)]*)\)$'; 50 | $grep_cmd = sprintf( 51 | 'grep -hP %s %s', 52 | escapeshellarg($grep_re), 53 | escapeshellarg($uscript_c) 54 | ); 55 | $hardcoded_strs = explode("\n", shell_exec($grep_cmd)); 56 | $hardcoded_strs = array_filter($hardcoded_strs); 57 | $protos = array_map(function($hardcoded_str) use ($grep_re) { 58 | $m = null; 59 | if (!preg_match("@{$grep_re}@", $hardcoded_str, $m)) { 60 | throw new RuntimeException("Failed to parse hardcoded proto: $hardcoded_str"); 61 | } 62 | $params = preg_split('@\s*,\s*@', $m['params']); 63 | $params = array_map(function($param) { 64 | return sprintf("void *%s", $param); 65 | }, $params); 66 | $params_str = implode(', ', $params); 67 | $proto_str = sprintf("void %s(%s);", $m['name'], $params_str); 68 | $proto = new Proto($proto_str); 69 | $proto->is_hardcoded = true; 70 | return $proto; 71 | }, $hardcoded_strs); 72 | return array_combine( 73 | array_column($protos, 'name'), 74 | $protos 75 | ); 76 | } 77 | 78 | function printLibTable($protos) { 79 | echo 'static const struct luaL_Reg mle_lib[] = {' . "\n"; 80 | foreach ($protos as $proto) { 81 | printf(' { "%s", %s },' . "\n", $proto->name, $proto->c_func); 82 | } 83 | echo " { NULL, NULL }\n"; 84 | echo "};\n\n"; 85 | } 86 | 87 | function printFuncs($protos) { 88 | foreach ($protos as $proto) { 89 | $this->printFunc($proto); 90 | } 91 | } 92 | 93 | function printFunc($proto) { 94 | $is_hardcoded = $proto->is_hardcoded; 95 | printf( 96 | "%sstatic int %s(lua_State *L) {\n", 97 | $is_hardcoded ? '// ' : '', 98 | $proto->c_func 99 | ); 100 | if (!$is_hardcoded) { 101 | printf(" %s%srv;\n", $proto->ret_type, $proto->ret_is_pointer ? '' : ' '); 102 | foreach ($proto->params as $param) { 103 | if ($param->is_ret) { 104 | printf(" %s%s%s = %s;\n", $param->ret_type, $param->ret_is_pointer ? '' : ' ', $param->name, $param->ret_zero_val); 105 | } else { 106 | printf(" %s%s%s;\n", $param->type, $param->is_pointer ? '' : ' ', $param->name); 107 | } 108 | } 109 | $param_num = 1; 110 | foreach ($proto->params as $param) { 111 | if ($param->is_ret) continue; 112 | $this->fromLua($param, $param_num++); 113 | } 114 | $call_names = array_map(function($param) { 115 | return $param->call_name; 116 | }, $proto->params); 117 | printf(" rv = %s(%s);\n", $proto->name, implode(', ', $call_names)); 118 | printf(" lua_createtable(L, 0, %d);\n", $proto->ret_count); 119 | $this->toLua('rv', $proto->ret_type); 120 | foreach ($proto->params as $param) { 121 | if (!$param->is_ret) continue; 122 | $this->toLua($param->name, $param->ret_type); 123 | } 124 | printf(" lua_pushvalue(L, -1);\n"); 125 | printf(" return 1;\n"); 126 | } 127 | printf("%s}\n\n", $is_hardcoded ? '// ' : ''); 128 | } 129 | 130 | function toLua($name, $type) { 131 | printf(' lua_pushstring(L, "%s");' . "\n", $name); 132 | if (strpos($type, 'int') !== false || $type === 'char') { 133 | printf(" lua_pushinteger(L, (lua_Integer)%s);\n", $name); 134 | } else if ($type === 'char *' || $type === 'const char *') { 135 | printf(" lua_pushstring(L, (const char*)%s);\n", $name); 136 | } else if (preg_match($this->valid_pointer_re, $type)) { 137 | printf(" lua_pushpointer(L, (void*)%s);\n", $name); 138 | } else { 139 | throw new RuntimeException("Unhandled type: $type"); 140 | } 141 | printf(" lua_settable(L, -3);\n"); 142 | } 143 | 144 | function fromLua($param, $slot) { 145 | $type = $param->type; 146 | $name = $param->name; 147 | if (strpos($type, 'int') !== false || $type === 'char' || $type === 'size_t') { 148 | if ($param->is_opt) { 149 | printf(" %s = (%s)luaL_optinteger(L, %d, 0);\n", $name, $type, $slot); 150 | } else { 151 | printf(" %s = (%s)luaL_checkinteger(L, %d);\n", $name, $type, $slot); 152 | } 153 | } else if ($type === 'float' || $type === 'double') { 154 | if ($param->is_opt) { 155 | printf(" %s = (%s)luaL_optnumber(L, %d, 0);\n", $name, $type, $slot); 156 | } else { 157 | printf(" %s = (%s)luaL_checknumber(L, %d);\n", $name, $type, $slot); 158 | } 159 | } else if ($type === 'char *' || $type === 'const char *') { 160 | if ($param->is_opt) { 161 | printf(" %s = (%s)luaL_optstring(L, %d, NULL);\n", $name, $type, $slot); 162 | } else { 163 | printf(" %s = (%s)luaL_checkstring(L, %d);\n", $name, $type, $slot); 164 | } 165 | } else if (preg_match($this->valid_pointer_re, $type)) { 166 | if ($param->is_opt) { 167 | printf(" %s = (%s)luaL_optpointer(L, %d, NULL);\n", $name, $type, $slot); 168 | } else { 169 | printf(" %s = (%s)luaL_checkpointer(L, %d);\n", $name, $type, $slot); 170 | } 171 | } else { 172 | throw new RuntimeException("Unhandled type: $type"); 173 | } 174 | } 175 | } 176 | 177 | class Proto { 178 | function __construct($proto_str) { 179 | $parse_re = '@^(?\S+) (?\*?)(?[^\(]+)\((?.*?)\);$@'; 180 | $match = []; 181 | if (!preg_match($parse_re, $proto_str, $match)) { 182 | throw new RuntimeException("Could not parse proto: $proto_str"); 183 | } 184 | $this->name = $match['name']; 185 | $this->c_func = '_uscript_func_' . $this->name; 186 | $this->ret_type = trim($match['ret_type'] . ' ' . $match['ret_type_ptr']); 187 | $this->ret_is_pointer = substr($this->ret_type, -1) === '*'; 188 | $this->params = []; 189 | $this->ret_count = 1; 190 | $this->has_funcs = false; 191 | $this->is_hardcoded = false; 192 | if (empty($match['params'])) { 193 | return; 194 | } 195 | $param_strs = preg_split('@\s*,\s*@', $match['params']); 196 | foreach ($param_strs as $param_str) { 197 | $param = new Param($param_str); 198 | $this->params[] = $param; 199 | if ($param->is_ret) { 200 | $this->ret_count += 1; 201 | } 202 | if ($param->is_func) { 203 | $this->has_funcs = true; 204 | } 205 | } 206 | } 207 | } 208 | 209 | class Param { 210 | function __construct($param_str) { 211 | $parts = preg_split('@\s+@', $param_str); 212 | $name_w_ptr = current(array_slice($parts, -1)); 213 | $type_wout_ptr = implode(' ', array_slice($parts, 0, -1)); 214 | $ptr_i = 0; 215 | while (substr($name_w_ptr, $ptr_i, 1) === '*') $ptr_i += 1; 216 | $ptr = substr($name_w_ptr, 0, $ptr_i); 217 | $this->type = trim($type_wout_ptr . ' ' . $ptr); 218 | $this->name = substr($name_w_ptr, $ptr_i); 219 | $this->is_func = preg_match('@^fn_@', $this->name); 220 | $this->is_opt = preg_match('@^opt(ret)?_@', $this->name); 221 | $this->is_ret = preg_match('@^(opt)?ret_@', $this->name); 222 | $this->is_optret = preg_match('@^optret_@', $this->name); 223 | $this->is_pointer = strpos($this->type, '*') !== false; 224 | $this->is_string = $this->type === 'char *'; 225 | if ($this->is_ret) { 226 | if (!$this->is_pointer) { 227 | throw new RuntimeException("Expected ret param ptr: $param_str"); 228 | } 229 | $this->ret_type = trim(substr($this->type, 0, -1)); 230 | $this->ret_is_pointer = strpos($this->ret_type, '*') !== false; 231 | $this->ret_zero_val = $this->ret_is_pointer ? 'NULL' : '0'; 232 | } else { 233 | $this->ret_type = null; 234 | $this->ret_is_pointer = false; 235 | $this->ret_zero_val = '0'; 236 | } 237 | $this->call_name = ($this->is_ret ? '&' : '') . $this->name; 238 | } 239 | } 240 | 241 | (new CodeGen())->run(); 242 | -------------------------------------------------------------------------------- /uscript.lua: -------------------------------------------------------------------------------- 1 | -- mle -N -x uscript.lua -K lua_kmap,,1 -k cmd_lua_test,f11, -n lua_kmap 2 | 3 | mle.editor_register_observer("buffer:save", function (bview) 4 | r = mle.util_shell_exec("ls", -1) 5 | print("ls " .. r["output"]) 6 | end) 7 | 8 | mle.editor_register_cmd("cmd_lua_test", function (ctx) 9 | name = mle.editor_prompt("Enter your name") 10 | if name then 11 | print("hello <" .. name .. "> from lua") 12 | else 13 | print("you hit cancel") 14 | end 15 | end) 16 | 17 | mle.editor_register_observer("cmd:cmd_copy:before", function (ctx) 18 | local rv = mle.cursor_get_anchor(ctx["cursor"]) 19 | local anchor = rv["ret_anchor"] 20 | 21 | rv = mle.mark_get_between(ctx["mark"], anchor) 22 | rv = mle.util_escape_shell_arg(rv["ret_str"]) 23 | 24 | mle.util_shell_exec("echo -n " .. rv["output"] .. " | xclip -sel c >/dev/null", 1) 25 | end) 26 | -------------------------------------------------------------------------------- /util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "mle.h" 15 | 16 | static const unsigned char utf8_mask[6] = {0x7f, 0x1f, 0x0f, 0x07, 0x03, 0x01}; 17 | 18 | // Run a shell command, optionally feeding stdin, collecting stdout 19 | // Specify timeout_s=-1 for no timeout 20 | int util_shell_exec(editor_t *editor, char *cmd, long timeout_s, char *input, size_t input_len, int setsid, char *opt_shell, char **optret_output, size_t *optret_output_len, int *optret_exit_code) { 21 | // TODO clean this crap up 22 | int rv; 23 | int do_read; 24 | int do_write; 25 | int readfd; 26 | int writefd; 27 | ssize_t rc; 28 | ssize_t nbytes; 29 | fd_set readfds; 30 | struct timeval timeout; 31 | struct timeval *timeoutptr; 32 | pid_t pid; 33 | int exit_status; 34 | str_t readbuf = {0}; 35 | 36 | do_read = optret_output != NULL ? 1 : 0; 37 | do_write = input && input_len > 0 ? 1 : 0; 38 | readfd = -1; 39 | writefd = -1; 40 | pid = -1; 41 | readbuf.inc = -2; // double capacity on each allocation 42 | rv = MLE_OK; 43 | nbytes = 0; 44 | if (do_read) { 45 | *optret_output = NULL; 46 | *optret_output_len = 0; 47 | } 48 | 49 | // Open cmd 50 | if (!util_popen2( 51 | cmd, 52 | setsid, 53 | opt_shell, 54 | do_read ? &readfd : NULL, 55 | do_write ? &writefd : NULL, 56 | &pid 57 | )) { 58 | MLE_RETURN_ERR(editor, "Failed to exec shell cmd: %s", cmd); 59 | } 60 | 61 | // Read-write loop 62 | do { 63 | // Write to shell cmd if input is remaining 64 | if (do_write && writefd >= 0) { 65 | rc = write(writefd, input, input_len); 66 | if (rc > 0) { 67 | input += rc; 68 | input_len -= rc; 69 | if (input_len < 1) { 70 | close(writefd); 71 | writefd = -1; 72 | } 73 | } else { 74 | // write err 75 | MLE_SET_ERR(editor, "write error: %s", strerror(errno)); 76 | rv = MLE_ERR; 77 | break; 78 | } 79 | } 80 | 81 | // Read shell cmd, timing out after timeout_sec 82 | if (do_read) { 83 | if (timeout_s >= 0) { 84 | timeout.tv_sec = timeout_s; 85 | timeout.tv_usec = 0; 86 | timeoutptr = &timeout; 87 | } else { 88 | timeoutptr = NULL; 89 | } 90 | FD_ZERO(&readfds); 91 | FD_SET(readfd, &readfds); 92 | rc = select(readfd + 1, &readfds, NULL, NULL, timeoutptr); 93 | if (rc < 0) { 94 | // Err on select 95 | MLE_SET_ERR(editor, "select error: %s", strerror(errno)); 96 | rv = MLE_ERR; 97 | break; 98 | } else if (rc == 0) { 99 | // Timed out 100 | rv = MLE_ERR; 101 | break; 102 | } else { 103 | // Read a kilobyte of data 104 | str_ensure_cap(&readbuf, readbuf.len + 1024); 105 | nbytes = read(readfd, readbuf.data + readbuf.len, 1024); 106 | if (nbytes < 0) { 107 | // read err or EAGAIN/EWOULDBLOCK 108 | MLE_SET_ERR(editor, "read error: %s", strerror(errno)); 109 | rv = MLE_ERR; 110 | break; 111 | } else if (nbytes > 0) { 112 | // Got data 113 | readbuf.len += nbytes; 114 | } 115 | } 116 | } 117 | } while(nbytes > 0); // until EOF 118 | 119 | // Close pipes and reap child proc 120 | if (readfd >= 0) close(readfd); 121 | if (writefd >= 0) close(writefd); 122 | exit_status = -1; 123 | waitpid(pid, &exit_status, do_read ? WNOHANG : 0); 124 | if (optret_exit_code) *optret_exit_code = WEXITSTATUS(exit_status); 125 | 126 | if (do_read) { 127 | *optret_output = readbuf.data; 128 | *optret_output_len = readbuf.len; 129 | } 130 | 131 | // Force redraw to correct artifacts from child process writing to stderr. 132 | // Piping stderr to `/dev/null` in the child process would be cleaner, 133 | // however that breaks interactive apps like less(1) which require stdio 134 | // ttys to behave properly. 135 | if (!_editor.headless_mode) editor_force_redraw(&_editor); 136 | 137 | return rv; 138 | } 139 | 140 | // Like popen, but more control over pipes. Returns 1 on success, 0 on failure. 141 | int util_popen2(char *cmd, int do_setsid, char *opt_shell, int *optret_fdread, int *optret_fdwrite, pid_t *optret_pid) { 142 | pid_t pid; 143 | int do_read; 144 | int do_write; 145 | int pout[2]; 146 | int pin[2]; 147 | 148 | // Set r/w 149 | do_read = optret_fdread != NULL ? 1 : 0; 150 | do_write = optret_fdwrite != NULL ? 1 : 0; 151 | 152 | // Set shell 153 | opt_shell = opt_shell ? opt_shell : "sh"; 154 | 155 | // Make pipes 156 | if (do_read) if (pipe(pout)) return 0; 157 | if (do_write) if (pipe(pin)) return 0; 158 | 159 | // Fork 160 | pid = fork(); 161 | if (pid < 0) { 162 | // Fork failed 163 | return 0; 164 | } else if (pid == 0) { 165 | // Child 166 | if (do_read) { 167 | close(pout[0]); 168 | dup2(pout[1], STDOUT_FILENO); 169 | close(pout[1]); 170 | } 171 | if (do_write) { 172 | close(pin[1]); 173 | dup2(pin[0], STDIN_FILENO); 174 | close(pin[0]); 175 | } 176 | if (do_setsid) setsid(); 177 | execlp(opt_shell, opt_shell, "-c", cmd, NULL); 178 | exit(EXIT_FAILURE); 179 | } 180 | // Parent 181 | if (do_read) { 182 | close(pout[1]); 183 | *optret_fdread = pout[0]; 184 | } 185 | if (do_write) { 186 | close(pin[0]); 187 | *optret_fdwrite = pin[1]; 188 | } 189 | if (optret_pid) *optret_pid = pid; 190 | return 1; 191 | } 192 | 193 | // Return paired bracket if ch is a bracket, else return 0 194 | int util_get_bracket_pair(uint32_t ch, int *optret_is_closing) { 195 | switch (ch) { 196 | case '[': if (optret_is_closing) *optret_is_closing = 0; return ']'; 197 | case '(': if (optret_is_closing) *optret_is_closing = 0; return ')'; 198 | case '{': if (optret_is_closing) *optret_is_closing = 0; return '}'; 199 | case ']': if (optret_is_closing) *optret_is_closing = 1; return '['; 200 | case ')': if (optret_is_closing) *optret_is_closing = 1; return '('; 201 | case '}': if (optret_is_closing) *optret_is_closing = 1; return '{'; 202 | default: return 0; 203 | } 204 | return 0; 205 | } 206 | 207 | // Return 1 if path is file 208 | int util_is_file(char *path, char *opt_mode, FILE **optret_file) { 209 | struct stat sb; 210 | if (stat(path, &sb) != 0 || !S_ISREG(sb.st_mode)) return 0; 211 | if (opt_mode && optret_file) { 212 | *optret_file = fopen(path, opt_mode); 213 | if (!*optret_file) return 0; 214 | } 215 | return 1; 216 | } 217 | 218 | // Return 1 if path is dir 219 | int util_is_dir(char *path) { 220 | struct stat sb; 221 | if (stat(path, &sb) != 0 || !S_ISDIR(sb.st_mode)) return 0; 222 | return 1; 223 | } 224 | 225 | // Return 1 if re matches subject 226 | int util_pcre_match(char *re, char *subject, int subject_len, char **optret_capture, int *optret_capture_len) { 227 | int rc; 228 | pcre2_code *cre; 229 | int errcode; 230 | PCRE2_SIZE erroffset; 231 | PCRE2_SIZE ovector[3]; 232 | cre = pcre2_compile((PCRE2_SPTR)re, (PCRE2_SIZE)strlen(re), (optret_capture ? 0 : PCRE2_NO_AUTO_CAPTURE) | PCRE2_CASELESS, &errcode, &erroffset, NULL); 233 | if (!cre) return 0; 234 | rc = pcre2_match(cre, (PCRE2_SPTR)subject, (PCRE2_SIZE)subject_len, 0, 0, pcre2_md, NULL); 235 | memcpy(ovector, pcre2_get_ovector_pointer(pcre2_md), 3 * sizeof(PCRE2_SIZE)); 236 | pcre2_code_free(cre); 237 | if (optret_capture) { 238 | if (rc >= 0) { 239 | *optret_capture = subject + ovector[0]; 240 | *optret_capture_len = ovector[1] - ovector[0]; 241 | } else { 242 | *optret_capture = NULL; 243 | *optret_capture_len = 0; 244 | } 245 | } 246 | return rc >= 0 ? 1 : 0; 247 | } 248 | 249 | // Perform a regex replace with back-references. Return number of replacements 250 | // made. If regex is invalid, `ret_result` is set to NULL, `ret_result_len` is 251 | // set to 0 and 0 is returned. 252 | int util_pcre_replace(char *re, char *subj, char *repl, char **ret_result, int *ret_result_len) { 253 | int rc; 254 | pcre2_code *cre; 255 | int errcode; 256 | PCRE2_SIZE erroffset; 257 | int subj_offset; 258 | int subj_offset_z; 259 | int subj_len; 260 | int subj_look_offset; 261 | int last_look_offset; 262 | PCRE2_SIZE ovector[30]; 263 | int num_repls; 264 | int got_match = 0; 265 | str_t result = {0}; 266 | 267 | *ret_result = NULL; 268 | *ret_result_len = 0; 269 | 270 | // Compile regex 271 | cre = pcre2_compile((PCRE2_SPTR)re, (PCRE2_SIZE)strlen(re), PCRE2_CASELESS, &errcode, &erroffset, NULL); 272 | if (!cre) return 0; 273 | 274 | // Start match-replace loop 275 | num_repls = 0; 276 | subj_len = strlen(subj); 277 | subj_offset = 0; 278 | subj_look_offset = 0; 279 | last_look_offset = 0; 280 | while (subj_offset < subj_len) { 281 | // Find match 282 | rc = pcre2_match(cre, (PCRE2_SPTR)subj, (PCRE2_SIZE)subj_len, (PCRE2_SIZE)subj_look_offset, 0, pcre2_md, NULL); 283 | memcpy(ovector, pcre2_get_ovector_pointer(pcre2_md), 30 * sizeof(PCRE2_SIZE)); 284 | if (rc < 0 || ovector[0] == PCRE2_UNSET) { 285 | got_match = 0; 286 | subj_offset_z = subj_len; 287 | } else { 288 | got_match = 1; 289 | subj_offset_z = ovector[0]; 290 | } 291 | 292 | // Append part before match 293 | str_append_stop(&result, subj + subj_offset, subj + subj_offset_z); 294 | subj_offset = ovector[1]; 295 | subj_look_offset = subj_offset + (subj_offset > last_look_offset ? 0 : 1); // Prevent infinite loop 296 | last_look_offset = subj_look_offset; 297 | 298 | // Break if no match 299 | if (!got_match) break; 300 | 301 | // Append replacements with backrefs 302 | str_append_replace_with_backrefs(&result, subj, repl, rc, ovector, 30); 303 | 304 | // Increment num_repls 305 | num_repls += 1; 306 | } 307 | 308 | // Free regex 309 | pcre2_code_free(cre); 310 | 311 | // Return result 312 | *ret_result = result.data ? result.data : strdup(""); 313 | *ret_result_len = result.len; 314 | 315 | // Return number of replacements 316 | return num_repls; 317 | } 318 | 319 | // Return 1 if a > b, else return 0. 320 | int util_timeval_is_gt(struct timeval *a, struct timeval *b) { 321 | if (a->tv_sec > b->tv_sec) { 322 | return 1; 323 | } else if (a->tv_sec == b->tv_sec) { 324 | return a->tv_usec > b->tv_usec ? 1 : 0; 325 | } 326 | return 0; 327 | } 328 | 329 | // Ported from php_escape_shell_arg 330 | // https://github.com/php/php-src/blob/master/ext/standard/exec.c 331 | char *util_escape_shell_arg(char *str, int l) { 332 | int x, y = 0; 333 | char *cmd; 334 | 335 | cmd = malloc(4 * l + 3); // worst case 336 | 337 | cmd[y++] = '\''; 338 | 339 | for (x = 0; x < l; x++) { 340 | int mb_len = tb_utf8_char_length(*(str + x)); 341 | 342 | // skip non-valid multibyte characters 343 | if (mb_len < 0) { 344 | continue; 345 | } else if (mb_len > 1) { 346 | memcpy(cmd + y, str + x, mb_len); 347 | y += mb_len; 348 | x += mb_len - 1; 349 | continue; 350 | } 351 | 352 | switch (str[x]) { 353 | case '\'': 354 | cmd[y++] = '\''; 355 | cmd[y++] = '\\'; 356 | cmd[y++] = '\''; 357 | // fall-through 358 | default: 359 | cmd[y++] = str[x]; 360 | } 361 | } 362 | cmd[y++] = '\''; 363 | cmd[y] = '\0'; 364 | 365 | return cmd; 366 | } 367 | 368 | // Attempt to replace leading ~/ with $HOME 369 | void util_expand_tilde(char *path, int path_len, char **ret_path, int *optret_path_len) { 370 | char *homedir; 371 | char *newpath; 372 | if (!util_is_file("~", NULL, NULL) 373 | && strncmp(path, "~/", 2) == 0 374 | && (homedir = getenv("HOME")) != NULL 375 | ) { 376 | newpath = malloc(strlen(homedir) + 1 + (path_len - 2) + 1); 377 | sprintf(newpath, "%s/%.*s", homedir, path_len-2, path+2); 378 | *ret_path = newpath; 379 | if (optret_path_len) *optret_path_len = strlen(*ret_path); 380 | return; 381 | } 382 | *ret_path = strndup(path, path_len); 383 | if (optret_path_len) *optret_path_len = strlen(*ret_path); 384 | } 385 | 386 | // Adapted from termbox src/demo/keyboard.c 387 | int tb_printf_rect(bview_rect_t rect, int x, int y, uint16_t fg, uint16_t bg, const char *fmt, ...) { 388 | char buf[4096]; 389 | va_list vl; 390 | va_start(vl, fmt); 391 | vsnprintf(buf, sizeof(buf), fmt, vl); 392 | va_end(vl); 393 | return tb_print(rect.x + x, rect.y + y, fg ? fg : rect.fg, bg ? bg : rect.bg, buf); 394 | } 395 | 396 | // Like tb_printf, but accepts @fg,bg; attributes inside the string. To print 397 | // a literal '@', use '@@' in the format string. Specify fg or bg of 0 to 398 | // reset that attribute. 399 | int tb_printf_attr(bview_rect_t rect, int x, int y, const char *fmt, ...) { 400 | char bufo[4096]; 401 | char *buf, *bufstop; 402 | int fg, bg, tfg, tbg, c, buflen; 403 | uint32_t uni; 404 | 405 | va_list vl; 406 | va_start(vl, fmt); 407 | buflen = vsnprintf(bufo, sizeof(bufo), fmt, vl); 408 | buflen = MLBUF_MIN(buflen, (int)(sizeof(bufo) - 1)); 409 | va_end(vl); 410 | 411 | 412 | fg = rect.fg; 413 | bg = rect.bg; 414 | x = rect.x + x; 415 | y = rect.y + y; 416 | 417 | c = 0; 418 | buf = bufo; 419 | bufstop = buf + buflen; 420 | while (*buf) { 421 | buf += utf8_char_to_unicode(&uni, buf, bufstop); 422 | if (uni == '@') { 423 | if (!*buf) break; 424 | utf8_char_to_unicode(&uni, buf, bufstop); 425 | if (uni != '@') { 426 | tfg = strtol(buf, &buf, 10); 427 | if (!*buf) break; 428 | utf8_char_to_unicode(&uni, buf, bufstop); 429 | if (uni == ',') { 430 | buf++; 431 | if (!*buf) break; 432 | tbg = strtol(buf, &buf, 10); 433 | fg = tfg <= 0 ? rect.fg : tfg; 434 | bg = tbg <= 0 ? rect.bg : tbg; 435 | if (!*buf) break; 436 | utf8_char_to_unicode(&uni, buf, bufstop); 437 | if (uni == ';') buf++; 438 | continue; 439 | } 440 | } 441 | } 442 | tb_set_cell(x, y, uni, fg, bg); 443 | x++; 444 | c++; 445 | } 446 | return c; 447 | } 448 | 449 | 450 | // Zero-fill realloc 451 | void *recalloc(void *ptr, size_t orig_num, size_t new_num, size_t el_size) { 452 | void *newptr; 453 | newptr = realloc(ptr, new_num * el_size); 454 | if (!newptr) return NULL; 455 | if (new_num > orig_num) { 456 | memset(newptr + (orig_num * el_size), 0, (new_num - orig_num) * el_size); 457 | } 458 | return newptr; 459 | } 460 | 461 | // Append from data up until data_stop to str 462 | void str_append_stop(str_t *str, char *data, char *data_stop) { 463 | size_t data_len; 464 | data_len = data_stop >= data ? data_stop - data : 0; 465 | str_append_len(str, data, data_len); 466 | } 467 | 468 | // Append data to str 469 | void str_append(str_t *str, char *data) { 470 | str_append_len(str, data, strlen(data)); 471 | } 472 | 473 | // Append data_len bytes of data to str 474 | void str_append_len(str_t *str, char *data, size_t data_len) { 475 | str_put_len(str, data, data_len, 0); 476 | } 477 | 478 | // Append char to str 479 | void str_append_char(str_t *str, char c) { 480 | str_put_len(str, &c, 1, 0); 481 | } 482 | 483 | // Prepend from data up until data_stop to str 484 | void str_prepend_stop(str_t *str, char *data, char *data_stop) { 485 | size_t data_len; 486 | data_len = data_stop >= data ? data_stop - data : 0; 487 | str_prepend_len(str, data, data_len); 488 | } 489 | 490 | // Prepend data to str 491 | void str_prepend(str_t *str, char *data) { 492 | str_prepend_len(str, data, strlen(data)); 493 | } 494 | 495 | // Prepend data_len bytes of data to str 496 | void str_prepend_len(str_t *str, char *data, size_t data_len) { 497 | str_put_len(str, data, data_len, 1); 498 | } 499 | 500 | // Set str to data 501 | void str_set(str_t *str, char *data) { 502 | str_set_len(str, data, strlen(data)); 503 | } 504 | 505 | // Set str to data for data_len bytes 506 | void str_set_len(str_t *str, char *data, size_t data_len) { 507 | str_ensure_cap(str, data_len+1); 508 | memcpy(str->data, data, data_len); 509 | str->len = data_len; 510 | *(str->data + str->len) = '\0'; 511 | } 512 | 513 | // Sprintf to str 514 | void str_sprintf(str_t *str, const char *fmt, ...) { 515 | va_list va; 516 | int len; 517 | 518 | va_start(va, fmt); 519 | len = vsnprintf(NULL, 0, fmt, va); 520 | va_end(va); 521 | 522 | str_ensure_cap(str, str->len + (size_t)len + 1); 523 | 524 | va_start(va, fmt); 525 | vsprintf(str->data + str->len, fmt, va); 526 | va_end(va); 527 | 528 | str->len += (size_t)len; 529 | } 530 | 531 | // Append/prepend data_len bytes of data to str 532 | void str_put_len(str_t *str, char *data, size_t data_len, int is_prepend) { 533 | size_t req_cap; 534 | req_cap = str->len + data_len + 1; 535 | 536 | str_ensure_cap(str, req_cap); 537 | 538 | if (is_prepend) { 539 | memmove(str->data + data_len, str->data, str->len); 540 | memcpy(str->data, data, data_len); 541 | } else { 542 | memcpy(str->data + str->len, data, data_len); 543 | } 544 | str->len += data_len; 545 | *(str->data + str->len) = '\0'; 546 | } 547 | 548 | // Ensure space in str 549 | void str_ensure_cap(str_t *str, size_t cap) { 550 | if (!str->data || cap > str->cap) { 551 | if (str->inc >= 0) { 552 | // If inc is positive, grow linearly 553 | cap = MLBUF_MAX(cap, str->cap + (str->inc > 0 ? str->inc : 128)); 554 | } else { 555 | // If inc is negative, grow geometrically 556 | cap = MLBUF_MAX(cap, str->cap * (str->inc <= -2 ? str->inc * -1 : 2)); 557 | } 558 | str->data = realloc(str->data, cap); 559 | str->cap = cap; 560 | } 561 | } 562 | 563 | // Clear str 564 | void str_clear(str_t *str) { 565 | str->len = 0; 566 | } 567 | 568 | // Free str 569 | void str_free(str_t *str) { 570 | if (str->data) free(str->data); 571 | memset(str, 0, sizeof(str_t)); 572 | } 573 | 574 | // Replace `repl` in `subj` and append result to `str`. PCRE style backrefs are 575 | // supported. 576 | // 577 | // str where to append data 578 | // subj subject string 579 | // repl replacement string with $1 or \1 style backrefs 580 | // pcre_rc return code from pcre_exec 581 | // pcre_ovector ovector used with pcre_exec 582 | // pcre_ovecsize size of pcre_ovector 583 | // 584 | void str_append_replace_with_backrefs(str_t *str, char *subj, char *repl, int pcre_rc, PCRE2_SIZE *pcre_ovector, int pcre_ovecsize) { 585 | char *repl_stop; 586 | char *repl_cur; 587 | char *repl_z; 588 | char *repl_backref; 589 | int repl_delta; 590 | int ibackref; 591 | char *term; 592 | char *term_stop; 593 | char hex[3]; 594 | char byte; 595 | 596 | repl_stop = repl + strlen(repl); 597 | 598 | // Start replace loop 599 | repl_cur = repl; 600 | while (repl_cur < repl_stop) { 601 | // Find backref marker (dollar sign or backslash) in replacement str 602 | repl_backref = strpbrk(repl_cur, "$\\"); 603 | repl_z = repl_backref ? repl_backref : repl_stop; 604 | 605 | // Append part before backref 606 | str_append_stop(str, repl_cur, repl_z); 607 | 608 | // Break if no backref 609 | if (!repl_backref) break; 610 | 611 | // Append backref 612 | term = NULL; 613 | repl_delta = 2; // marker + backref symbol 614 | if (repl_backref+1 >= repl_stop) { 615 | // No data after backref marker; append the marker itself 616 | term = repl_backref; 617 | term_stop = repl_stop; 618 | } else if (*(repl_backref+1) >= '0' && *(repl_backref+1) <= '9') { 619 | // $N; append Nth captured substring from match 620 | ibackref = *(repl_backref+1) - '0'; 621 | if (ibackref < pcre_rc && ibackref < pcre_ovecsize/3) { 622 | // Backref exists 623 | term = subj + pcre_ovector[ibackref*2]; 624 | term_stop = subj + pcre_ovector[ibackref*2 + 1]; 625 | } else { 626 | // Backref does not exist; append marker + whatever character it was 627 | term = repl_backref; 628 | term_stop = term + tb_utf8_char_length(*(term+1)); 629 | } 630 | } else if (*(repl_backref+1) == 'n') { 631 | // $n; append newline 632 | term = "\n"; 633 | term_stop = term + 1; 634 | } else if (*(repl_backref+1) == 't') { 635 | // $t; append tab 636 | term = "\t"; 637 | term_stop = term + 1; 638 | } else if (*(repl_backref+1) == 'x' && repl_backref+3 < repl_stop) { 639 | // $xNN; append byte 640 | strncpy(hex, repl_backref+2, 2); 641 | hex[2] = '\0'; 642 | byte = strtoul(hex, NULL, 16); 643 | term = &byte; 644 | term_stop = term + 1; 645 | repl_delta = 4; // marker + 'x' + d1 + d2 646 | } else { 647 | // $* (not number or 'n'); append marker + whatever character it was 648 | term = repl_backref; 649 | term_stop = term + tb_utf8_char_length(*(term+1)); 650 | } 651 | str_append_stop(str, term, term_stop); 652 | 653 | // Advance repl_cur by repl_delta bytes 654 | repl_cur = repl_backref + repl_delta; 655 | } 656 | } 657 | 658 | // Return a new aproc_t 659 | aproc_t *aproc_new(editor_t *editor, void *owner, aproc_t **owner_aproc, char *shell_cmd, int rw, aproc_cb_t callback) { 660 | aproc_t *aproc; 661 | aproc = calloc(1, sizeof(aproc_t)); 662 | aproc->editor = editor; 663 | aproc_set_owner(aproc, owner, owner_aproc); 664 | if (rw) { 665 | if (!util_popen2(shell_cmd, 0, NULL, &aproc->rfd, &aproc->wfd, &aproc->pid)) { 666 | goto aproc_new_failure; 667 | } 668 | aproc->rpipe = fdopen(aproc->rfd, "r"); 669 | aproc->wpipe = fdopen(aproc->wfd, "w"); 670 | } else { 671 | if (!(aproc->rpipe = popen(shell_cmd, "r"))) { 672 | goto aproc_new_failure; 673 | } 674 | aproc->rfd = fileno(aproc->rpipe); 675 | } 676 | setvbuf(aproc->rpipe, NULL, _IONBF, 0); 677 | if (aproc->wpipe) setvbuf(aproc->wpipe, NULL, _IONBF, 0); 678 | aproc->callback = callback; 679 | DL_APPEND(editor->aprocs, aproc); 680 | return aproc; 681 | 682 | aproc_new_failure: 683 | free(aproc); 684 | return NULL; 685 | } 686 | 687 | // Set aproc owner 688 | int aproc_set_owner(aproc_t *aproc, void *owner, aproc_t **owner_aproc) { 689 | if (aproc->owner_aproc) { 690 | *aproc->owner_aproc = NULL; 691 | } 692 | *owner_aproc = aproc; 693 | aproc->owner = owner; 694 | aproc->owner_aproc = owner_aproc; 695 | return MLE_OK; 696 | } 697 | 698 | // Destroy an aproc_t 699 | int aproc_destroy(aproc_t *aproc, int preempt) { 700 | DL_DELETE(aproc->editor->aprocs, aproc); 701 | if (aproc->owner_aproc) *aproc->owner_aproc = NULL; 702 | if (preempt) { 703 | if (aproc->rfd) close(aproc->rfd); 704 | if (aproc->wfd) close(aproc->wfd); 705 | if (aproc->pid) kill(aproc->pid, SIGTERM); 706 | } 707 | if (aproc->rpipe) pclose(aproc->rpipe); 708 | if (aproc->wpipe) pclose(aproc->wpipe); 709 | free(aproc); 710 | return MLE_OK; 711 | } 712 | 713 | // Manage async procs, giving priority to user input. Return 1 if drain should 714 | // be called again, else return 0. 715 | int aproc_drain_all(aproc_t *aprocs, int *ttyfd) { 716 | int maxfd; 717 | fd_set readfds; 718 | aproc_t *aproc; 719 | aproc_t *aproc_tmp; 720 | char buf[1024 + 1]; 721 | ssize_t nbytes; 722 | int rc; 723 | 724 | // Exit early if no aprocs 725 | if (!aprocs) return 0; 726 | 727 | // Open ttyfd if not already open 728 | if (!*ttyfd) { 729 | if ((*ttyfd = open("/dev/tty", O_RDONLY)) < 0) { 730 | // TODO error 731 | return 0; 732 | } 733 | } 734 | 735 | // Add tty to readfds 736 | FD_ZERO(&readfds); 737 | FD_SET(*ttyfd, &readfds); 738 | 739 | // Add async procs to readfds 740 | // Simultaneously check for solo, which takes precedence over everything 741 | maxfd = *ttyfd; 742 | DL_FOREACH(aprocs, aproc) { 743 | FD_SET(aproc->rfd, &readfds); 744 | if (aproc->rfd > maxfd) maxfd = aproc->rfd; 745 | } 746 | 747 | // Perform select 748 | rc = select(maxfd + 1, &readfds, NULL, NULL, NULL); 749 | if (rc < 0) { 750 | return 0; // TODO error 751 | } else if (rc == 0) { 752 | return 1; // Nothing to read, call again 753 | } 754 | 755 | if (FD_ISSET(*ttyfd, &readfds)) { 756 | // Immediately give priority to user input 757 | return 0; 758 | } else { 759 | // Read async procs 760 | DL_FOREACH_SAFE(aprocs, aproc, aproc_tmp) { 761 | // Read and invoke callback 762 | if (FD_ISSET(aproc->rfd, &readfds)) { 763 | nbytes = read(aproc->rfd, &buf, 1024); 764 | buf[nbytes] = '\0'; 765 | if (nbytes == 0) aproc->is_done = 1; 766 | aproc->callback(aproc, buf, nbytes); 767 | } 768 | // Destroy on eof. 769 | // Not sure if ferror and feof have any effect here given we're not 770 | // using fread. 771 | if (ferror(aproc->rpipe) || feof(aproc->rpipe) || aproc->is_done) { 772 | aproc_destroy(aproc, 0); 773 | } 774 | } 775 | } 776 | 777 | return 1; 778 | } 779 | 780 | size_t utf8_str_length(char *data, size_t len) { 781 | size_t slen; 782 | char *data_stop, *c; 783 | data_stop = data + len; 784 | c = data; 785 | slen = 0; 786 | while (c < data_stop) { 787 | c += tb_utf8_char_length(*c); 788 | slen += 1; 789 | } 790 | return slen; 791 | } 792 | 793 | // Like tb_utf8_char_to_unicode but obeys `stop` and returns U+FFFD if invalid 794 | int utf8_char_to_unicode(uint32_t *out, const char *c, const char *stop) { 795 | if (*c == '\0') return 0; 796 | 797 | int i; 798 | unsigned char len = tb_utf8_char_length(*c); 799 | unsigned char mask = utf8_mask[len - 1]; 800 | uint32_t result = c[0] & mask; 801 | for (i = 1; i < len && (c + i) < stop; ++i) { 802 | result <<= 6; 803 | result |= c[i] & 0x3f; 804 | } 805 | 806 | if (i != len) { 807 | result = 0xfffd; // replace incomplete code point with replacement char 808 | len = i; 809 | } 810 | 811 | *out = result; 812 | return (int)len; 813 | } 814 | -------------------------------------------------------------------------------- /vendor/Makefile: -------------------------------------------------------------------------------- 1 | lua_cflags:=-std=c99 -Wall -Wextra -g -O3 -DLUA_USE_POSIX $(CFLAGS) 2 | lua_objects:=$(patsubst lua/%.c,lua/%.o,$(filter-out lua/lua.c lua/onelua.c, $(wildcard lua/*.c))) 3 | 4 | pcre2_cflags:=-std=c99 -Wall -Wextra -g -O3 -DPCRE2_CODE_UNIT_WIDTH=8 -DSUPPORT_JIT -DSUPPORT_UNICODE -DHAVE_CONFIG_H $(CFLAGS) 5 | pcre2_objects:=$(addprefix pcre2/src/, \ 6 | pcre2_auto_possess.o pcre2_chartables.o pcre2_chkdint.o pcre2_compile.o \ 7 | pcre2_compile_class.o pcre2_config.o pcre2_context.o pcre2_convert.o \ 8 | pcre2_dfa_match.o pcre2_error.o pcre2_extuni.o pcre2_find_bracket.o \ 9 | pcre2_jit_compile.o pcre2_maketables.o pcre2_match_data.o pcre2_match.o \ 10 | pcre2_newline.o pcre2_ord2utf.o pcre2_pattern_info.o pcre2_script_run.o \ 11 | pcre2_serialize.o pcre2_string_utils.o pcre2_study.o pcre2_substitute.o \ 12 | pcre2_substring.o pcre2_tables.o pcre2_ucd.o pcre2_valid_utf.o \ 13 | pcre2_xclass.o) 14 | 15 | all: uthash/src/utlist.h pcre2/libpcre2-8.a lua/liblua5.4.a 16 | 17 | uthash/src/utlist.h: 18 | command -v git && git submodule update --init --recursive 19 | 20 | lua/liblua5.4.a: $(lua_objects) 21 | $(AR) rcs $@ $(lua_objects) 22 | 23 | $(lua_objects): %.o: %.c 24 | $(CC) -c $(lua_cflags) $< -o $@ 25 | 26 | pcre2/libpcre2-8.a: $(pcre2_objects) 27 | $(AR) rcs $@ $(pcre2_objects) 28 | 29 | pcre2/src/pcre2.h: pcre2/src/pcre2.h.generic 30 | cp -f $< $@ 31 | 32 | pcre2/src/config.h: pcre2/src/config.h.generic 33 | cp -f $< $@ 34 | 35 | pcre2/src/pcre2_chartables.c: pcre2/src/pcre2_chartables.c.dist 36 | cp -f $< $@ 37 | 38 | $(pcre2_objects): %.o: %.c 39 | $(CC) -c $(pcre2_cflags) $< -o $@ 40 | 41 | $(pcre2_objects:.o=.c): pcre2/src/pcre2.h pcre2/src/config.h 42 | 43 | clean: 44 | rm -f pcre2/libpcre2-8.a pcre2/src/pcre2.h pcre2/src/config.h pcre2/src/pcre2_chartables.c $(pcre2_objects) 45 | rm -f lua/liblua5.4.a $(lua_objects) 46 | 47 | .NOTPARALLEL: 48 | 49 | .PHONY: all clean 50 | -------------------------------------------------------------------------------- /vendor/lua5.4: -------------------------------------------------------------------------------- 1 | lua --------------------------------------------------------------------------------