├── .gitignore ├── tests ├── main.c ├── testdata │ ├── tags.c │ ├── tags │ └── utf8.txt ├── asserts.h ├── util.c ├── mock_termbox.c ├── os.c ├── buffer.c ├── tags.c ├── draw.c ├── window.c ├── options.c └── editor.c ├── terminal.h ├── attrs.h ├── .gitmodules ├── tags.h ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── syntax.h ├── motion.h ├── search.h ├── history.h ├── terminal.c ├── options.h ├── buf.h ├── util.h ├── history.c ├── mode.h ├── README.md ├── main.c ├── window.h ├── buffer.h ├── operator_pending_mode.c ├── gap.h ├── visual_mode.c ├── util.c ├── search.c ├── buf.c ├── Makefile ├── editor.h ├── tags.c ├── syntax.c ├── buffer.c ├── normal_mode.c ├── insert_mode.c ├── gap.c ├── cmdline_mode.c ├── options.c ├── motion.c └── window.c /.gitignore: -------------------------------------------------------------------------------- 1 | asan-build 2 | build 3 | compile_commands.json 4 | coverage 5 | coverage-build 6 | tags 7 | -------------------------------------------------------------------------------- /tests/main.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | 3 | int main(int argc, char *argv[]) { 4 | return clar_test(argc, argv); 5 | } 6 | -------------------------------------------------------------------------------- /terminal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void terminal_init(void); 4 | void terminal_resume(void); 5 | void terminal_shutdown(void); 6 | void terminal_suspend(void); 7 | -------------------------------------------------------------------------------- /tests/testdata/tags.c: -------------------------------------------------------------------------------- 1 | // This line intentionally left blank 2 | int quux; 3 | 4 | void baz(void) { 5 | } 6 | 7 | void bar(void) { 8 | } 9 | 10 | void foo(void) { 11 | bar(); 12 | baz(); 13 | quux = 3; 14 | } 15 | -------------------------------------------------------------------------------- /attrs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if __has_attribute(fallthrough) 4 | #define ATTR_FALLTHROUGH __attribute__((fallthrough)) 5 | #else 6 | #define ATTR_FALLTHROUGH 7 | #endif 8 | 9 | #define ATTR_PRINTFLIKE(fmtarg, firstvararg) \ 10 | __attribute__((__format__(__printf__, fmtarg, firstvararg))) 11 | #define ATTR_UNUSED __attribute__((unused)) 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/clar"] 2 | path = vendor/clar 3 | url = https://github.com/clar-test/clar 4 | [submodule "vendor/termbox"] 5 | path = vendor/termbox 6 | url = https://github.com/tomas/termbox 7 | [submodule "vendor/libclipboard"] 8 | path = vendor/libclipboard 9 | url = https://github.com/jtanx/libclipboard 10 | [submodule "vendor/pcre2"] 11 | path = vendor/pcre2 12 | url = https://github.com/BurntSushi/pcre2-mirror.git 13 | branch = release/10.32 14 | -------------------------------------------------------------------------------- /tests/testdata/tags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ 2 | !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/ 4 | !_TAG_PROGRAM_NAME Exuberant Ctags // 5 | !_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/ 6 | !_TAG_PROGRAM_VERSION 5.8 // 7 | bar tags.c /^void bar(void) {$/;" f 8 | baz tags.c /^void baz(void) {$/;" f 9 | foo tags.c /^void foo(void) {$/;" f 10 | quux tags.c /^int quux;$/;" v 11 | -------------------------------------------------------------------------------- /tags.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | struct editor; 9 | 10 | struct tag { 11 | char *name; 12 | char *path; 13 | char *cmd; 14 | }; 15 | 16 | struct tag_jump { 17 | struct buffer *buffer; 18 | size_t cursor; 19 | struct tag *tag; 20 | 21 | TAILQ_ENTRY(tag_jump) pointers; 22 | }; 23 | 24 | struct tags { 25 | struct tag *tags; 26 | size_t len; 27 | 28 | const char *file; 29 | time_t loaded_at; 30 | }; 31 | 32 | struct tags *tags_create(const char *path); 33 | void tags_clear(struct tags *tags); 34 | struct tag *tags_find(struct tags *tags, char *name); 35 | 36 | void editor_jump_to_tag(struct editor *editor, char *tag); 37 | 38 | void editor_tag_stack_prev(struct editor *editor); 39 | void editor_tag_stack_next(struct editor *editor); 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macOS-latest] 14 | compiler: [gcc, clang] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | with: 21 | submodules: true 22 | - name: make 23 | run: make CC=${{ matrix.compiler }} 24 | - name: make test 25 | run: make test CC=${{ matrix.compiler }} 26 | - name: make test-asan 27 | run: make test-asan CC=${{ matrix.compiler }} 28 | 29 | coverage: 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: Install lcov 34 | run: sudo apt-get install lcov 35 | - uses: actions/checkout@v1 36 | with: 37 | submodules: true 38 | - name: make coverage 39 | run: make coverage CC=gcc 40 | - name: Upload coverage report 41 | uses: actions/upload-artifact@v1 42 | with: 43 | name: Coverage 44 | path: ./coverage 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ismail Badawi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /syntax.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | struct syntax_token { 9 | enum syntax_token_kind { 10 | SYNTAX_TOKEN_NONE, 11 | SYNTAX_TOKEN_COMMENT, 12 | SYNTAX_TOKEN_IDENTIFIER, 13 | SYNTAX_TOKEN_LITERAL_CHAR, 14 | SYNTAX_TOKEN_LITERAL_NUMBER, 15 | SYNTAX_TOKEN_LITERAL_STRING, 16 | SYNTAX_TOKEN_PREPROC, 17 | SYNTAX_TOKEN_PUNCTUATION, 18 | SYNTAX_TOKEN_STATEMENT, 19 | SYNTAX_TOKEN_TYPE, 20 | } kind; 21 | size_t pos; 22 | size_t len; 23 | }; 24 | 25 | struct syntax; 26 | typedef void (*tokenizer_func)(struct syntax*, struct syntax_token*, size_t); 27 | struct syntax { 28 | struct buffer *buffer; 29 | tokenizer_func tokenizer; 30 | size_t pos; 31 | enum syntax_state { 32 | STATE_INIT, 33 | STATE_PREPROC 34 | } state; 35 | pcre2_code *regex; 36 | pcre2_match_data *groups; 37 | }; 38 | 39 | char *syntax_detect_filetype(char *path); 40 | bool syntax_init(struct syntax *syntax, struct buffer *buffer); 41 | void syntax_deinit(struct syntax *syntax); 42 | void syntax_token_at(struct syntax *syntax, struct syntax_token *token, size_t pos); 43 | -------------------------------------------------------------------------------- /tests/asserts.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define assert_contents(text_) do { \ 4 | size_t expected_len = strlen(text_); \ 5 | size_t actual_len = gb_size(buffer->text); \ 6 | cl_assert_equal_i(expected_len, actual_len); \ 7 | struct buf *buf = gb_getstring(buffer->text, 0, expected_len); \ 8 | cl_assert_equal_s(buf->buf, text_); \ 9 | buf_free(buf); \ 10 | } while (0) 11 | 12 | #define assert_buffer_contents(text_) do { \ 13 | struct buffer* buffer = editor->window->buffer; \ 14 | assert_contents(text_); \ 15 | } while (0) 16 | 17 | #define assert_cursor_at(expected_line, expected_column) do { \ 18 | size_t actual_line, actual_column; \ 19 | gb_pos_to_linecol( \ 20 | editor->window->buffer->text, \ 21 | window_cursor(editor->window), \ 22 | &actual_line, &actual_column); \ 23 | cl_assert_equal_i(expected_line, actual_line); \ 24 | cl_assert_equal_i(expected_column, actual_column); \ 25 | } while (0) 26 | 27 | #define assert_cursor_over(c) do { \ 28 | struct gapbuf *text = editor->window->buffer->text; \ 29 | size_t cursor = window_cursor(editor->window); \ 30 | cl_assert_equal_i(gb_getchar(text, cursor), c); \ 31 | } while (0) 32 | 33 | -------------------------------------------------------------------------------- /motion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct motion_context { 7 | size_t pos; 8 | struct window *window; 9 | struct editor *editor; 10 | }; 11 | 12 | struct motion { 13 | char name; 14 | size_t (*op)(struct motion_context); 15 | // Whether the motion is linewise (vs. characterwise). 16 | // (See :help linewise in vim for details). 17 | bool linewise; 18 | // Whether the motion is exclusive (vs. inclusive). 19 | // (See :help exclusive in vim for details). 20 | // This is only relevant for characterwise motions. 21 | bool exclusive; 22 | // Whether the motion callback should be invoked repeatedly 23 | // (as opposed to being aware of editor->count). 24 | bool repeat; 25 | }; 26 | 27 | struct tb_event; 28 | struct motion *motion_get(struct editor *editor, struct tb_event *ev); 29 | 30 | size_t motion_apply(struct motion *motion, struct editor *editor); 31 | 32 | // TODO(isbadawi): This is an awkward place to put this. 33 | // But a lot of knowledge about words lives in motion.c right now. 34 | struct buf *motion_word_under_cursor(struct window *window); 35 | struct buf *motion_word_before_cursor(struct window *window); 36 | 37 | struct buf *motion_filename_under_cursor(struct window *window); 38 | -------------------------------------------------------------------------------- /search.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "util.h" 9 | 10 | struct editor; 11 | 12 | enum search_direction { 13 | SEARCH_FORWARDS, 14 | SEARCH_BACKWARDS 15 | }; 16 | 17 | struct search { 18 | pcre2_code *regex; 19 | pcre2_match_data *groups; 20 | unsigned char *str; 21 | size_t len; 22 | size_t start; 23 | }; 24 | 25 | int search_init(struct search *search, char *pattern, bool ignore_case, 26 | char *str, size_t len); 27 | void search_get_error(int error, char *buf, size_t buflen); 28 | void search_deinit(struct search *search); 29 | bool search_next_match(struct search *search, struct region *match); 30 | 31 | // Search for the given pattern in the currently opened buffer, returning the 32 | // first match. If pattern is NULL, will instead search for the pattern stored 33 | // in the last-search-pattern register (/). 34 | bool editor_search( 35 | struct editor *editor, char *pattern, 36 | size_t start, enum search_direction direction, struct region *match); 37 | 38 | bool editor_ignore_case(struct editor *editor, char *pattern); 39 | 40 | void editor_jump_to_match(struct editor *editor, char *pattern, 41 | size_t start, enum search_direction direction); 42 | -------------------------------------------------------------------------------- /history.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | struct history_entry { 8 | struct buf *buf; 9 | 10 | TAILQ_ENTRY(history_entry) pointers; 11 | }; 12 | 13 | struct history { 14 | TAILQ_HEAD(history_list, history_entry) entries; 15 | int len; 16 | 17 | // How many history entries we keep (older ones get discarded). 18 | // This can change between calls if the user runs 'set history=val'. 19 | // The new limit only takes effect the next time history_add_item is called. 20 | // TODO(ibadawi): maybe have callbacks that run when option values change? 21 | int *limit; 22 | }; 23 | 24 | void history_init(struct history *history, int *limit); 25 | void history_deinit(struct history *history); 26 | void history_add_item(struct history *history, char *item); 27 | 28 | typedef bool (history_predicate) (struct buf*, char*); 29 | 30 | struct history_entry *history_first( 31 | struct history *history, history_predicate p, char *arg); 32 | struct history_entry *history_last( 33 | struct history *history, history_predicate p, char *arg); 34 | struct history_entry *history_prev( 35 | struct history_entry *entry, history_predicate p, char *arg); 36 | struct history_entry *history_next( 37 | struct history_entry *entry, history_predicate p, char *arg); 38 | -------------------------------------------------------------------------------- /terminal.c: -------------------------------------------------------------------------------- 1 | #include "terminal.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | // Ward against calling tb_shutdown twice. A common case where this would 11 | // happen is if we receive SIGTERM while the editor is in the background. 12 | static bool terminal_is_shut_down = false; 13 | 14 | void terminal_resume(void) { 15 | int err = tb_init(); 16 | if (err) { 17 | fprintf(stderr, "tb_init() failed with error code %d\n", err); 18 | exit(1); 19 | } 20 | terminal_is_shut_down = false; 21 | } 22 | 23 | void terminal_shutdown(void) { 24 | if (!terminal_is_shut_down) { 25 | terminal_is_shut_down = true; 26 | tb_shutdown(); 27 | } 28 | } 29 | 30 | void terminal_suspend(void) { 31 | terminal_shutdown(); 32 | // SIGTSTP is the event that is normally generated by hitting Ctrl-Z. 33 | raise(SIGTSTP); 34 | terminal_resume(); 35 | } 36 | 37 | static void terminal_sighandler(int signum) { 38 | terminal_shutdown(); 39 | raise(signum); 40 | } 41 | 42 | void terminal_init(void) { 43 | terminal_resume(); 44 | atexit(terminal_shutdown); 45 | 46 | struct sigaction sa; 47 | sa.sa_handler = terminal_sighandler; 48 | sigemptyset(&sa.sa_mask); 49 | sa.sa_flags = SA_RESETHAND; 50 | sigaction(SIGABRT, &sa, NULL); 51 | sigaction(SIGINT, &sa, NULL); 52 | sigaction(SIGQUIT, &sa, NULL); 53 | sigaction(SIGSEGV, &sa, NULL); 54 | sigaction(SIGTERM, &sa, NULL); 55 | } 56 | -------------------------------------------------------------------------------- /tests/util.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | #include "util.h" 3 | 4 | #define cl_assert_equal_s_free(a, b) { \ 5 | char *_tmp = a; \ 6 | cl_assert_equal_s(_tmp, b); \ 7 | free(_tmp); \ 8 | } 9 | 10 | void test_util__abspath(void) { 11 | cl_assert_equal_s_free(abspath("/a//b"), "/a/b"); 12 | cl_assert_equal_s_free(abspath("/a/./b"), "/a/b"); 13 | cl_assert_equal_s_free(abspath("/a//./b"), "/a/b"); 14 | cl_assert_equal_s_free(abspath("/"), "/"); 15 | cl_assert_equal_s_free(abspath("/.."), "/"); 16 | cl_assert_equal_s_free(abspath("/../a"), "/a"); 17 | cl_assert_equal_s_free(abspath("/.././..//../.././"), "/"); 18 | cl_assert_equal_s_free(abspath("/a/../b"), "/b"); 19 | cl_assert_equal_s_free(abspath("/a//b/../c/./d"), "/a/c/d"); 20 | cl_assert_equal_s_free(abspath("/a/b/c/d/../../e"), "/a/b/e"); 21 | cl_assert_equal_s_free(abspath("/a/b/c/d/../../e/../.."), "/a"); 22 | cl_assert_equal_s_free(abspath("/a/./b/c//d/.././../e/../../f"), "/a/f"); 23 | cl_assert_equal_s_free(abspath("/a/.."), "/"); 24 | } 25 | 26 | void test_util__relpath(void) { 27 | cl_assert_equal_s(relpath("/a/b", "/a/b"), ""); 28 | cl_assert_equal_s(relpath("/a/b", "/a"), "b"); 29 | cl_assert_equal_s(relpath("/foo.txt", "/a/b"), "/foo.txt"); 30 | } 31 | 32 | void test_util__strrep(void) { 33 | cl_assert_equal_s_free(strrep("hello %", "%", "world"), "hello world"); 34 | cl_assert_equal_s_free(strrep("foo % % bar", "%", "baz"), "foo baz baz bar"); 35 | cl_assert_equal_s_free(strrep("barb barn", "bar", "foo"), "foob foon"); 36 | } 37 | -------------------------------------------------------------------------------- /options.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef char* string; 6 | 7 | #define BUFFER_OPTIONS \ 8 | OPTION(autoindent, bool, false) \ 9 | OPTION(cinwords, string, "if,else,while,do,for,switch") \ 10 | OPTION(expandtab, bool, false) \ 11 | OPTION(filetype, string, "") \ 12 | OPTION(modifiable, bool, true) \ 13 | OPTION(modified, bool, false) \ 14 | OPTION(readonly, bool, false) \ 15 | OPTION(shiftwidth, int, 8) \ 16 | OPTION(smartindent, bool, false) \ 17 | OPTION(suffixesadd, string, "") \ 18 | OPTION(tabstop, int, 8) \ 19 | 20 | #define WINDOW_OPTIONS \ 21 | OPTION(cursorline, bool, false) \ 22 | OPTION(number, bool, false) \ 23 | OPTION(numberwidth, int, 4) \ 24 | OPTION(relativenumber, bool, false) \ 25 | 26 | #define EDITOR_OPTIONS \ 27 | OPTION(equalalways, bool, true) \ 28 | OPTION(history, int, 50) \ 29 | OPTION(hlsearch, bool, false) \ 30 | OPTION(ignorecase, bool, false) \ 31 | OPTION(incsearch, bool, false) \ 32 | OPTION(path, string, ".,/usr/include,,") \ 33 | OPTION(ruler, bool, false) \ 34 | OPTION(showmode, bool, true) \ 35 | OPTION(sidescroll, int, 0) \ 36 | OPTION(smartcase, bool, false) \ 37 | OPTION(splitbelow, bool, false) \ 38 | OPTION(splitright, bool, false) \ 39 | 40 | struct editor; 41 | struct window; 42 | struct buffer; 43 | 44 | void editor_init_options(struct editor *editor); 45 | void window_init_options(struct window *window); 46 | void editor_free_options(struct editor *editor); 47 | void window_free_options(struct window *window); 48 | void buffer_free_options(struct buffer *buffer); 49 | void buffer_inherit_editor_options(struct buffer *buffer, struct editor *editor); 50 | void window_inherit_parent_options(struct window *window); 51 | 52 | char **options_get_sorted(int *len); 53 | -------------------------------------------------------------------------------- /buf.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "attrs.h" 9 | 10 | // struct buf is a simple growable string. 11 | struct buf { 12 | char *buf; 13 | size_t len; 14 | size_t cap; 15 | }; 16 | 17 | struct buf *buf_create(size_t cap); 18 | struct buf *buf_from_cstr(char *s); 19 | struct buf *buf_from_char(char c); 20 | struct buf *buf_from_utf8(uint32_t c); 21 | struct buf *buf_copy(struct buf *buf); 22 | void buf_free(struct buf *buf); 23 | 24 | void buf_clear(struct buf *buf); 25 | void buf_grow(struct buf *buf, size_t cap); 26 | void buf_delete(struct buf *buf, size_t pos, size_t len); 27 | void buf_insert(struct buf *buf, char *s, size_t pos); 28 | void buf_append(struct buf *buf, char *s); 29 | void buf_append_char(struct buf *buf, char c); 30 | 31 | // Write the formatted data to buf (overwriting what was there), 32 | // automatically growing it if needed. 33 | ATTR_PRINTFLIKE(2, 3) 34 | void buf_printf(struct buf *buf, const char *format, ...); 35 | ATTR_PRINTFLIKE(2, 3) 36 | void buf_appendf(struct buf *buf, const char *format, ...); 37 | ATTR_PRINTFLIKE(2, 0) 38 | void buf_vprintf(struct buf *buf, const char *format, va_list args); 39 | 40 | bool buf_equals(struct buf *buf, char *s); 41 | bool buf_startswith(struct buf *buf, char *prefix); 42 | bool buf_endswith(struct buf *buf, char *suffix); 43 | void buf_strip_whitespace(struct buf *buf); 44 | 45 | // Similar to struct buf but for ints. 46 | struct intbuf { 47 | unsigned int *buf; 48 | size_t len; 49 | size_t cap; 50 | }; 51 | 52 | struct intbuf *intbuf_create(size_t cap); 53 | void intbuf_free(struct intbuf *buf); 54 | 55 | void intbuf_insert(struct intbuf *buf, unsigned int i, size_t pos); 56 | void intbuf_add(struct intbuf *buf, unsigned int i); 57 | void intbuf_remove(struct intbuf *buf, size_t pos); 58 | -------------------------------------------------------------------------------- /util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "attrs.h" 9 | 10 | #define min(x, y) ((x) < (y) ? (x) : (y)) 11 | #define max(x, y) ((x) > (y) ? (x) : (y)) 12 | 13 | // TAILQ_FOREACH_SAFE and TAILQ_FOREACH_REVERSE_SAFE, which allow elements to 14 | // be removed or freed during the loop, are available on BSDs but not in glibc, 15 | // so shim them here. The implementation is copied from sys/queue.h on macOS. 16 | 17 | #ifndef TAILQ_FOREACH_SAFE 18 | #define TAILQ_FOREACH_SAFE(var, head, field, tvar) \ 19 | for ((var) = TAILQ_FIRST((head)); \ 20 | (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \ 21 | (var) = (tvar)) 22 | #endif 23 | 24 | #ifndef TAILQ_FOREACH_REVERSE_SAFE 25 | #define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \ 26 | for ((var) = TAILQ_LAST((head), headname); \ 27 | (var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \ 28 | (var) = (tvar)) 29 | #endif 30 | 31 | ATTR_PRINTFLIKE(1, 2) 32 | void debug(const char *format, ...); 33 | 34 | void *xmalloc(size_t size); 35 | void *xrealloc(void *ptr, size_t size); 36 | char *xstrdup(const char *s); 37 | bool strtoi(char *s, int *result); 38 | 39 | // Returns a new string where all occurrences of 'from' are replaced with 'to'. 40 | char *strrep(char *s, char *from, char *to); 41 | 42 | size_t strcnt(char *s, char c); 43 | 44 | // Returns absolute path, with unnecessary //, ./ or ../ removed. 45 | // Also expands a leading ~ to the home directory. 46 | // Unlike realpath(3), does not resolve symlinks. 47 | char *abspath(const char *path); 48 | const char *relpath(const char *path, const char *start); 49 | const char *homedir(void); 50 | 51 | struct region { 52 | size_t start; 53 | size_t end; 54 | }; 55 | 56 | struct region *region_set(struct region *region, size_t start, size_t end); 57 | -------------------------------------------------------------------------------- /history.c: -------------------------------------------------------------------------------- 1 | #include "history.h" 2 | 3 | #include 4 | 5 | #include "buf.h" 6 | #include "util.h" 7 | 8 | void history_init(struct history *history, int *limit) { 9 | TAILQ_INIT(&history->entries); 10 | history->len = 0; 11 | history->limit = limit; 12 | } 13 | 14 | void history_deinit(struct history *history) { 15 | struct history_entry *entry, *te; 16 | TAILQ_FOREACH_SAFE(entry, &history->entries, pointers, te) { 17 | buf_free(entry->buf); 18 | free(entry); 19 | } 20 | } 21 | 22 | struct history_entry *history_first( 23 | struct history *history, history_predicate p, char* arg) { 24 | struct history_entry *entry = TAILQ_FIRST(&history->entries); 25 | if (!entry || p(entry->buf, arg)) { 26 | return entry; 27 | } 28 | return history_next(entry, p, arg); 29 | } 30 | 31 | struct history_entry *history_last( 32 | struct history *history, history_predicate p, char* arg) { 33 | struct history_entry *entry; 34 | struct history_entry *last = NULL; 35 | TAILQ_FOREACH(entry, &history->entries, pointers) { 36 | if (p(entry->buf, arg)) { 37 | last = entry; 38 | } 39 | } 40 | return last; 41 | } 42 | 43 | struct history_entry *history_next( 44 | struct history_entry *entry, history_predicate p, char* arg) { 45 | struct history_entry *next = entry; 46 | while ((next = TAILQ_NEXT(next, pointers))) { 47 | if (p(next->buf, arg)) { 48 | return next; 49 | } 50 | } 51 | return NULL; 52 | } 53 | 54 | struct history_entry *history_prev( 55 | struct history_entry *entry, history_predicate p, char* arg) { 56 | struct history_entry *prev = entry; 57 | while ((prev = TAILQ_PREV(prev, history_list, pointers))) { 58 | if (p(prev->buf, arg)) { 59 | return prev; 60 | } 61 | } 62 | return NULL; 63 | } 64 | 65 | static void history_truncate_to_limit(struct history *history) { 66 | while (history->limit && history->len > *history->limit) { 67 | struct history_entry *last = TAILQ_LAST(&history->entries, history_list); 68 | TAILQ_REMOVE(&history->entries, last, pointers); 69 | buf_free(last->buf); 70 | free(last); 71 | history->len--; 72 | } 73 | } 74 | 75 | void history_add_item(struct history *history, char *item) { 76 | struct history_entry *entry = history_first(history, buf_equals, item); 77 | if (entry) { 78 | TAILQ_REMOVE(&history->entries, entry, pointers); 79 | history->len--; 80 | } 81 | history_truncate_to_limit(history); 82 | 83 | if (!entry) { 84 | entry = xmalloc(sizeof(*entry)); 85 | entry->buf = buf_from_cstr(item); 86 | } 87 | TAILQ_INSERT_HEAD(&history->entries, entry, pointers); 88 | history->len++; 89 | } 90 | -------------------------------------------------------------------------------- /tests/mock_termbox.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "attrs.h" 9 | 10 | #define MOCK_TB_WIDTH 80 11 | #define MOCK_TB_HEIGHT 24 12 | 13 | static struct tb_cell cell_buffer[MOCK_TB_WIDTH * MOCK_TB_HEIGHT]; 14 | 15 | int tb_init(void) { 16 | memset(cell_buffer, 0, sizeof(cell_buffer)); 17 | return 0; 18 | } 19 | 20 | void tb_shutdown(void) { 21 | return; 22 | } 23 | 24 | int tb_width(void) { 25 | return MOCK_TB_WIDTH; 26 | } 27 | 28 | int tb_height(void) { 29 | return MOCK_TB_HEIGHT; 30 | } 31 | 32 | void tb_clear_buffer(void) { 33 | for (int i = 0; i < MOCK_TB_HEIGHT; ++i) { 34 | for (int j = 0; j < MOCK_TB_WIDTH; ++j) { 35 | tb_char(j, i, TB_DEFAULT, TB_DEFAULT, '\0'); 36 | } 37 | } 38 | } 39 | 40 | void tb_render(void) { 41 | return; 42 | } 43 | 44 | void tb_char(int x, int y, tb_color fg, tb_color bg, tb_chr ch) { 45 | struct tb_cell *cell = &cell_buffer[y * MOCK_TB_WIDTH + x]; 46 | cell->ch = ch; 47 | cell->fg = fg; 48 | cell->bg = bg; 49 | } 50 | 51 | int tb_string(int x, int y, tb_color fg, tb_color bg, const char * str) { 52 | int l; 53 | for (l = 0; *str; l++) { 54 | tb_char(x++, y, fg, bg, *str++); 55 | } 56 | return l; 57 | } 58 | 59 | int tb_string_with_limit(int x, int y, tb_color fg, tb_color bg, 60 | const char * str, int limit) { 61 | int l; 62 | for (l = 0; *str && l < limit; l++) { 63 | tb_char(x++, y, fg, bg, *str++); 64 | } 65 | return l; 66 | } 67 | 68 | int tb_stringf(int x, int y, tb_color fg, tb_color bg, const char *fmt, ...) { 69 | char buf[512]; 70 | va_list vl; 71 | va_start(vl, fmt); 72 | vsnprintf(buf, sizeof(buf), fmt, vl); 73 | va_end(vl); 74 | return tb_string(x, y, fg, bg, buf); 75 | } 76 | 77 | void tb_empty(int x, int y, tb_color bg, int width) { 78 | char buf[512]; 79 | snprintf(buf, sizeof(buf), "%*s", width, ""); 80 | tb_string(x, y, TB_DEFAULT, bg, buf); 81 | } 82 | 83 | struct tb_cell *tb_cell_buffer(void) { 84 | return cell_buffer; 85 | } 86 | 87 | int tb_select_output_mode(int mode ATTR_UNUSED) { 88 | return 0; 89 | } 90 | 91 | 92 | int tb_poll_event(struct tb_event *event ATTR_UNUSED) { 93 | return 0; 94 | } 95 | 96 | int tb_utf8_char_length(char c ATTR_UNUSED) { 97 | return 1; 98 | } 99 | 100 | int tb_utf8_char_to_unicode(uint32_t *out, const char *c) { 101 | *out = (uint32_t) *c; 102 | return 1; 103 | } 104 | 105 | int tb_utf8_unicode_to_char(char *out, uint32_t c) { 106 | *out = (char) c; 107 | return 1; 108 | } 109 | -------------------------------------------------------------------------------- /mode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "util.h" 7 | 8 | struct editor; 9 | struct tb_event; 10 | 11 | #define MODES \ 12 | MODE(normal) \ 13 | MODE(insert) \ 14 | MODE(visual) \ 15 | MODE(cmdline) \ 16 | MODE(operator_pending) 17 | 18 | struct editing_mode { 19 | enum editing_mode_kind { 20 | #define MODE(name) EDITING_MODE_##name, 21 | MODES 22 | #undef MODE 23 | } kind; 24 | // Called when the editor switches into this mode (optional). 25 | void (*entered)(struct editor*); 26 | // Called when the editor switches out of this mode (optional). 27 | void (*exited)(struct editor*); 28 | // Called when this mode is active and a key is pressed. 29 | void (*key_pressed)(struct editor*, struct tb_event*); 30 | // The mode the editor was in when this mode was entered. 31 | struct editing_mode *parent; 32 | // Mode-specific argument. 33 | uint64_t arg; 34 | }; 35 | 36 | struct normal_mode { 37 | struct editing_mode mode; 38 | }; 39 | 40 | struct insert_mode { 41 | struct editing_mode mode; 42 | 43 | char *completion_prefix; 44 | struct history_entry *completion; 45 | struct history *completions; 46 | }; 47 | 48 | struct visual_mode { 49 | struct editing_mode mode; 50 | enum visual_mode_kind { 51 | VISUAL_MODE_CHARACTERWISE, 52 | VISUAL_MODE_LINEWISE, 53 | // TODO(ibadawi): VISUAL_MODE_BLOCKWISE 54 | } kind; 55 | // The position of the cursor when the mode was entered. 56 | size_t cursor; 57 | struct region selection; 58 | }; 59 | void visual_mode_selection_update(struct editor *editor); 60 | 61 | struct cmdline_mode { 62 | struct editing_mode mode; 63 | // The position of the cursor when the mode was entered. 64 | size_t cursor; 65 | struct history_entry *history_entry; 66 | struct buf *history_prefix; 67 | 68 | struct history_entry *completion; 69 | struct history *completions; 70 | enum completion_kind { 71 | COMPLETION_NONE, 72 | COMPLETION_CMDS, 73 | COMPLETION_OPTIONS, 74 | COMPLETION_TAGS, 75 | COMPLETION_PATHS, 76 | } completion_kind; 77 | char prompt; 78 | }; 79 | 80 | void editor_load_completions(struct editor *editor, 81 | enum completion_kind kind, struct history *history); 82 | 83 | typedef void (op_func)(struct editor*, struct region*); 84 | op_func *op_find(char name); 85 | struct operator_pending_mode { 86 | struct editing_mode mode; 87 | op_func *op; 88 | }; 89 | 90 | #define MODE(name) \ 91 | void editor_push_##name##_mode(struct editor *editor, uint64_t arg); \ 92 | struct name##_mode *editor_get_##name##_mode(struct editor *editor); \ 93 | void name##_mode_entered(struct editor *editor); \ 94 | void name##_mode_exited(struct editor *editor); \ 95 | void name##_mode_key_pressed(struct editor* editor, struct tb_event* ev); 96 | MODES 97 | #undef MODE 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # badavi 2 | 3 | [![Build Status](https://github.com/isbadawi/badavi/workflows/CI/badge.svg)](https://github.com/isbadawi/badavi/actions) 4 | 5 | `badavi` is a vi-like terminal mode text editor, implemented in C and using the 6 | [`termbox`](https://github.com/tomas/termbox) library to draw to the terminal. 7 | 8 | It's meant to be a learning exercise and fun project to hack on rather than a 9 | serious day-to-day editor, although who knows where it'll end up. 10 | 11 | ### Features supported so far 12 | 13 | * Normal, insert, and visual modes. 14 | 15 | * Motions -- `h`, `j`, `k`, `l`, `0`, `$`, `^`, `{`, `}`, `b`, `B`, `w`, `W`, 16 | `e`, `E`, `G`, `g_`, `ge`, `gE`, `gg`, `%`, and more. Motions can be prefixed 17 | with an optional count. 18 | 19 | * `:` commands -- `:w [path]`, `:wq`, `:q`, `:e path` and more. 20 | 21 | * Split windows with `:split` (horizontal) and `:vsplit` (vertical). You can 22 | navigate between them with ` hjkl`, resize the current window with 23 | ` +` and ` -`, and equalize the layout with ` =`. 24 | 25 | * Delete (`d`), change (`c`) and yank (`y`) operators, which can be applied to 26 | any of the motions, or the visual mode selection. (Text objects aren't 27 | implemented yet). The affected region is saved into the unnamed register, used 28 | by `p` to paste text. Named registers from `a` to `z` are also implemented, and 29 | can be specified by prefixing the operator (or `p`) with `"a` through `"z`. 30 | 31 | * Undo (`u`) and redo (``) (only single-level for now, unlike vim). 32 | 33 | * `ctags` support -- on startup badavi looks for a tags file called `tags` in 34 | the current directory (`'tags'` option not supported yet). The `:tag` command 35 | jumps to the specified tag, and `` jumps to the tag of the word under 36 | the cursor. `` and `:tag` can be used to walk up and down the tag stack. 37 | The `-t` command line option can also be passed in to start editing at the 38 | given tag, as in e.g. `badavi -t main`. 39 | 40 | * A small subset of the options are implemented. You can manipulate them with 41 | `:set`, `:setlocal` and `:setglobal`. These commands accept a similar syntax 42 | as vim, e.g. `:set number`, `:set number!`, `:set nonumber`, `:set number?`, 43 | etc. Options can be read from a file with `:source path`. At startup a file 44 | called `~/.badavimrc` is sourced. 45 | 46 | * Search forwards with `/`, backwards with `?`. Standard POSIX regexes are 47 | used, so the syntax is not exactly the same as vim's. For instance, word 48 | boundaries are specified with `[[:<:]]` and `[[:>:]]` instead of `\<` and 49 | `\>`. `n` and `N` can be used to cycle through matches. `*` and `#` can be 50 | used to search forwards or backwards for the next occurrence of the word 51 | under the cursor. Searching is a motion, so it works with the operators. The 52 | `'incsearch'` and `'hlsearch'` options are also implemented. 53 | 54 | ### Building 55 | 56 | Just run `make`. 57 | 58 | ### License 59 | 60 | MIT -- see `LICENSE` file for details. 61 | -------------------------------------------------------------------------------- /tests/os.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | #include "editor.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "buf.h" 13 | #include "buffer.h" 14 | #include "gap.h" 15 | #include "window.h" 16 | #include "util.h" 17 | 18 | #include "asserts.h" 19 | 20 | static struct editor *editor = NULL; 21 | 22 | static char *type(const char *keys) { 23 | editor_send_keys(editor, keys); 24 | return editor->status->buf; 25 | } 26 | 27 | char oldcwd[PATH_MAX] = {0}; 28 | char scratch[PATH_MAX] = {0}; 29 | 30 | void test_os__initialize(void) { 31 | if (!*oldcwd) { 32 | getcwd(oldcwd, sizeof(oldcwd)); 33 | strcpy(scratch, oldcwd); 34 | strcat(scratch, "/scratch"); 35 | } 36 | 37 | // Note that clar runs tests inside a tmp dir, so relative paths are ok. 38 | system("rm -rf ./scratch && mkdir -p ./scratch"); 39 | chdir(scratch); 40 | system( 41 | "echo 'hello world' > foo.txt && " 42 | "echo 'foo bar' > bar.txt && " 43 | "mkdir -p subdir && echo 'magic' > subdir/baz.txt"); 44 | 45 | editor = editor_create(tb_width(), tb_height()); 46 | } 47 | 48 | void test_os__cleanup(void) { 49 | editor_free(editor); 50 | chdir(oldcwd); 51 | } 52 | 53 | void test_os__open_file(void) { 54 | type(":e foo.txt"); 55 | assert_buffer_contents("hello world\n"); 56 | } 57 | 58 | void test_os__chdir_commands(void) { 59 | cl_assert_equal_s(type(":pwd"), scratch); 60 | cl_assert_equal_s(type(":cd ..:pwd"), oldcwd); 61 | cl_assert_equal_s(type(":cd scratch:pwd"), scratch); 62 | 63 | type(":set splitright:vsplit"); 64 | cl_assert_equal_s(type(":lcd ..:pwd"), oldcwd); 65 | cl_assert_equal_s(type(":pwd"), scratch); 66 | cl_assert_equal_s(type(":pwd"), oldcwd); 67 | } 68 | 69 | void test_os__browse_directory(void) { 70 | type(":e ."); 71 | assert_buffer_contents("bar.txt\nfoo.txt\nsubdir/\n"); 72 | 73 | type("jj"); 74 | 75 | assert_buffer_contents("baz.txt\n"); 76 | type(""); 77 | assert_buffer_contents("magic\n"); 78 | type("--"); 79 | assert_buffer_contents( 80 | "bar.txt\n" 81 | "foo.txt\n" 82 | "subdir/\n"); 83 | assert_cursor_at(2, 0); 84 | } 85 | 86 | void test_os__write_files(void) { 87 | type(":e foo.txt"); 88 | assert_buffer_contents("hello world\n"); 89 | type(":w copy.txt"); 90 | type(":e copy.txt"); 91 | assert_buffer_contents("hello world\n"); 92 | type(":e ."); 93 | assert_buffer_contents("bar.txt\ncopy.txt\nfoo.txt\nsubdir/\n"); 94 | } 95 | 96 | void test_os__completion(void) { 97 | cl_assert_equal_s(type(":edit "), ":edit bar.txt"); 98 | cl_assert_equal_s(type(""), ":edit foo.txt"); 99 | cl_assert_equal_s(type(""), ":edit subdir/"); 100 | cl_assert_equal_s(type(""), ":edit bar.txt"); 101 | type(""); 102 | } 103 | -------------------------------------------------------------------------------- /tests/buffer.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | #include "buffer.h" 3 | 4 | #include 5 | #include 6 | 7 | #include "buf.h" 8 | #include "gap.h" 9 | #include "util.h" 10 | 11 | #include "asserts.h" 12 | 13 | static struct buffer *buffer = NULL; 14 | 15 | static void insert_text(size_t pos, char *text) { 16 | buffer_do_insert(buffer, buf_from_cstr(text), pos); 17 | } 18 | 19 | static void delete_text(size_t pos, size_t len) { 20 | buffer_do_delete(buffer, len, pos); 21 | } 22 | 23 | void test_buffer__initialize(void) { 24 | buffer = buffer_create(NULL); 25 | } 26 | 27 | void test_buffer__cleanup(void) { 28 | buffer_free(buffer); 29 | } 30 | 31 | void test_buffer__empty(void) { 32 | cl_assert_equal_p(buffer->path, NULL); 33 | cl_assert(!buffer->opt.modified); 34 | assert_contents("\n"); 35 | } 36 | 37 | void test_buffer__insert_delete(void) { 38 | insert_text(0, "hello, world"); 39 | assert_contents("hello, world\n"); 40 | 41 | delete_text(1, 3); 42 | assert_contents("ho, world\n"); 43 | 44 | cl_assert(buffer->opt.modified); 45 | } 46 | 47 | void test_buffer__undo_redo(void) { 48 | size_t cursor_pos; 49 | 50 | assert_contents("\n"); 51 | 52 | buffer_start_action_group(buffer); 53 | insert_text(0, "world"); 54 | assert_contents("world\n"); 55 | 56 | buffer_start_action_group(buffer); 57 | insert_text(0, "hello, "); 58 | assert_contents("hello, world\n"); 59 | 60 | buffer_undo(buffer, &cursor_pos); 61 | assert_contents("world\n"); 62 | cl_assert_equal_i(cursor_pos, 0); 63 | 64 | buffer_undo(buffer, &cursor_pos); 65 | assert_contents("\n"); 66 | cl_assert_equal_i(cursor_pos, 0); 67 | 68 | buffer_redo(buffer, &cursor_pos); 69 | assert_contents("world\n"); 70 | cl_assert_equal_i(cursor_pos, 0); 71 | 72 | buffer_redo(buffer, &cursor_pos); 73 | assert_contents("hello, world\n"); 74 | cl_assert_equal_i(cursor_pos, 0); 75 | 76 | buffer_start_action_group(buffer); 77 | delete_text(1, 3); 78 | assert_contents("ho, world\n"); 79 | 80 | buffer_undo(buffer, &cursor_pos); 81 | assert_contents("hello, world\n"); 82 | cl_assert_equal_i(cursor_pos, 1); 83 | } 84 | 85 | void test_buffer__undo_group(void) { 86 | buffer_start_action_group(buffer); 87 | size_t cursor_pos; 88 | 89 | assert_contents("\n"); 90 | 91 | insert_text(0, "world"); 92 | assert_contents("world\n"); 93 | insert_text(0, "hello, "); 94 | assert_contents("hello, world\n"); 95 | 96 | buffer_undo(buffer, &cursor_pos); 97 | assert_contents("\n"); 98 | cl_assert_equal_i(cursor_pos, 0); 99 | 100 | buffer_redo(buffer, &cursor_pos); 101 | assert_contents("hello, world\n"); 102 | cl_assert_equal_i(cursor_pos, 0); 103 | } 104 | 105 | void test_buffer__marks(void) { 106 | struct mark mark; 107 | region_set(&mark.region, 0, 1); 108 | TAILQ_INSERT_TAIL(&buffer->marks, &mark, pointers); 109 | 110 | cl_assert_equal_i(mark.region.start, 0); 111 | cl_assert_equal_i(mark.region.end, 1); 112 | 113 | insert_text(0, "hello, world"); 114 | 115 | cl_assert_equal_i(mark.region.start, 12); 116 | cl_assert_equal_i(mark.region.end, 13); 117 | } 118 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "editor.h" 9 | #include "tags.h" 10 | #include "terminal.h" 11 | #include "util.h" 12 | #include "window.h" 13 | 14 | int main(int argc, char *argv[]) { 15 | char *line = NULL; 16 | char *tag = NULL; 17 | enum window_split_type split_type = WINDOW_LEAF; 18 | bool pathargs[argc]; 19 | memset(pathargs, 0, sizeof(pathargs)); 20 | char *initial_path = NULL; 21 | 22 | char *rc = NULL; 23 | char default_rc[255]; 24 | 25 | for (int i = 1; i < argc; ++i) { 26 | char *arg = argv[i]; 27 | if (!strcmp(arg, "-u")) { 28 | if (i == argc - 1) { 29 | fprintf(stderr, "argument missing after -u\n"); 30 | return 1; 31 | } 32 | rc = argv[++i]; 33 | } else if (!strcmp(arg, "-t")) { 34 | if (i == argc - 1) { 35 | fprintf(stderr, "argument missing after -t\n"); 36 | return 1; 37 | } 38 | tag = argv[++i]; 39 | } else if (!strcmp(arg, "-o")) { 40 | split_type = WINDOW_SPLIT_HORIZONTAL; 41 | } else if (!strcmp(arg, "-O")) { 42 | split_type = WINDOW_SPLIT_VERTICAL; 43 | } else if (*arg == '+') { 44 | line = arg + 1; 45 | } else { 46 | if (!initial_path) { 47 | initial_path = arg; 48 | } else { 49 | pathargs[i] = true; 50 | } 51 | } 52 | } 53 | 54 | terminal_init(); 55 | 56 | struct editor *editor = editor_create_and_open( 57 | (size_t) tb_width(), (size_t) tb_height(), initial_path); 58 | 59 | if (!rc) { 60 | snprintf(default_rc, sizeof(default_rc), "%s/.badavimrc", homedir()); 61 | if (!access(default_rc, F_OK)) { 62 | rc = default_rc; 63 | } 64 | } 65 | 66 | if (rc && strcmp(rc, "NONE") != 0) { 67 | editor_source(editor, rc); 68 | } 69 | 70 | if (tag) { 71 | editor_jump_to_tag(editor, tag); 72 | } else { 73 | if (split_type != WINDOW_LEAF) { 74 | enum window_split_direction direction; 75 | if (split_type == WINDOW_SPLIT_HORIZONTAL) { 76 | direction = WINDOW_SPLIT_BELOW; 77 | } else { 78 | assert(split_type == WINDOW_SPLIT_VERTICAL); 79 | direction = WINDOW_SPLIT_RIGHT; 80 | } 81 | for (int i = 1; i < argc; ++i) { 82 | if (pathargs[i]) { 83 | editor->window = window_split(editor->window, direction); 84 | editor_open(editor, argv[i]); 85 | } 86 | } 87 | editor->window = window_first_leaf(window_root(editor->window)); 88 | window_equalize(editor->window, split_type); 89 | } 90 | 91 | if (line) { 92 | if (!*line) { 93 | editor_jump_to_end(editor); 94 | } else { 95 | int linenum; 96 | if (strtoi(line, &linenum)) { 97 | editor_jump_to_line(editor, linenum - 1); 98 | } 99 | } 100 | } 101 | } 102 | 103 | editor_draw(editor); 104 | 105 | struct tb_event ev; 106 | while (editor_waitkey(editor, &ev)) { 107 | editor_handle_key_press(editor, &ev); 108 | editor_draw(editor); 109 | } 110 | 111 | return 0; 112 | } 113 | -------------------------------------------------------------------------------- /window.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "options.h" 8 | #include "util.h" 9 | 10 | struct window { 11 | struct window *parent; 12 | enum window_split_type { 13 | WINDOW_LEAF, 14 | WINDOW_SPLIT_VERTICAL, 15 | WINDOW_SPLIT_HORIZONTAL 16 | } split_type; 17 | 18 | // The size of the window. Only valid for the root window. 19 | size_t w; 20 | size_t h; 21 | 22 | struct { 23 | #define OPTION(name, type, _) type name; 24 | WINDOW_OPTIONS 25 | #undef OPTION 26 | } opt; 27 | 28 | union { 29 | struct { 30 | // The buffer being edited. 31 | struct buffer *buffer; 32 | 33 | char *alternate_path; 34 | 35 | // The coordinates of the top left cell visible on screen. 36 | size_t top; 37 | size_t left; 38 | 39 | // Window-local working directory if set (otherwise NULL). 40 | char *pwd; 41 | 42 | // The offset of the cursor. 43 | struct mark *cursor; 44 | 45 | // The incremental match if 'incsearch' is enabled. 46 | bool have_incsearch_match; 47 | struct region incsearch_match; 48 | 49 | // The visual mode selection. 50 | // NULL if not in visual mode. 51 | struct region *visual_mode_selection; 52 | 53 | TAILQ_HEAD(tag_list, tag_jump) tag_stack; 54 | struct tag_jump *tag; 55 | }; 56 | 57 | struct { 58 | struct window *first; 59 | struct window *second; 60 | size_t point; 61 | } split; 62 | }; 63 | }; 64 | 65 | struct window *window_create(struct buffer *buffer, size_t w, size_t h); 66 | void window_free(struct window *window); 67 | 68 | // Closes the current window and returns a pointer to the "next" window. 69 | // Caller should use window_free on passed-in window afterwards. 70 | struct window *window_close(struct window *window); 71 | 72 | enum window_split_direction { 73 | WINDOW_SPLIT_LEFT, 74 | WINDOW_SPLIT_RIGHT, 75 | WINDOW_SPLIT_ABOVE, 76 | WINDOW_SPLIT_BELOW 77 | }; 78 | 79 | struct window *window_split(struct window *window, 80 | enum window_split_direction direction); 81 | 82 | void window_resize(struct window *window, int dw, int dh); 83 | void window_equalize(struct window *window, 84 | enum window_split_type type); 85 | 86 | struct window *window_root(struct window *window); 87 | struct window *window_left(struct window *window); 88 | struct window *window_right(struct window *window); 89 | struct window *window_up(struct window *window); 90 | struct window *window_down(struct window *window); 91 | 92 | struct window *window_first_leaf(struct window *window); 93 | 94 | void window_set_buffer(struct window *window, struct buffer *buffer); 95 | 96 | size_t window_cursor(struct window *window); 97 | void window_set_cursor(struct window *window, size_t pos); 98 | void window_center_cursor(struct window *window); 99 | 100 | size_t window_w(struct window *window); 101 | size_t window_h(struct window *window); 102 | size_t window_x(struct window *window); 103 | size_t window_y(struct window *window); 104 | 105 | void window_page_up(struct window *window); 106 | void window_page_down(struct window *window); 107 | 108 | void window_clear_working_directories(struct window *window); 109 | -------------------------------------------------------------------------------- /buffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "options.h" 9 | #include "syntax.h" 10 | #include "util.h" 11 | 12 | struct buf; 13 | 14 | struct mark { 15 | struct region region; 16 | 17 | TAILQ_ENTRY(mark) pointers; 18 | }; 19 | 20 | // A single edit action -- either an insert or delete. 21 | struct edit_action { 22 | enum { EDIT_ACTION_INSERT, EDIT_ACTION_DELETE } type; 23 | // The position at which the action occurred. 24 | size_t pos; 25 | // The text added (for insertions) or removed (for deletions). 26 | struct buf *buf; 27 | 28 | TAILQ_ENTRY(edit_action) pointers; 29 | }; 30 | 31 | struct edit_action_group { 32 | TAILQ_HEAD(action_list, edit_action) actions; 33 | 34 | TAILQ_ENTRY(edit_action_group) pointers; 35 | }; 36 | 37 | TAILQ_HEAD(action_group_list, edit_action_group); 38 | 39 | // struct buffer is the in-memory text of a file. 40 | struct buffer { 41 | // The absolute path of the file this buffer was loaded from (possibly NULL). 42 | char *path; 43 | // The text proper. 44 | struct gapbuf *text; 45 | // Whether this is a directory buffer. 46 | bool directory; 47 | 48 | // Undo and redo stacks. 49 | // The elements are lists of actions. 50 | struct action_group_list undo_stack; 51 | struct action_group_list redo_stack; 52 | 53 | // Marked regions, whose positions are updated as edits are made via 54 | // buffer_do_insert and buffer_do_delete. 55 | TAILQ_HEAD(mark_list, mark) marks; 56 | 57 | struct { 58 | #define OPTION(name, type, _) type name; 59 | BUFFER_OPTIONS 60 | #undef OPTION 61 | } opt; 62 | 63 | TAILQ_ENTRY(buffer) pointers; 64 | }; 65 | 66 | // Reads the given path into a struct buffer object. The path must exist. 67 | // Returns NULL if buffer can't be opened or we're out of memory. 68 | struct buffer *buffer_open(char *path); 69 | 70 | // Returns an empty buffer (i.e. with a single empty line). 71 | struct buffer *buffer_create(char *path); 72 | 73 | // Free the given buffer. 74 | void buffer_free(struct buffer *buffer); 75 | 76 | // Writes the contents of the given buffer to buffer->name. 77 | // Returns false if this buffer has no name. 78 | bool buffer_write(struct buffer *buffer); 79 | 80 | // Writes to the contents of the given buffer to the path. 81 | // Returns false if the file couldn't be opened for writing. 82 | bool buffer_saveas(struct buffer *buffer, char *path); 83 | 84 | // Insert the given buf into the buffer's text at offset pos, 85 | // updating the undo information along the way. 86 | void buffer_do_insert(struct buffer *buffer, struct buf *buf, size_t pos); 87 | // Delete n characters from the buffer's text starting at offset pos, 88 | // updating the undo information along the way. 89 | void buffer_do_delete(struct buffer *buffer, size_t n, size_t pos); 90 | 91 | // Undo the last action group. Return false if there is nothing to undo. 92 | bool buffer_undo(struct buffer *buffer, size_t *cursor_pos); 93 | // Redo the last undone action group. Return false if there is nothing to redo. 94 | bool buffer_redo(struct buffer *buffer, size_t *cursor_pos); 95 | 96 | // Start a new action group, clearing the redo stack as a side effect. 97 | // Subsequent calls to buffer_do_insert or buffer_do_delete will add actions to 98 | // this group, which will be the target of the next buffer_undo call. 99 | void buffer_start_action_group(struct buffer *buffer); 100 | -------------------------------------------------------------------------------- /operator_pending_mode.c: -------------------------------------------------------------------------------- 1 | #include "mode.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "attrs.h" 9 | #include "buf.h" 10 | #include "buffer.h" 11 | #include "editor.h" 12 | #include "gap.h" 13 | #include "motion.h" 14 | #include "window.h" 15 | #include "util.h" 16 | 17 | static void yank_op(struct editor *editor, struct region *region) { 18 | struct editor_register *reg = editor_get_register(editor, editor->register_); 19 | struct gapbuf *gb = editor->window->buffer->text; 20 | 21 | size_t n = region->end - region->start; 22 | struct buf *buf = gb_getstring(gb, region->start, n); 23 | reg->write(reg, buf->buf); 24 | buf_free(buf); 25 | editor->register_ = '"'; 26 | } 27 | 28 | static void delete_op(struct editor *editor, struct region *region) { 29 | if (!editor_try_modify(editor)) { 30 | return; 31 | } 32 | 33 | yank_op(editor, region); 34 | if (region->end == gb_size(editor->window->buffer->text)) { 35 | region->end--; 36 | } 37 | 38 | buffer_start_action_group(editor->window->buffer); 39 | buffer_do_delete(editor->window->buffer, region->end - region->start, region->start); 40 | window_set_cursor(editor->window, region->start); 41 | } 42 | 43 | static void change_op(struct editor *editor, struct region *region) { 44 | if (!editor_try_modify(editor)) { 45 | return; 46 | } 47 | 48 | delete_op(editor, region); 49 | editor_push_insert_mode(editor, 0); 50 | } 51 | 52 | static struct { char name; op_func *op; } op_table[] = { 53 | {'d', delete_op}, 54 | {'c', change_op}, 55 | {'y', yank_op}, 56 | {-1, NULL} 57 | }; 58 | 59 | op_func *op_find(char name) { 60 | for (int i = 0; op_table[i].op; ++i) { 61 | if (op_table[i].name == name) { 62 | return op_table[i].op; 63 | } 64 | } 65 | return NULL; 66 | } 67 | 68 | void operator_pending_mode_entered(struct editor *editor) { 69 | struct operator_pending_mode *mode = editor_get_operator_pending_mode(editor); 70 | op_func *op = op_find((char) mode->mode.arg); 71 | if (!op) { 72 | editor_pop_mode(editor); 73 | return; 74 | } 75 | mode->op = op; 76 | } 77 | 78 | void operator_pending_mode_exited(struct editor *editor ATTR_UNUSED) { 79 | return; 80 | } 81 | 82 | void operator_pending_mode_key_pressed( 83 | struct editor *editor, struct tb_event *ev) { 84 | if (ev->ch != '0' && isdigit((int) ev->ch)) { 85 | editor->count = 0; 86 | while (isdigit((int) ev->ch)) { 87 | editor->count *= 10; 88 | editor->count += ev->ch - '0'; 89 | editor_waitkey(editor, ev); 90 | } 91 | } 92 | 93 | struct motion *motion = motion_get(editor, ev); 94 | if (!motion) { 95 | editor_pop_mode(editor); 96 | return; 97 | } 98 | 99 | struct gapbuf *gb = editor->window->buffer->text; 100 | 101 | struct region region; 102 | region_set(®ion, 103 | window_cursor(editor->window), 104 | motion_apply(motion, editor)); 105 | 106 | ssize_t last = gb_lastindexof(gb, '\n', region.start - 1); 107 | size_t next = gb_indexof(gb, '\n', region.end); 108 | if (motion->linewise) { 109 | region.start = (size_t)(last + 1); 110 | region.end = min(gb_size(gb), next + 1); 111 | } else if (region.start == region.end) { 112 | editor_pop_mode(editor); 113 | return; 114 | } else if (!motion->exclusive) { 115 | region.end = min(region.end + 1, next); 116 | } 117 | 118 | struct operator_pending_mode* mode = editor_get_operator_pending_mode(editor); 119 | editor_pop_mode(editor); 120 | mode->op(editor, ®ion); 121 | } 122 | -------------------------------------------------------------------------------- /gap.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct buf; 8 | 9 | // A "gap buffer" or "split buffer". It's a big buffer that internally 10 | // is separated into two buffers with a gap in the middle -- this allows 11 | // edits at the gap to be efficient. 12 | struct gapbuf { 13 | // Points to the start of the buffer. 14 | char *bufstart; 15 | // Points to one past the end of the buffer. 16 | char *bufend; 17 | // Points to the start of the gap. 18 | char *gapstart; 19 | // Points to one past the end of the gap. 20 | char *gapend; 21 | 22 | // An array of line lengths, with one element per line. 23 | struct intbuf *lines; 24 | }; 25 | 26 | // The gap is just an implementation detail. In what follows, "the buffer" 27 | // refers to the "logical" buffer, without the gap. 28 | 29 | // Create an empty buffer. 30 | struct gapbuf *gb_create(void); 31 | // Read fp into memory and create a buffer from the contents. 32 | struct gapbuf *gb_fromfile(char *path); 33 | // Create a buffer from the provided string. 34 | struct gapbuf *gb_fromstring(struct buf *buf); 35 | 36 | // Frees the given buffer. 37 | void gb_free(struct gapbuf *gb); 38 | 39 | // Returns the size of the buffer. 40 | size_t gb_size(struct gapbuf *gb); 41 | // Returns the number of lines. 42 | size_t gb_nlines(struct gapbuf *gb); 43 | 44 | // Writes the contents of the buffer into fp. 45 | void gb_save(struct gapbuf *gb, FILE *fp); 46 | 47 | // Returns the character at offset pos from the start of the buffer. 48 | char gb_getchar(struct gapbuf *gb, size_t pos); 49 | 50 | // Returns the unicode codepoint at offset pos from the start of the buffer. 51 | uint32_t gb_utf8(struct gapbuf *gb, size_t pos); 52 | // Returns the utf8 length of the character at offset pos from the start of the buffer. 53 | int gb_utf8len(struct gapbuf *gb, size_t pos); 54 | // Returns the offset of the next utf8 start byte after pos. 55 | size_t gb_utf8next(struct gapbuf *gb, size_t pos); 56 | // Returns the offset of the previous utf8 start byte before pos. 57 | size_t gb_utf8prev(struct gapbuf *gb, size_t pos); 58 | // Returns the number of the unicode codepoints for the given line. 59 | size_t gb_utf8len_line(struct gapbuf *gb, size_t line_pos); 60 | 61 | // Reads n characters starting at offset pos. 62 | struct buf *gb_getstring(struct gapbuf *gb, size_t pos, size_t n); 63 | 64 | // Like gb_getstring, but read into a buffer instead of allocating. 65 | void gb_getstring_into(struct gapbuf *gb, size_t pos, size_t n, char *buf); 66 | 67 | // Returns the line the given pos is on. 68 | struct buf *gb_getline(struct gapbuf *gb, size_t pos); 69 | 70 | // Moves the gap so that gb->bufstart + pos == gb->gapstart. 71 | void gb_mvgap(struct gapbuf *gb, size_t pos); 72 | 73 | // Insert a single character after offset pos. 74 | void gb_putchar(struct gapbuf *gb, char c, size_t pos); 75 | // Insert a string of n characters after offset pos. 76 | void gb_putstring(struct gapbuf *gb, char *buf, size_t n, size_t pos); 77 | 78 | // Remove the n characters before offset pos. 79 | void gb_del(struct gapbuf *gb, size_t n, size_t pos); 80 | 81 | // Return the offset of the first occurrence of c in the buffer, starting at 82 | // offset start, or gb_size(gb) if c is not found. 83 | size_t gb_indexof(struct gapbuf *gb, char c, size_t start); 84 | // Return the offset of the last occurrence of c in the buffer, starting at 85 | // offset start, or -1 if c is not found. 86 | ssize_t gb_lastindexof(struct gapbuf *gb, char c, size_t start); 87 | 88 | // Converts a buffer offset into a line number, and offset within that line. 89 | void gb_pos_to_linecol(struct gapbuf *gb, size_t pos, size_t *line, size_t *offset); 90 | // Vice versa. 91 | size_t gb_linecol_to_pos(struct gapbuf *gb, size_t line, size_t offset); 92 | -------------------------------------------------------------------------------- /visual_mode.c: -------------------------------------------------------------------------------- 1 | #include "mode.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "buf.h" 9 | #include "buffer.h" 10 | #include "gap.h" 11 | #include "editor.h" 12 | #include "motion.h" 13 | #include "util.h" 14 | #include "window.h" 15 | 16 | void visual_mode_entered(struct editor *editor) { 17 | struct visual_mode *mode = editor_get_visual_mode(editor); 18 | mode->kind = (enum visual_mode_kind) mode->mode.arg; 19 | // If we're entering the mode by popping (e.g. because we did a search and 20 | // finished), then keep the current anchor. 21 | if (!mode->cursor) { 22 | mode->cursor = window_cursor(editor->window); 23 | editor->window->visual_mode_selection = &mode->selection; 24 | visual_mode_selection_update(editor); 25 | } 26 | if (editor->opt.showmode) { 27 | editor_status_msg(editor, "-- VISUAL --"); 28 | } 29 | } 30 | 31 | void visual_mode_exited(struct editor *editor) { 32 | struct visual_mode *mode = editor_get_visual_mode(editor); 33 | mode->cursor = 0; 34 | editor->window->visual_mode_selection = NULL; 35 | buf_clear(editor->status); 36 | } 37 | 38 | void visual_mode_key_pressed(struct editor* editor, struct tb_event* ev) { 39 | if (ev->ch != '0' && isdigit((int) ev->ch)) { 40 | editor->count = 0; 41 | while (isdigit((int) ev->ch)) { 42 | editor->count *= 10; 43 | editor->count += ev->ch - '0'; 44 | editor_waitkey(editor, ev); 45 | } 46 | } 47 | 48 | if (ev->ch == '"') { 49 | editor_waitkey(editor, ev); 50 | char name = (char) tolower((int) ev->ch); 51 | if (editor_get_register(editor, name)) { 52 | editor->register_ = name; 53 | } 54 | return; 55 | } 56 | 57 | switch (ev->key) { 58 | case TB_KEY_ESC: case TB_KEY_CTRL_C: 59 | editor_pop_mode(editor); 60 | return; 61 | case TB_KEY_CTRL_B: 62 | window_page_up(editor->window); 63 | break; 64 | case TB_KEY_CTRL_F: 65 | window_page_down(editor->window); 66 | break; 67 | default: { 68 | struct motion *motion = motion_get(editor, ev); 69 | if (motion) { 70 | window_set_cursor(editor->window, motion_apply(motion, editor)); 71 | return; 72 | } 73 | op_func *op = op_find((char) ev->ch); 74 | if (op) { 75 | struct region *selection = editor->window->visual_mode_selection; 76 | editor_pop_mode(editor); 77 | op(editor, selection); 78 | } 79 | } 80 | } 81 | } 82 | 83 | // FIXME(ibadawi): Duplicated from motion.c 84 | static bool is_line_start(struct gapbuf *gb, size_t pos) { 85 | return pos == 0 || gb_getchar(gb, pos - 1) == '\n'; 86 | } 87 | 88 | static bool is_line_end(struct gapbuf *gb, size_t pos) { 89 | return pos == gb_size(gb) - 1 || gb_getchar(gb, pos) == '\n'; 90 | } 91 | 92 | static size_t line_start(struct gapbuf *gb, size_t pos) { 93 | if (is_line_start(gb, pos)) { 94 | return pos; 95 | } 96 | return (size_t) (gb_lastindexof(gb, '\n', pos - 1) + 1); 97 | } 98 | 99 | static size_t line_end(struct gapbuf *gb, size_t pos) { 100 | if (is_line_end(gb, pos)) { 101 | return pos; 102 | } 103 | return gb_indexof(gb, '\n', pos); 104 | } 105 | 106 | void visual_mode_selection_update(struct editor *editor) { 107 | struct visual_mode *mode = (struct visual_mode*) editor->mode; 108 | while (mode && mode != &editor->modes.visual) { 109 | mode = (struct visual_mode*) mode->mode.parent; 110 | } 111 | if (!mode) { 112 | return; 113 | } 114 | 115 | struct region *selection = &mode->selection; 116 | region_set(selection, window_cursor(editor->window), mode->cursor); 117 | 118 | struct gapbuf *gb = editor->window->buffer->text; 119 | if (mode->kind == VISUAL_MODE_LINEWISE) { 120 | selection->start = line_start(gb, selection->start); 121 | selection->end = line_end(gb, selection->end); 122 | } 123 | if (mode->kind == VISUAL_MODE_LINEWISE || 124 | !is_line_end(gb, selection->end)) { 125 | ++selection->end; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/tags.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | #include "tags.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "buf.h" 11 | #include "buffer.h" 12 | #include "gap.h" 13 | #include "editor.h" 14 | #include "util.h" 15 | #include "window.h" 16 | 17 | #include "asserts.h" 18 | 19 | static struct editor *editor = NULL; 20 | static struct tags *tags = NULL; 21 | 22 | static char *type(const char *keys) { 23 | editor_send_keys(editor, keys); 24 | return editor->status->buf; 25 | } 26 | 27 | void test_tags__initialize(void) { 28 | cl_fixture_sandbox("tags.c"); 29 | cl_fixture_sandbox("tags"); 30 | tb_init(); 31 | editor = editor_create(tb_width(), tb_height()); 32 | tags = editor->tags; 33 | cl_assert(tags && tags->len > 0); 34 | } 35 | 36 | void test_tags__cleanup(void) { 37 | editor_free(editor); 38 | cl_fixture_cleanup("tags"); 39 | } 40 | 41 | void test_tags__find(void) { 42 | struct tag *tag = tags_find(tags, "foo"); 43 | cl_assert(tag); 44 | cl_assert_equal_s("foo", tag->name); 45 | cl_assert_equal_s("tags.c", tag->path); 46 | cl_assert_equal_s("/^void foo\\(void\\) \\{$", tag->cmd); 47 | 48 | struct tag *garbage = tags_find(tags, "garbage"); 49 | cl_assert(!garbage); 50 | } 51 | 52 | void test_tags__updated(void) { 53 | time_t first_loaded = --tags->loaded_at; 54 | utimes(tags->file, NULL); 55 | tags_find(tags, "foo"); 56 | cl_assert(tags->loaded_at > first_loaded); 57 | } 58 | 59 | void test_tags__deleted(void) { 60 | cl_assert(tags->len > 0); 61 | remove(tags->file); 62 | struct tag *tag = tags_find(tags, "foo"); 63 | cl_assert(!tag); 64 | cl_assert_equal_i(tags->len, 0); 65 | } 66 | 67 | #define assert_editor_error(msg) do { \ 68 | cl_assert_equal_i(editor->status_error, true); \ 69 | cl_assert_equal_s(editor->status->buf, msg); \ 70 | } while (0) 71 | 72 | void test_tags__editor_tag_stack(void) { 73 | editor_jump_to_tag(editor, "garbage"); 74 | assert_editor_error("tag not found: garbage"); 75 | 76 | editor_tag_stack_prev(editor); 77 | assert_editor_error("tag stack empty"); 78 | editor_tag_stack_next(editor); 79 | assert_editor_error("tag stack empty"); 80 | 81 | // Build up the tag stack 82 | assert_cursor_at(0, 0); 83 | editor_jump_to_tag(editor, "foo"); 84 | assert_cursor_at(9, 0); 85 | editor_jump_to_tag(editor, "bar"); 86 | assert_cursor_at(6, 0); 87 | editor_jump_to_tag(editor, "baz"); 88 | assert_cursor_at(3, 0); 89 | 90 | // Navigate up and down 91 | editor_tag_stack_next(editor); 92 | assert_editor_error("at top of tag stack"); 93 | 94 | editor_tag_stack_prev(editor); 95 | assert_cursor_at(6, 0); 96 | editor_tag_stack_next(editor); 97 | assert_cursor_at(3, 0); 98 | 99 | editor_tag_stack_prev(editor); 100 | editor_tag_stack_prev(editor); 101 | editor_tag_stack_prev(editor); 102 | assert_cursor_at(0, 0); 103 | 104 | editor_tag_stack_prev(editor); 105 | assert_editor_error("at bottom of tag stack"); 106 | 107 | // Jump to a new tag from within the tag stack 108 | editor_tag_stack_next(editor); 109 | editor_tag_stack_next(editor); 110 | assert_cursor_at(6, 0); 111 | editor_jump_to_tag(editor, "quux"); 112 | assert_cursor_at(1, 0); 113 | 114 | // 'baz' (3, 0) no longer on the stack 115 | editor_tag_stack_prev(editor); 116 | assert_cursor_at(6, 0); 117 | editor_tag_stack_next(editor); 118 | assert_cursor_at(1, 0); 119 | editor_tag_stack_next(editor); 120 | assert_editor_error("at top of tag stack"); 121 | } 122 | 123 | void test_tags__completion(void) { 124 | cl_assert_equal_s(type(":tag "), ":tag "); 125 | cl_assert_equal_s(type(""), ":tag bar"); 126 | cl_assert_equal_s(type(""), ":tag baz"); 127 | cl_assert_equal_s(type(""), ":tag foo"); 128 | cl_assert_equal_s(type(""), ":tag quux"); 129 | cl_assert_equal_s(type(""), ":tag bar"); 130 | cl_assert_equal_s(type(":tag f"), ":tag foo"); 131 | type(""); 132 | } 133 | -------------------------------------------------------------------------------- /tests/draw.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | #include "editor.h" 3 | 4 | #include 5 | #include 6 | 7 | #include "buf.h" 8 | 9 | static struct editor *editor = NULL; 10 | static struct tb_cell *cells = NULL; 11 | char line_buffer[80]; 12 | 13 | static void type(const char *keys) { 14 | editor_send_keys(editor, keys); 15 | } 16 | 17 | static char color(uint32_t color) { 18 | switch (color) { 19 | case TB_DEFAULT: return '.'; 20 | case TB_BLACK: return 'b'; 21 | case TB_RED: return 'r'; 22 | case TB_YELLOW: return 'y'; 23 | case TB_BLUE: return 'b'; 24 | case 0x07: return 'w'; 25 | case TB_DARK_GRAY: return 'g'; 26 | default: return '?'; 27 | } 28 | } 29 | 30 | static char chars(struct tb_cell *cell) { return cell->ch ? cell->ch : '.'; } 31 | static char fgcolors(struct tb_cell *cell) { return color(cell->fg); } 32 | static char bgcolors(struct tb_cell *cell) { return color(cell->bg); } 33 | 34 | #define assert_line(number, func, expected) do { \ 35 | struct tb_cell *line = &cells[tb_width() * (number)]; \ 36 | size_t len = strlen(expected); \ 37 | size_t i; \ 38 | for (i = 0; i < len; ++i) { \ 39 | line_buffer[i] = func(&line[i]); \ 40 | } \ 41 | line_buffer[i] = '\0'; \ 42 | cl_assert_equal_s(expected, line_buffer); \ 43 | } while (0) 44 | 45 | void test_draw__initialize(void) { 46 | tb_init(); 47 | cells = tb_cell_buffer(); 48 | editor = editor_create(tb_width(), tb_height()); 49 | } 50 | 51 | void test_draw__cleanup(void) { 52 | editor_free(editor); 53 | } 54 | 55 | void test_draw__simple_text(void) { 56 | type("ihello, world!"); 57 | editor_draw(editor); 58 | assert_line(0, chars, "hello, world!"); 59 | assert_line(0, fgcolors, "wwwwwwwwwwwww"); 60 | assert_line(0, bgcolors, "............."); 61 | } 62 | 63 | void test_draw__visual_mode(void) { 64 | type("ihello, world!0lvw"); 65 | editor_draw(editor); 66 | assert_line(0, chars, "hello, world!"); 67 | assert_line(0, fgcolors, "wwwwwbwwwwwww"); 68 | assert_line(0, bgcolors, ".ggggw......."); 69 | } 70 | 71 | void test_draw__linewise_visual_mode(void) { 72 | type("ihello, world!0lVw"); 73 | editor_draw(editor); 74 | assert_line(0, chars, "hello, world!"); 75 | assert_line(0, fgcolors, "wwwwwbwwwwwww"); 76 | assert_line(0, bgcolors, "gggggwggggggg"); 77 | } 78 | 79 | void test_draw__number(void) { 80 | type(":set number"); 81 | type("ihello, world!"); 82 | editor_draw(editor); 83 | assert_line(0, chars, " 1.hello, world!"); 84 | assert_line(0, fgcolors, "yyy.wwwwwwwwwwwww"); 85 | assert_line(0, bgcolors, "................."); 86 | } 87 | 88 | void test_draw__hlsearch(void) { 89 | type(":set hlsearch"); 90 | type("ihello, word world!0"); 91 | type("/wor"); 92 | editor_draw(editor); 93 | assert_line(0, chars, "hello, word world!"); 94 | assert_line(0, fgcolors, "wwwwwwwbbbwwbbbwww"); 95 | assert_line(0, bgcolors, ".......wyy..yyy..."); 96 | } 97 | 98 | void test_draw__message(void) { 99 | type("i1234gg"); 100 | editor_draw(editor); 101 | assert_line(0, chars, "1"); 102 | assert_line(1, chars, "2"); 103 | assert_line(2, chars, "3"); 104 | assert_line(3, chars, "4"); 105 | assert_line(4, chars, "~"); 106 | assert_line(5, chars, "~"); 107 | assert_line(editor->height - 3, chars, "~....."); 108 | assert_line(editor->height - 2, chars, "~....."); 109 | 110 | buf_printf(editor->message, "hello, world!\nsecond line"); 111 | editor_draw(editor); 112 | assert_line(0, chars, "3"); 113 | assert_line(1, chars, "4"); 114 | assert_line(2, chars, "~"); 115 | assert_line(3, chars, "~"); 116 | assert_line(editor->height - 3, chars, "hello, world!"); 117 | assert_line(editor->height - 2, chars, "second line"); 118 | 119 | buf_clear(editor->message); 120 | editor_draw(editor); 121 | assert_line(0, chars, "1"); 122 | assert_line(1, chars, "2"); 123 | assert_line(2, chars, "3"); 124 | assert_line(3, chars, "4"); 125 | assert_line(4, chars, "~"); 126 | assert_line(5, chars, "~"); 127 | assert_line(editor->height - 3, chars, "~....."); 128 | assert_line(editor->height - 2, chars, "~....."); 129 | } 130 | -------------------------------------------------------------------------------- /util.c: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "buf.h" 15 | 16 | void debug(const char *format, ...) { 17 | static FILE *debug_fp = NULL; 18 | if (!debug_fp) { 19 | time_t timestamp = time(0); 20 | struct tm *now = localtime(×tamp); 21 | char name[64]; 22 | strftime(name, sizeof(name), "/tmp/badavi_log.%Y%m%d-%H%M%S.txt", now); 23 | debug_fp = fopen(name, "w"); 24 | assert(debug_fp); 25 | } 26 | 27 | va_list args; 28 | va_start(args, format); 29 | vfprintf(debug_fp, format, args); 30 | va_end(args); 31 | fflush(debug_fp); 32 | } 33 | 34 | void *xmalloc(size_t size) { 35 | void *mem = malloc(size); 36 | if (!mem) { 37 | fprintf(stderr, "failed to allocate memory (%zu bytes)", size); 38 | abort(); 39 | } 40 | return mem; 41 | } 42 | 43 | void *xrealloc(void *ptr, size_t size) { 44 | void *mem = realloc(ptr, size); 45 | if (size && !mem) { 46 | fprintf(stderr, "failed to allocate memory (%zu bytes)", size); 47 | abort(); 48 | } 49 | return mem; 50 | } 51 | 52 | char *xstrdup(const char *s) { 53 | char *copy = strdup(s); 54 | if (!copy) { 55 | fprintf(stderr, "failed to allocate memory (%zu bytes)", strlen(s) + 1); 56 | abort(); 57 | } 58 | return copy; 59 | } 60 | 61 | bool strtoi(char *s, int *result) { 62 | char *nptr; 63 | long val = strtol(s, &nptr, 10); 64 | if (*nptr == '\0') { 65 | *result = (int) val; 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | char *strrep(char *s, char *from, char *to) { 72 | struct buf *buf = buf_create(strlen(s)); 73 | 74 | size_t fromlen = strlen(from); 75 | for (char *p = s; *p; ++p) { 76 | if (!strncmp(p, from, fromlen)) { 77 | buf_append(buf, to); 78 | p += fromlen - 1; 79 | } else { 80 | buf_append_char(buf, *p); 81 | } 82 | } 83 | 84 | char *result = buf->buf; 85 | free(buf); 86 | return result; 87 | } 88 | 89 | size_t strcnt(char *s, char c) { 90 | size_t cnt = 0; 91 | for (char *p = s; *p; ++p) { 92 | if (*p == c) { 93 | ++cnt; 94 | } 95 | } 96 | return cnt; 97 | } 98 | 99 | char *abspath(const char *path) { 100 | char buf[PATH_MAX]; 101 | if (*path == '~') { 102 | strcpy(buf, homedir()); 103 | strcat(buf, path + 1); 104 | } else if (*path != '/') { 105 | getcwd(buf, sizeof(buf)); 106 | strcat(buf, "/"); 107 | strcat(buf, path); 108 | } else { 109 | strcpy(buf, path); 110 | } 111 | 112 | char *result = xmalloc(strlen(buf) + 1); 113 | strcpy(result, "/"); 114 | 115 | if (!strcmp(path, "/")) { 116 | return result; 117 | } 118 | 119 | char *p = result; 120 | char *prevslash = result; 121 | char *component = strtok(buf, "/"); 122 | do { 123 | if (!strcmp(component, "") || 124 | !strcmp(component, ".")) { 125 | continue; 126 | } 127 | if (!strcmp(component, "..")) { 128 | p = prevslash; 129 | prevslash = result; 130 | if (p != result) { 131 | *p = '\0'; 132 | prevslash = strrchr(result, '/'); 133 | } else { 134 | p[1] = '\0'; 135 | } 136 | continue; 137 | } 138 | prevslash = p; 139 | *p = '/'; 140 | size_t len = strlen(component); 141 | memcpy(p + 1, component, len); 142 | p += len + 1; 143 | *p = '\0'; 144 | } while ((component = strtok(NULL, "/"))); 145 | 146 | return result; 147 | } 148 | 149 | const char *relpath(const char *path, const char *start) { 150 | size_t startlen = strlen(start); 151 | assert(start[startlen - 1] != '/'); 152 | if (!strncmp(path, start, startlen)) { 153 | const char *rel = path + startlen; 154 | return *rel == '/' ? rel + 1 : rel; 155 | } 156 | return path; 157 | } 158 | 159 | const char *homedir(void) { 160 | const char *home = getenv("HOME"); 161 | return home ? home : getpwuid(getuid())->pw_dir; 162 | } 163 | 164 | struct region *region_set(struct region *region, size_t start, size_t end) { 165 | region->start = min(start, end); 166 | region->end = max(start, end); 167 | return region; 168 | } 169 | -------------------------------------------------------------------------------- /tests/window.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | #include "window.h" 3 | 4 | #include "buffer.h" 5 | 6 | #define assert_window_whxy(window, w, h, x, y) do { \ 7 | cl_assert_equal_i(window_w(window), w); \ 8 | cl_assert_equal_i(window_h(window), h); \ 9 | cl_assert_equal_i(window_x(window), x); \ 10 | cl_assert_equal_i(window_y(window), y); \ 11 | } while (0) 12 | 13 | struct buffer *buffer = NULL; 14 | struct window *window = NULL; 15 | 16 | void test_window__initialize(void) { 17 | buffer = buffer_create(NULL); 18 | window = window_create(buffer, 80, 16); 19 | } 20 | 21 | void test_window__cleanup(void) { 22 | window_free(window); 23 | buffer_free(buffer); 24 | } 25 | 26 | void test_window__create(void) { 27 | cl_assert(!window->parent); 28 | cl_assert_equal_i(window->split_type, WINDOW_LEAF); 29 | assert_window_whxy(window, 80, 16, 0, 0); 30 | 31 | cl_assert(!window_left(window)); 32 | cl_assert(!window_right(window)); 33 | cl_assert(!window_down(window)); 34 | cl_assert(!window_up(window)); 35 | } 36 | 37 | void test_window__vsplit(void) { 38 | struct window *left = window_split(window, WINDOW_SPLIT_LEFT); 39 | cl_assert_equal_p(left, window->split.first); 40 | struct window *right = window->split.second; 41 | 42 | cl_assert_equal_i(window->split_type, WINDOW_SPLIT_VERTICAL); 43 | cl_assert_equal_p(left->parent, window); 44 | cl_assert_equal_p(right->parent, window); 45 | 46 | cl_assert_equal_i(left->split_type, WINDOW_LEAF); 47 | assert_window_whxy(left, 40, 16, 0, 0); 48 | 49 | cl_assert_equal_i(right->split_type, WINDOW_LEAF); 50 | assert_window_whxy(right, 40, 16, 40, 0); 51 | 52 | cl_assert_equal_p(left, window_left(right)); 53 | cl_assert_equal_p(right, window_right(left)); 54 | } 55 | 56 | void test_window__split(void) { 57 | struct window *up = window_split(window, WINDOW_SPLIT_ABOVE); 58 | cl_assert_equal_p(up, window->split.first); 59 | struct window *down = window->split.second; 60 | 61 | cl_assert_equal_i(window->split_type, WINDOW_SPLIT_HORIZONTAL); 62 | cl_assert_equal_p(up->parent, window); 63 | cl_assert_equal_p(down->parent, window); 64 | 65 | cl_assert_equal_i(up->split_type, WINDOW_LEAF); 66 | assert_window_whxy(up, 80, 8, 0, 0); 67 | 68 | cl_assert_equal_i(down->split_type, WINDOW_LEAF); 69 | assert_window_whxy(down, 80, 8, 0, 8); 70 | 71 | cl_assert_equal_p(up, window_up(down)); 72 | cl_assert_equal_p(down, window_down(up)); 73 | } 74 | 75 | void test_window__close(void) { 76 | window_split(window, WINDOW_SPLIT_ABOVE); 77 | 78 | struct window *old_window = window->split.second; 79 | struct window *next = window_close(window->split.second); 80 | cl_assert(!next->parent); 81 | cl_assert_equal_i(next->split_type, WINDOW_LEAF); 82 | assert_window_whxy(next, 80, 16, 0, 0); 83 | window_free(old_window); 84 | } 85 | 86 | void test_window__resize(void) { 87 | // _________ 88 | // | | | 89 | // | A | | 90 | // |---| C | 91 | // | B | | 92 | // |___|___| 93 | struct window *left = window_split(window, WINDOW_SPLIT_LEFT); 94 | struct window *A = window_split(left, WINDOW_SPLIT_ABOVE); 95 | struct window *B = left->split.second; 96 | struct window *C = window->split.second; 97 | 98 | cl_assert_equal_p(B, window_down(A)); 99 | cl_assert_equal_p(A, window_up(B)); 100 | cl_assert_equal_p(C, window_right(A)); 101 | cl_assert_equal_p(C, window_right(B)); 102 | 103 | assert_window_whxy(A, 40, 8, 0, 0); 104 | assert_window_whxy(B, 40, 8, 0, 8); 105 | assert_window_whxy(C, 40, 16, 40, 0); 106 | 107 | window_resize(A, 0, 5); 108 | assert_window_whxy(A, 40, 13, 0, 0); 109 | assert_window_whxy(B, 40, 3, 0, 13); 110 | assert_window_whxy(C, 40, 16, 40, 0); 111 | 112 | window_resize(B, 0, 10); 113 | assert_window_whxy(A, 40, 3, 0, 0); 114 | assert_window_whxy(B, 40, 13, 0, 3); 115 | assert_window_whxy(C, 40, 16, 40, 0); 116 | 117 | window_resize(A, 20, 0); 118 | assert_window_whxy(A, 60, 3, 0, 0); 119 | assert_window_whxy(B, 60, 13, 0, 3); 120 | assert_window_whxy(C, 20, 16, 60, 0); 121 | 122 | window_resize(C, 40, 0); 123 | assert_window_whxy(A, 20, 3, 0, 0); 124 | assert_window_whxy(B, 20, 13, 0, 3); 125 | assert_window_whxy(C, 60, 16, 20, 0); 126 | 127 | window_equalize(window, WINDOW_SPLIT_HORIZONTAL); 128 | assert_window_whxy(A, 20, 8, 0, 0); 129 | assert_window_whxy(B, 20, 8, 0, 8); 130 | assert_window_whxy(C, 60, 16, 20, 0); 131 | 132 | window_equalize(window, WINDOW_SPLIT_VERTICAL); 133 | assert_window_whxy(A, 40, 8, 0, 0); 134 | assert_window_whxy(B, 40, 8, 0, 8); 135 | assert_window_whxy(C, 40, 16, 40, 0); 136 | } 137 | -------------------------------------------------------------------------------- /search.c: -------------------------------------------------------------------------------- 1 | #include "search.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "buf.h" 11 | #include "buffer.h" 12 | #include "editor.h" 13 | #include "gap.h" 14 | #include "util.h" 15 | #include "window.h" 16 | 17 | bool editor_ignore_case(struct editor *editor, char *pattern) { 18 | if (!editor->opt.ignorecase) { 19 | return false; 20 | } 21 | if (!editor->opt.smartcase) { 22 | return true; 23 | } 24 | 25 | for (char *p = pattern; *p; ++p) { 26 | if (isupper((int) *p)) { 27 | return false; 28 | } 29 | } 30 | return true; 31 | } 32 | 33 | int search_init(struct search *search, char *pattern, bool ignore_case, 34 | char *str, size_t len) { 35 | memset(search, 0, sizeof(*search)); 36 | 37 | int flags = PCRE2_MULTILINE; 38 | if (ignore_case) { 39 | flags |= PCRE2_CASELESS; 40 | } 41 | int errorcode = 0; 42 | PCRE2_SIZE erroroffset = 0; 43 | search->regex = pcre2_compile( 44 | (unsigned char*) pattern, PCRE2_ZERO_TERMINATED, 45 | flags, &errorcode, &erroroffset, NULL); 46 | if (!search->regex) { 47 | assert(errorcode != 0); 48 | return errorcode; 49 | } 50 | search->groups = pcre2_match_data_create_from_pattern(search->regex, NULL); 51 | 52 | search->str = (unsigned char*)str; 53 | search->len = len; 54 | search->start = 0; 55 | return 0; 56 | } 57 | 58 | void search_deinit(struct search *search) { 59 | pcre2_code_free(search->regex); 60 | pcre2_match_data_free(search->groups); 61 | } 62 | 63 | void search_get_error(int error, char *buf, size_t buflen) { 64 | pcre2_get_error_message(error, (unsigned char*)buf, buflen); 65 | } 66 | 67 | bool search_next_match(struct search *search, struct region *match) { 68 | // In multiline mode, pcre2 assumes the string is at the beginning of a 69 | // line unless told otherwise. This affects patterns that use ^. 70 | int flags = 0; 71 | if (search->start > 0 && search->str[search->start - 1] != '\n') { 72 | flags |= PCRE2_NOTBOL; 73 | } 74 | 75 | int rc = pcre2_match( 76 | search->regex, search->str, search->len, 77 | search->start, flags, search->groups, NULL); 78 | 79 | if (rc > 0) { 80 | PCRE2_SIZE *offsets = pcre2_get_ovector_pointer(search->groups); 81 | region_set(match, offsets[0], offsets[1]); 82 | search->start += max(1, offsets[1] - search->start); 83 | return true; 84 | } 85 | return false; 86 | } 87 | 88 | bool editor_search(struct editor *editor, char *pattern, 89 | size_t start, enum search_direction direction, struct region *match) { 90 | if (!pattern) { 91 | struct editor_register *reg = editor_get_register(editor, '/'); 92 | assert(reg->buf); 93 | pattern = reg->buf->buf; 94 | if (!*pattern) { 95 | editor_status_err(editor, "No previous regular expression"); 96 | return NULL; 97 | } 98 | } 99 | 100 | struct gapbuf *gb = editor->window->buffer->text; 101 | // Move the gap so the searched region is contiguous. 102 | gb_mvgap(gb, 0); 103 | bool ignore_case = editor_ignore_case(editor, pattern); 104 | 105 | struct search search; 106 | int rc = search_init(&search, pattern, ignore_case, gb->gapend, gb_size(gb)); 107 | 108 | if (rc) { 109 | char error[48]; 110 | search_get_error(rc, error, sizeof(error)); 111 | editor_status_err(editor, "Bad regex \"%s\": %s", pattern, error); 112 | return false; 113 | } 114 | 115 | #define return search_deinit(&search); return 116 | 117 | if (direction == SEARCH_FORWARDS) { 118 | search.start = start + 1; 119 | if (search_next_match(&search, match)) { 120 | return true; 121 | } 122 | 123 | editor_status_msg(editor, "search hit BOTTOM, continuing at TOP"); 124 | search.start = 0; 125 | if (search_next_match(&search, match)) { 126 | return true; 127 | } 128 | 129 | editor_status_err(editor, "Pattern not found: \"%s\"", pattern); 130 | return false; 131 | } 132 | 133 | assert(direction == SEARCH_BACKWARDS); 134 | 135 | if (!search_next_match(&search, match)) { 136 | editor_status_err(editor, "Pattern not found: \"%s\"", pattern); 137 | return false; 138 | } 139 | 140 | if (match->start >= start) { 141 | editor_status_msg(editor, "search hit TOP, continuing at BOTTOM"); 142 | while (search_next_match(&search, match)) {} 143 | return true; 144 | } 145 | 146 | struct region prev = *match; 147 | while (match->start < start) { 148 | prev = *match; 149 | if (!search_next_match(&search, match)) { 150 | return true; 151 | } 152 | } 153 | *match = prev; 154 | return true; 155 | #undef return 156 | } 157 | 158 | void editor_jump_to_match(struct editor *editor, char *pattern, 159 | size_t start, enum search_direction direction) { 160 | struct region match; 161 | if (editor_search(editor, pattern, start, direction, &match)) { 162 | window_set_cursor(editor->window, match.start); 163 | } else { 164 | window_set_cursor(editor->window, window_cursor(editor->window)); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /buf.c: -------------------------------------------------------------------------------- 1 | #include "buf.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "util.h" 12 | 13 | void buf_grow(struct buf *buf, size_t cap) { 14 | buf->buf = xrealloc(buf->buf, cap); 15 | buf->cap = cap; 16 | } 17 | 18 | struct buf *buf_create(size_t cap) { 19 | struct buf *buf = xmalloc(sizeof(*buf)); 20 | 21 | buf->buf = NULL; 22 | buf->cap = 0; 23 | buf_grow(buf, cap); 24 | 25 | buf_clear(buf); 26 | buf->cap = cap; 27 | return buf; 28 | } 29 | 30 | struct buf *buf_from_cstr(char *s) { 31 | struct buf *buf = buf_create(max(1, strlen(s)) * 2); 32 | buf_append(buf, s); 33 | return buf; 34 | } 35 | 36 | struct buf *buf_from_char(char c) { 37 | char s[2] = {c, '\0'}; 38 | return buf_from_cstr(s); 39 | } 40 | 41 | struct buf *buf_from_utf8(uint32_t ch) { 42 | char s[8]; 43 | int len = tb_utf8_unicode_to_char(s, ch); 44 | s[len] = '\0'; 45 | return buf_from_cstr(s); 46 | } 47 | 48 | struct buf *buf_copy(struct buf *buf) { 49 | return buf_from_cstr(buf->buf); 50 | } 51 | 52 | void buf_free(struct buf *buf) { 53 | free(buf->buf); 54 | free(buf); 55 | } 56 | 57 | void buf_clear(struct buf *buf) { 58 | buf->buf[0] = '\0'; 59 | buf->len = 0; 60 | } 61 | 62 | void buf_insert(struct buf *buf, char *s, size_t pos) { 63 | size_t len = strlen(s); 64 | size_t new_len = buf->len + len; 65 | 66 | size_t new_cap = buf->cap; 67 | if (new_len + 1 >= new_cap) { 68 | while (new_len + 1 >= new_cap) { 69 | new_cap *= 2; 70 | } 71 | buf_grow(buf, new_cap); 72 | } 73 | 74 | // Move the bit after the insertion... 75 | memmove( 76 | buf->buf + pos + len, 77 | buf->buf + pos, 78 | buf->len - pos); 79 | 80 | // Stitch the new part in. 81 | memmove( 82 | buf->buf + pos, 83 | s, 84 | len); 85 | 86 | buf->len += len; 87 | buf->buf[buf->len] = '\0'; 88 | } 89 | 90 | void buf_append(struct buf *buf, char *s) { 91 | buf_insert(buf, s, buf->len); 92 | } 93 | 94 | void buf_append_char(struct buf *buf, char c) { 95 | char s[2] = {c, '\0'}; 96 | buf_append(buf, s); 97 | } 98 | 99 | void buf_delete(struct buf *buf, size_t pos, size_t len) { 100 | if (pos >= buf->len || pos + len > buf->len) { 101 | return; 102 | } 103 | 104 | memmove( 105 | buf->buf + pos, 106 | buf->buf + pos + len, 107 | buf->len - (pos + len)); 108 | 109 | buf->len -= len; 110 | buf->buf[buf->len] = '\0'; 111 | } 112 | 113 | void buf_printf(struct buf *buf, const char *format, ...) { 114 | va_list args; 115 | va_start(args, format); 116 | buf_vprintf(buf, format, args); 117 | va_end(args); 118 | } 119 | 120 | void buf_appendf(struct buf *buf, const char *format, ...) { 121 | struct buf *suffix = buf_create(1); 122 | va_list args; 123 | va_start(args, format); 124 | buf_vprintf(suffix, format, args); 125 | va_end(args); 126 | buf_append(buf, suffix->buf); 127 | buf_free(suffix); 128 | } 129 | 130 | void buf_vprintf(struct buf *buf, const char *format, va_list args) { 131 | va_list args_copy; 132 | va_copy(args_copy, args); 133 | // Try once... 134 | size_t n = (size_t) vsnprintf(buf->buf, buf->cap, format, args); 135 | 136 | // vsnprintf returns the required size if it wasn't enough, so grow to that 137 | // size and try again. 138 | if (n >= buf->cap) { 139 | buf_grow(buf, n + 1); 140 | n = (size_t) vsnprintf(buf->buf, buf->cap, format, args_copy); 141 | } 142 | 143 | va_end(args_copy); 144 | buf->len = n; 145 | } 146 | 147 | bool buf_equals(struct buf *buf, char *s) { 148 | return !strcmp(buf->buf, s); 149 | } 150 | 151 | bool buf_startswith(struct buf *buf, char *prefix) { 152 | return !strncmp(buf->buf, prefix, strlen(prefix)); 153 | } 154 | 155 | bool buf_endswith(struct buf *buf, char *suffix) { 156 | size_t suffix_len = strlen(suffix); 157 | if (buf->len < suffix_len) { 158 | return false; 159 | } 160 | return !strcmp(buf->buf + (buf->len - suffix_len), suffix); 161 | } 162 | 163 | void buf_strip_whitespace(struct buf *buf) { 164 | while (buf->len && isspace(buf->buf[0])) { 165 | buf_delete(buf, 0, 1); 166 | } 167 | while (buf->len && isspace(buf->buf[buf->len - 1])) { 168 | buf_delete(buf, buf->len - 1, 1); 169 | } 170 | } 171 | 172 | static void intbuf_grow(struct intbuf *buf, size_t cap) { 173 | buf->buf = xrealloc(buf->buf, cap * sizeof(*buf->buf)); 174 | buf->cap = cap; 175 | } 176 | 177 | struct intbuf *intbuf_create(size_t cap) { 178 | struct intbuf *buf = xmalloc(sizeof(*buf)); 179 | 180 | buf->buf = NULL; 181 | buf->cap = 0; 182 | intbuf_grow(buf, cap); 183 | 184 | buf->len = 0; 185 | buf->cap = cap; 186 | return buf; 187 | } 188 | 189 | void intbuf_free(struct intbuf *buf) { 190 | free(buf->buf); 191 | free(buf); 192 | } 193 | 194 | void intbuf_insert(struct intbuf *buf, unsigned int i, size_t pos) { 195 | if (buf->len == buf->cap) { 196 | intbuf_grow(buf, 2 * buf->cap); 197 | } 198 | 199 | memmove(buf->buf + pos + 1, buf->buf + pos, (buf->len++ - pos) * sizeof(*buf->buf)); 200 | buf->buf[pos] = i; 201 | } 202 | 203 | void intbuf_add(struct intbuf *buf, unsigned int i) { 204 | intbuf_insert(buf, i, buf->len); 205 | } 206 | 207 | void intbuf_remove(struct intbuf *buf, size_t pos) { 208 | memmove(buf->buf + pos, buf->buf + pos + 1, (buf->len-- - pos) * sizeof(*buf->buf)); 209 | } 210 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_DIR := build 2 | 3 | CLAR_DIR := vendor/clar 4 | 5 | TERMBOX_DIR := vendor/termbox 6 | TERMBOX_INSTALL_DIR := $(BUILD_DIR)/termbox 7 | TERMBOX_BUILD_DIR := $(TERMBOX_INSTALL_DIR)/build 8 | TERMBOX_HEADER := $(TERMBOX_INSTALL_DIR)/include/termbox.h 9 | TERMBOX_LIBRARY := $(TERMBOX_INSTALL_DIR)/lib/libtermbox.a 10 | TERMBOX := $(TERMBOX_HEADER) $(TERMBOX_LIBRARY) 11 | 12 | LIBCLIPBOARD_DIR := vendor/libclipboard 13 | LIBCLIPBOARD_INSTALL_DIR := $(BUILD_DIR)/libclipboard 14 | LIBCLIPBOARD_BUILD_DIR := $(LIBCLIPBOARD_INSTALL_DIR)/build 15 | LIBCLIPBOARD_HEADER := $(LIBCLIPBOARD_INSTALL_DIR)/include/libclipboard.h 16 | LIBCLIPBOARD_LIBRARY := $(LIBCLIPBOARD_INSTALL_DIR)/lib/libclipboard.a 17 | LIBCLIPBOARD := $(LIBCLIPBOARD_HEADER) $(LIBCLIPBOARD_LIBRARY) 18 | 19 | PCRE2_DIR := vendor/pcre2 20 | PCRE2_INSTALL_DIR := $(BUILD_DIR)/pcre2 21 | PCRE2_BUILD_DIR := $(PCRE2_INSTALL_DIR)/build 22 | PCRE2_HEADER := $(PCRE2_INSTALL_DIR)/include/pcre2.h 23 | PCRE2_LIBRARY := $(PCRE2_INSTALL_DIR)/lib/libpcre2-8.a 24 | PCRE2 := $(PCRE2_HEADER) $(PCRE2_LIBRARY) 25 | 26 | OS := $(shell uname) 27 | ifeq ($(OS),Darwin) 28 | LIBCLIPBOARD_LDFLAGS := -framework Cocoa 29 | else ifeq ($(OS),Linux) 30 | LIBCLIPBOARD_LDFLAGS := -lxcb -lpthread 31 | endif 32 | LDFLAGS := $(LIBCLIPBOARD_LDFLAGS) 33 | 34 | THIRD_PARTY_HEADERS := $(TERMBOX_HEADER) $(LIBCLIPBOARD_HEADER) $(PCRE2_HEADER) 35 | THIRD_PARTY_LIBRARIES := $(TERMBOX_LIBRARY) $(LIBCLIPBOARD_LIBRARY) $(PCRE2_LIBRARY) 36 | 37 | WARNING_CFLAGS := -Wall -Wextra -Werror 38 | 39 | COVERAGE_CFLAGS := $(if $(COVERAGE),-coverage) 40 | ASAN_CFLAGS := $(if $(ASAN),-fsanitize=address -fsanitize-recover=address) 41 | LDFLAGS += $(COVERAGE_CFLAGS) $(ASAN_CFLAGS) 42 | 43 | # Use C11 for anonymous structs. 44 | COMMON_CFLAGS := -g -std=c11 -D_GNU_SOURCE -DPCRE2_CODE_UNIT_WIDTH=8 \ 45 | $(addprefix -isystem ,$(dir $(THIRD_PARTY_HEADERS))) \ 46 | $(WARNING_CFLAGS) 47 | 48 | CFLAGS := $(COMMON_CFLAGS) $(COVERAGE_CFLAGS) $(ASAN_CFLAGS) 49 | 50 | TEST_CFLAGS := $(COMMON_CFLAGS) -Wno-missing-prototypes \ 51 | -I. -isystem $(CLAR_DIR) -I$(BUILD_DIR)/tests \ 52 | -DCLAR_FIXTURE_PATH=\"$(abspath tests/testdata)\" 53 | 54 | PROG := badavi 55 | SRCS := $(wildcard *.c) 56 | HDRS := $(wildcard *.h) 57 | OBJS := $(SRCS:.c=.o) 58 | OBJS := $(addprefix $(BUILD_DIR)/,$(OBJS)) 59 | DEPS := $(OBJS:.o=.d) 60 | 61 | TEST_PROG := $(PROG)_test 62 | TEST_SRCS := $(wildcard tests/*.c) 63 | TEST_OBJS := $(TEST_SRCS:.c=.o) 64 | TEST_OBJS := $(addprefix $(BUILD_DIR)/,$(TEST_OBJS)) 65 | TEST_DEPS := $(TEST_OBJS:.o=.d) 66 | TEST_OBJS += $(filter-out $(BUILD_DIR)/main.o,$(OBJS)) 67 | TEST_OBJS += $(BUILD_DIR)/tests/clar.o 68 | 69 | .PHONY: $(PROG) 70 | $(PROG): $(BUILD_DIR)/$(PROG) 71 | 72 | .PHONY: $(TEST_PROG) 73 | $(TEST_PROG): $(BUILD_DIR)/$(TEST_PROG) 74 | 75 | .PHONY: test 76 | test: $(BUILD_DIR)/$(TEST_PROG) 77 | ./$^ 78 | 79 | .PHONY: coverage 80 | coverage: 81 | $(MAKE) BUILD_DIR=coverage-build COVERAGE=1 $(TEST_PROG) 82 | lcov -q -c -i -d coverage-build -o coverage-build/coverage.base 83 | ./coverage-build/$(TEST_PROG) 84 | lcov -q -c -d coverage-build -o coverage-build/coverage.run 85 | lcov -q -d . -a coverage-build/coverage.base -a coverage-build/coverage.run -o coverage-build/coverage.total 86 | genhtml -q --no-branch-coverage -o $@ coverage-build/coverage.total 87 | 88 | .PHONY: asan 89 | asan: 90 | $(MAKE) BUILD_DIR=asan-build ASAN=1 $(PROG) 91 | 92 | .PHONY: test-asan 93 | test-asan: 94 | $(MAKE) BUILD_DIR=asan-build ASAN=1 $(TEST_PROG) 95 | ./asan-build/$(TEST_PROG) 96 | 97 | # Enable second expansion to access automatic variables in prerequisite lists. 98 | # In particular, we write $$(@D)/. to refer to the directory of the target. 99 | # Note the trailing dot -- make 3.81 seems to ignore trailing slashes. 100 | .SECONDEXPANSION: 101 | 102 | $(BUILD_DIR)/.: 103 | mkdir -p $@ 104 | 105 | $(BUILD_DIR)%/.: 106 | mkdir -p $@ 107 | 108 | define cmake_dep 109 | $$($(1)_INSTALL_DIR): | $$$$(@D)/. $$($(1)_BUILD_DIR)/. 110 | (cd $$($(1)_BUILD_DIR) && \ 111 | cmake -DCMAKE_INSTALL_PREFIX=$$(abspath $$@) $(2) $$(abspath $$($(1)_DIR)) && \ 112 | $(MAKE) && \ 113 | $(MAKE) install) 114 | 115 | $$($(1)): $$($(1)_INSTALL_DIR) 116 | endef 117 | 118 | $(eval $(call cmake_dep,TERMBOX,-DBUILD_SHARED_LIBS=OFF -DBUILD_DEMOS=OFF)) 119 | $(eval $(call cmake_dep,LIBCLIPBOARD)) 120 | $(eval $(call cmake_dep,PCRE2,-DPCRE2_BUILD_PCRE2GREP=OFF -DPCRE2_BUILD_TESTS=OFF)) 121 | 122 | # We define the rule for test objects first because in GNU make 3.81, when 123 | # multiple pattern rules match a target, the first one is chosen. This is 124 | # different than 3.82 and later, where the most specific one (i.e. the one with 125 | # the shortest stem) is chosen. 126 | $(BUILD_DIR)/tests/%.o: tests/%.c $(THIRD_PARTY_HEADERS) | $$(@D)/. 127 | $(CC) -MMD -MP $(TEST_CFLAGS) -c -o $@ $< 128 | 129 | $(BUILD_DIR)/tests/clar.o: \ 130 | $(CLAR_DIR)/clar.c $(BUILD_DIR)/tests/clar.suite | $$(@D)/. 131 | $(CC) $(TEST_CFLAGS) -w -c -o $@ $< 132 | 133 | $(BUILD_DIR)/tests/clar.suite: $(TEST_SRCS) | $$(@D)/. 134 | $(CLAR_DIR)/generate.py --output=$(@D) tests 135 | 136 | $(BUILD_DIR)/%.o: %.c $(THIRD_PARTY_HEADERS) | $$(@D)/. 137 | $(CC) -MMD -MP -o $@ -c $< $(CFLAGS) 138 | 139 | $(BUILD_DIR)/%.pp: %.c $(THIRD_PARTY_HEADERS) | $$(@D)/. 140 | $(CC) -E -o $@ -c $< $(CFLAGS) 141 | 142 | -include $(DEPS) 143 | -include $(TEST_DEPS) 144 | 145 | $(BUILD_DIR)/$(PROG): $(OBJS) $(THIRD_PARTY_LIBRARIES) | $$(@D)/. 146 | $(CC) -o $@ $^ $(LDFLAGS) 147 | 148 | $(BUILD_DIR)/$(TEST_PROG): $(TEST_OBJS) $(filter-out $(TERMBOX_LIBRARY),$(THIRD_PARTY_LIBRARIES)) | $$(@D)/. 149 | $(CC) -o $@ $^ $(LDFLAGS) 150 | 151 | tags: $(SRCS) $(HDRS) 152 | ctags $^ 153 | -------------------------------------------------------------------------------- /editor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "attrs.h" 10 | #include "history.h" 11 | #include "mode.h" 12 | #include "options.h" 13 | 14 | struct editor_event { 15 | uint8_t type; 16 | uint16_t key; 17 | uint32_t ch; 18 | 19 | TAILQ_ENTRY(editor_event) pointers; 20 | }; 21 | 22 | // The "editor" holds the main state of the program. 23 | struct editor { 24 | // The loaded buffers. 25 | TAILQ_HEAD(buffer_list, buffer) buffers; 26 | 27 | // The focused window. 28 | struct window *window; 29 | 30 | // A stack of editing modes. 31 | struct editing_mode* mode; 32 | 33 | struct { 34 | #define MODE(name) struct name##_mode name; 35 | MODES 36 | #undef MODE 37 | } modes; 38 | 39 | // Global working directory if explicitly set (otherwise NULL). 40 | // This is not necessarily the current working directory of the process. 41 | char *pwd; 42 | 43 | // Optional output above the status bar. 44 | struct buf *message; 45 | // What's written to the status bar. 46 | struct buf *status; 47 | 48 | struct editor_popup { 49 | bool visible; 50 | size_t pos; 51 | char **lines; 52 | int offset; 53 | int selected; 54 | int len; 55 | } popup; 56 | 57 | // Whether the status is an error. 58 | bool status_error; 59 | // Whether the status should be displayed. 60 | bool status_silence; 61 | // The position of the cursor in cmdline mode. 62 | size_t status_cursor; 63 | 64 | // An array of registers (a-z, /, ", * and +). 65 | #define EDITOR_NUM_REGISTERS 30 66 | struct editor_register { 67 | char name; 68 | struct buf *buf; 69 | // Read from the register. Caller must free the returned buffer. 70 | char *(*read)(struct editor_register *reg); 71 | // Write to the register. Makes a copy of the input buffer. 72 | void (*write)(struct editor_register *reg, char*); 73 | } registers[EDITOR_NUM_REGISTERS]; 74 | 75 | // List of loaded ctags. 76 | struct tags *tags; 77 | 78 | // The width and height of the screen. 79 | size_t width; 80 | size_t height; 81 | 82 | bool highlight_search_matches; 83 | 84 | // Temporary input state. 85 | // TODO(isbadawi): This feels like a kludge but I don't know... 86 | unsigned int count; 87 | char register_; 88 | 89 | struct history command_history; 90 | struct history search_history; 91 | 92 | // Synthetic input events generated by editor_send_keys. 93 | // These take precedence over real events emitted by termbox. 94 | TAILQ_HEAD(event_list, editor_event) synthetic_events; 95 | 96 | struct { 97 | #define OPTION(name, type, _) type name; 98 | BUFFER_OPTIONS 99 | EDITOR_OPTIONS 100 | #undef OPTION 101 | } opt; 102 | }; 103 | 104 | struct editor *editor_create(size_t width, size_t height); 105 | struct editor *editor_create_and_open(size_t width, size_t height, char *path); 106 | void editor_free(struct editor *editor); 107 | 108 | void editor_open(struct editor *editor, char *path); 109 | 110 | void editor_set_window(struct editor *editor, struct window *window); 111 | 112 | void editor_push_mode(struct editor *editor, struct editing_mode *mode); 113 | void editor_pop_mode(struct editor *editor); 114 | 115 | bool editor_save_buffer(struct editor *editor, char *path); 116 | void editor_draw(struct editor *editor); 117 | 118 | void editor_jump_to_line(struct editor *editor, int line); 119 | void editor_jump_to_end(struct editor *editor); 120 | 121 | const char *editor_buffer_name(struct editor *editor, struct buffer *buffer); 122 | 123 | const char *editor_relpath(struct editor *editor, const char *path); 124 | 125 | struct editor_register *editor_get_register(struct editor *editor, char name); 126 | 127 | struct tb_event; 128 | bool editor_waitkey(struct editor *editor, struct tb_event *ev); 129 | char editor_getchar(struct editor *editor); 130 | void editor_handle_key_press(struct editor *editor, struct tb_event *ev); 131 | 132 | ATTR_PRINTFLIKE(2, 3) 133 | void editor_status_msg(struct editor *editor, const char *format, ...); 134 | ATTR_PRINTFLIKE(2, 3) 135 | void editor_status_err(struct editor *editor, const char *format, ...); 136 | 137 | void editor_push_event(struct editor *editor, struct tb_event *ev); 138 | void editor_send_keys(struct editor *editor, const char *keys); 139 | 140 | void editor_undo(struct editor *editor); 141 | void editor_redo(struct editor *editor); 142 | 143 | void editor_source(struct editor *editor, char *path); 144 | bool editor_try_modify(struct editor *editor); 145 | 146 | char *editor_find_in_path(struct editor *editor, char *file); 147 | 148 | struct editor_command { 149 | const char *name; 150 | const char *shortname; 151 | enum completion_kind completion_kind; 152 | void (*action)(struct editor*, char*, bool); 153 | 154 | TAILQ_ENTRY(editor_command) pointers; 155 | }; 156 | void register_editor_command(struct editor_command *command); 157 | struct editor_command *command_parse(char *command, char **arg, bool *force); 158 | char **commands_get_sorted(int *len); 159 | 160 | void editor_execute_command(struct editor *editor, char *command); 161 | 162 | #define EDITOR_COMMAND_WITH_COMPLETION(name, shortname, completion) \ 163 | static void editor_command_##name(struct editor*, char*, bool); \ 164 | static struct editor_command command_##name = { \ 165 | #name, #shortname, completion, editor_command_##name, {0}}; \ 166 | __attribute__((constructor)) \ 167 | static void _constructor_##name(void) { \ 168 | register_editor_command(&command_##name); \ 169 | } \ 170 | static void editor_command_##name( \ 171 | struct editor *editor ATTR_UNUSED, \ 172 | char *arg ATTR_UNUSED, \ 173 | bool force ATTR_UNUSED) 174 | 175 | #define EDITOR_COMMAND(name, shortname) \ 176 | EDITOR_COMMAND_WITH_COMPLETION(name, shortname, COMPLETION_NONE) 177 | -------------------------------------------------------------------------------- /tests/options.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | #include "editor.h" 3 | 4 | #include 5 | 6 | #include "buf.h" 7 | #include "buffer.h" 8 | #include "window.h" 9 | 10 | #include "util.h" 11 | 12 | static struct editor *editor = NULL; 13 | 14 | static char *type(const char *keys) { 15 | editor_send_keys(editor, keys); 16 | return editor->status->buf; 17 | } 18 | 19 | void test_options__initialize(void) { 20 | tb_init(); 21 | editor = editor_create(tb_width(), tb_height()); 22 | } 23 | 24 | void test_options__cleanup(void) { 25 | editor_free(editor); 26 | } 27 | 28 | void test_options__option_types(void) { 29 | type(":set ruler"); 30 | cl_assert(editor->opt.ruler); 31 | cl_assert_equal_s(type(":set ruler?"), "ruler"); 32 | 33 | type(":set numberwidth=6"); 34 | cl_assert_equal_i(editor->window->opt.numberwidth, 6); 35 | cl_assert_equal_s(type(":set numberwidth?"), "numberwidth=6"); 36 | 37 | type(":set cinwords=hello,world"); 38 | cl_assert_equal_s(type(":set cinwords?"), "cinwords=hello,world"); 39 | cl_assert_equal_s(editor->opt.cinwords, "hello,world"); 40 | cl_assert_equal_s(editor->window->buffer->opt.cinwords, "hello,world"); 41 | } 42 | 43 | void test_options__invalid_arguments(void) { 44 | cl_assert_equal_s( 45 | type(":set doesnotexist"), 46 | "Unknown option: doesnotexist"); 47 | cl_assert_equal_s( 48 | type(":set number=4"), 49 | "Invalid argument: number=4"); 50 | cl_assert_equal_s( 51 | type(":set cinwords!"), 52 | "Invalid argument: cinwords!"); 53 | cl_assert_equal_s( 54 | type(":set autoindent+=8"), 55 | "Invalid argument: autoindent+=8"); 56 | cl_assert_equal_s( 57 | type(":set nonumberwidth=4"), 58 | "Invalid argument: nonumberwidth=4"); 59 | cl_assert_equal_s( 60 | type(":set numberwidth=hello"), 61 | "Number required after =: numberwidth=hello"); 62 | cl_assert_equal_s( 63 | type(":set numberwidth+=hello"), 64 | "Number required after =: numberwidth+=hello"); 65 | } 66 | 67 | void test_options__reset_to_default(void) { 68 | cl_assert_equal_s(type(":set number?"), "nonumber"); 69 | type(":set number"); 70 | cl_assert_equal_s(type(":set number?"), "number"); 71 | type(":set number&"); 72 | cl_assert_equal_s(type(":set number?"), "nonumber"); 73 | 74 | cl_assert_equal_s(type(":set numberwidth?"), "numberwidth=4"); 75 | type(":set numberwidth=24"); 76 | cl_assert_equal_s(type(":set numberwidth?"), "numberwidth=24"); 77 | type(":set numberwidth&"); 78 | cl_assert_equal_s(type(":set numberwidth?"), "numberwidth=4"); 79 | 80 | cl_assert_equal_s(type(":set cinwords?"), "cinwords=if,else,while,do,for,switch"); 81 | type(":set cinwords=foo,bar"); 82 | cl_assert_equal_s(type(":set cinwords?"), "cinwords=foo,bar"); 83 | type(":set cinwords&"); 84 | cl_assert_equal_s(type(":set cinwords?"), "cinwords=if,else,while,do,for,switch"); 85 | } 86 | 87 | void test_options__plus_equals(void) { 88 | type(":set numberwidth=4"); 89 | cl_assert_equal_s(type(":set numberwidth?"), "numberwidth=4"); 90 | type(":set numberwidth+=6"); 91 | cl_assert_equal_s(type(":set numberwidth?"), "numberwidth=10"); 92 | 93 | type(":set cinwords="); 94 | cl_assert_equal_s(type(":set cinwords?"), "cinwords="); 95 | type(":set cinwords+=foo"); 96 | cl_assert_equal_s(type(":set cinwords?"), "cinwords=foo"); 97 | type(":set cinwords+=bar"); 98 | cl_assert_equal_s(type(":set cinwords?"), "cinwords=foo,bar"); 99 | type(":set cinwords+=foo"); 100 | cl_assert_equal_s(type(":set cinwords?"), "cinwords=foo,bar"); 101 | } 102 | 103 | void test_options__buffer_local(void) { 104 | // 'modifiable' by default is on. 105 | cl_assert_equal_s(type(":set modifiable?"), "modifiable"); 106 | cl_assert_equal_s(type(":setl modifiable?"), "modifiable"); 107 | cl_assert_equal_s(type(":setg modifiable?"), "modifiable"); 108 | 109 | // Turn it off locally for this buffer. 110 | type(":setl nomodifiable"); 111 | 112 | // It's off for this buffer, but the global one is on. 113 | cl_assert_equal_s(type(":set modifiable?"), "nomodifiable"); 114 | cl_assert_equal_s(type(":setl modifiable?"), "nomodifiable"); 115 | cl_assert_equal_s(type(":setg modifiable?"), "modifiable"); 116 | 117 | // Create a new buffer; it inherits the global value. 118 | type(":e newbuffer.c"); 119 | cl_assert_equal_s(type(":set modifiable?"), "modifiable"); 120 | cl_assert_equal_s(type(":setl modifiable?"), "modifiable"); 121 | cl_assert_equal_s(type(":setg modifiable?"), "modifiable"); 122 | 123 | // Turn it off globally. 124 | type(":setg nomodifiable"); 125 | 126 | // Turning it off globally doesn't affect existing buffers. 127 | cl_assert_equal_s(type(":set modifiable?"), "modifiable"); 128 | cl_assert_equal_s(type(":setl modifiable?"), "modifiable"); 129 | cl_assert_equal_s(type(":setg modifiable?"), "nomodifiable"); 130 | 131 | // New buffer inherits the global value, which is now off. 132 | type(":e newbuffer2.c"); 133 | cl_assert_equal_s(type(":set modifiable?"), "nomodifiable"); 134 | cl_assert_equal_s(type(":setl modifiable?"), "nomodifiable"); 135 | cl_assert_equal_s(type(":setg modifiable?"), "nomodifiable"); 136 | } 137 | 138 | void test_options__window_local(void) { 139 | // 'number' by default is off. 140 | cl_assert_equal_s(type(":set number?"), "nonumber"); 141 | 142 | // Turn it on. 143 | type(":set number"); 144 | cl_assert_equal_s(type(":set number?"), "number"); 145 | 146 | // Split windows; the new window inherits the value. 147 | type(":split"); 148 | cl_assert_equal_s(type(":set number?"), "number"); 149 | 150 | // Turn it off in this window. 151 | type(":set nonumber"); 152 | cl_assert_equal_s(type(":set number?"), "nonumber"); 153 | 154 | // It doesn't affect the value in the original window. 155 | type(":q"); 156 | cl_assert_equal_s(type(":set number?"), "number"); 157 | } 158 | 159 | void test_options__completion(void) { 160 | cl_assert_equal_s(type(":set nu"), ":set number"); 161 | cl_assert_equal_s(type(""), ":set numberwidth"); 162 | type(""); 163 | } 164 | -------------------------------------------------------------------------------- /tests/editor.c: -------------------------------------------------------------------------------- 1 | #include "clar.h" 2 | #include "editor.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "buf.h" 9 | #include "buffer.h" 10 | #include "gap.h" 11 | #include "window.h" 12 | #include "util.h" 13 | 14 | #include "asserts.h" 15 | 16 | static struct editor *editor = NULL; 17 | 18 | static char *type(const char *keys) { 19 | editor_send_keys(editor, keys); 20 | return editor->status->buf; 21 | } 22 | 23 | void test_editor__initialize(void) { 24 | tb_init(); 25 | editor = editor_create(tb_width(), tb_height()); 26 | assert_buffer_contents("\n"); 27 | assert_cursor_at(0, 0); 28 | } 29 | 30 | void test_editor__cleanup(void) { 31 | editor_free(editor); 32 | } 33 | 34 | void test_editor__basic_editing(void) { 35 | type("i"); 36 | type("hello"); 37 | assert_cursor_at(0, 5); 38 | assert_buffer_contents("hello\n"); 39 | type(""); 40 | assert_cursor_at(0, 2); 41 | assert_buffer_contents("he\n"); 42 | type("hi"); 43 | type("iv"); 44 | assert_cursor_at(0, 3); 45 | assert_buffer_contents("hive\n"); 46 | } 47 | 48 | void test_editor__basic_motions(void) { 49 | type("ithe quick brown fox jumps over the lazy dog0"); 50 | assert_cursor_over('t'); 51 | 52 | type("w"); assert_cursor_over('q'); 53 | type("e"); assert_cursor_over('k'); 54 | type("ge"); assert_cursor_over('e'); 55 | type("b"); assert_cursor_over('t'); 56 | type("tx"); assert_cursor_over('o'); 57 | type("fr"); assert_cursor_over('r'); 58 | type("Tn"); assert_cursor_over(' '); 59 | type("Fh"); assert_cursor_over('h'); 60 | type("4l"); assert_cursor_over('u'); 61 | type("$b"); assert_cursor_over('d'); 62 | } 63 | 64 | void test_editor__jump_to_line(void) { 65 | type("ihellohellohellohellohellogg"); 66 | assert_cursor_at(0, 0); 67 | type(":2"); 68 | assert_cursor_at(1, 0); 69 | type(":-1"); 70 | assert_cursor_at(0, 0); 71 | type(":+3"); 72 | assert_cursor_at(3, 0); 73 | 74 | type(":-20"); 75 | assert_cursor_at(0, 0); 76 | type(":+100"); 77 | assert_cursor_at(4, 0); 78 | } 79 | 80 | void test_editor__line_motions(void) { 81 | type("ione\ntwo\nthree\nfour\nfivegg0"); 82 | assert_cursor_at(0, 0); 83 | 84 | type("k"); assert_cursor_at(0, 0); 85 | type("j"); assert_cursor_at(1, 0); 86 | type("2j"); assert_cursor_at(3, 0); 87 | type("gg"); assert_cursor_at(0, 0); 88 | type("G"); assert_cursor_at(4, 0); 89 | type("3G"); assert_cursor_at(2, 0); 90 | 91 | type("gg0/our"); assert_cursor_at(3, 1); 92 | type("G0?wo"); assert_cursor_at(1, 1); 93 | } 94 | 95 | void test_editor__yank_put(void) { 96 | assert_buffer_contents("\n"); 97 | assert_cursor_at(0, 0); 98 | 99 | type("ithe quick brown fox jumps over the lazy dog0"); 100 | 101 | // 'brown' in "a register 102 | type("2w\"ayw"); 103 | // 'fox' in "b register 104 | type("w\"byw"); 105 | // whole line in unnamed register 106 | type("0d$"); 107 | 108 | type("\"bp\"ap"); 109 | type("op"); 110 | 111 | assert_buffer_contents( 112 | "fox brown \n" 113 | "the quick brown fox jumps over the lazy dog\n" 114 | ); 115 | } 116 | 117 | void test_editor__join_lines(void) { 118 | type("ihello\n worldgg"); 119 | type("J"); 120 | assert_buffer_contents("hello world\n"); 121 | assert_cursor_at(0, 5); 122 | } 123 | 124 | void test_editor__percent_motion(void) { 125 | type("i{ ( [ ) ] [ ] }0"); 126 | assert_cursor_over('{'); 127 | type("%"); assert_cursor_over('}'); 128 | type("%"); assert_cursor_over('{'); 129 | type("l%"); assert_cursor_over(')'); 130 | type("%"); assert_cursor_over('('); 131 | type("l%"); assert_cursor_over(']'); assert_cursor_at(0, 8); 132 | type("l%"); assert_cursor_over(']'); assert_cursor_at(0, 12); 133 | type("%"); assert_cursor_over('['); assert_cursor_at(0, 10); 134 | } 135 | 136 | void test_editor__command_history_traversal(void) { 137 | type(":set number"); 138 | type(":set norelativenumber"); 139 | 140 | type(":"); 141 | 142 | cl_assert_equal_s(type(""), ":set norelativenumber"); 143 | cl_assert_equal_s(type(""), ":set number"); 144 | cl_assert_equal_s(type(""), ":set number"); 145 | cl_assert_equal_s(type(""), ":set norelativenumber"); 146 | cl_assert_equal_s(type(""), ":"); 147 | cl_assert_equal_s(type(""), ":"); 148 | type(""); 149 | } 150 | 151 | void test_editor__command_history_filter_prefix(void) { 152 | type(":set number"); 153 | type(":set norelativenumber"); 154 | type(":nohlsearch"); 155 | type(":set nomodifiable"); 156 | type(":vsplit"); 157 | 158 | type(":set no"); 159 | 160 | cl_assert_equal_s(type(""), ":set nomodifiable"); 161 | cl_assert_equal_s(type(""), ":set norelativenumber"); 162 | cl_assert_equal_s(type(""), ":set norelativenumber"); 163 | cl_assert_equal_s(type(""), ":set nomodifiable"); 164 | cl_assert_equal_s(type(""), ":set no"); 165 | type(""); 166 | } 167 | 168 | void test_editor__command_history_duplicates_move_to_front(void) { 169 | type(":nohlsearch"); 170 | type(":set nomodifiable"); 171 | type(":vsplit"); 172 | type(":set nomodifiable"); 173 | 174 | type(":"); 175 | 176 | cl_assert_equal_s(type(""), ":set nomodifiable"); 177 | cl_assert_equal_s(type(""), ":vsplit"); 178 | cl_assert_equal_s(type(""), ":nohlsearch"); 179 | cl_assert_equal_s(type(""), ":nohlsearch"); 180 | type(""); 181 | } 182 | 183 | void test_editor__autoindent(void) { 184 | type(":set autoindent"); 185 | type("i helloworld"); 186 | assert_buffer_contents(" hello\n world\n"); 187 | 188 | type("ggdG"); 189 | type(":set smartindent"); 190 | type(":set shiftwidth=2"); 191 | 192 | type("ihello {world"); 193 | assert_buffer_contents("hello {\n world\n"); 194 | 195 | type("ggdG"); 196 | type(":set cinwords=custom"); 197 | type("icustomindented"); 198 | assert_buffer_contents("custom\n indented\n"); 199 | } 200 | 201 | void test_editor__completion(void) { 202 | cl_assert_equal_s(type(":sp"), ":split"); 203 | cl_assert_equal_s(type(""), ":splitfind"); 204 | cl_assert_equal_s(type(""), ":split"); 205 | cl_assert_equal_s(type("vsp"), ":vsplit"); 206 | type(""); 207 | } 208 | -------------------------------------------------------------------------------- /tags.c: -------------------------------------------------------------------------------- 1 | #include "tags.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "buf.h" 10 | #include "buffer.h" 11 | #include "editor.h" 12 | #include "gap.h" 13 | #include "search.h" 14 | #include "util.h" 15 | #include "window.h" 16 | 17 | static char *escape_regex(char *regex) { 18 | // Worst case, every char is escaped... 19 | size_t len = strlen(regex); 20 | char *result = xmalloc(len * 2 + 1); 21 | char *dest = result; 22 | for (char *src = regex; *src; ++src) { 23 | if (strchr("*+()[]{}", *src)) { 24 | *dest++ = '\\'; 25 | } 26 | *dest++ = *src; 27 | } 28 | *dest = '\0'; 29 | return result; 30 | } 31 | 32 | void tags_clear(struct tags *tags) { 33 | for (size_t i = 0; i < tags->len; ++i) { 34 | struct tag *tag = &tags->tags[i]; 35 | free(tag->name); 36 | free(tag->path); 37 | free(tag->cmd); 38 | } 39 | if (tags->len) { 40 | free(tags->tags); 41 | } 42 | tags->tags = NULL; 43 | tags->len = 0; 44 | tags->loaded_at = 0; 45 | } 46 | 47 | static void tags_load(struct tags *tags) { 48 | FILE *fp = fopen(tags->file, "r"); 49 | if (!fp) { 50 | return; 51 | } 52 | 53 | size_t nlines = 0; 54 | size_t n = 0; 55 | char *line = NULL; 56 | while (getline(&line, &n, fp) != -1) { 57 | if (*line != '!') { 58 | nlines++; 59 | } 60 | } 61 | fseek(fp, 0, SEEK_SET); 62 | 63 | if (tags->len) { 64 | tags_clear(tags); 65 | } 66 | tags->tags = xmalloc(sizeof(*tags->tags) * nlines); 67 | tags->len = nlines; 68 | 69 | size_t i = 0; 70 | ssize_t len = 0; 71 | while ((len = getline(&line, &n, fp)) != -1) { 72 | line[len - 1] = '\0'; 73 | if (*line == '!') { 74 | continue; 75 | } 76 | struct tag *tag = &tags->tags[i++]; 77 | tag->name = xstrdup(strtok(line, "\t")); 78 | tag->path = xstrdup(strtok(NULL, "\t")); 79 | tag->cmd = escape_regex(strtok(NULL, "\"")); 80 | tag->cmd[strlen(tag->cmd) - 2] = '\0'; 81 | } 82 | free(line); 83 | fclose(fp); 84 | 85 | tags->loaded_at = time(0); 86 | } 87 | 88 | struct tags *tags_create(const char *file) { 89 | struct tags *tags = xmalloc(sizeof(*tags)); 90 | tags->file = file; 91 | tags->tags = NULL; 92 | tags->len = 0; 93 | tags->loaded_at = 0; 94 | 95 | tags_load(tags); 96 | return tags; 97 | } 98 | 99 | static int tag_compare(const void *lhs, const void *rhs) { 100 | return strcmp((const char*) lhs, ((const struct tag*) rhs)->name); 101 | } 102 | 103 | struct tag *tags_find(struct tags *tags, char *name) { 104 | struct stat info; 105 | int err = stat(tags->file, &info); 106 | if (err) { 107 | tags_clear(tags); 108 | return NULL; 109 | } 110 | 111 | if (difftime(info.st_mtime, tags->loaded_at) > 0) { 112 | tags_load(tags); 113 | } 114 | 115 | return bsearch(name, tags->tags, tags->len, sizeof(struct tag), tag_compare); 116 | } 117 | 118 | void editor_jump_to_tag(struct editor *editor, char *name) { 119 | struct tag *tag = tags_find(editor->tags, name); 120 | if (!tag) { 121 | editor_status_err(editor, "tag not found: %s", name); 122 | return; 123 | } 124 | 125 | struct tag_jump *jump = xmalloc(sizeof(*jump)); 126 | jump->buffer = editor->window->buffer; 127 | jump->cursor = window_cursor(editor->window); 128 | jump->tag = tag; 129 | 130 | if (editor->window->tag) { 131 | struct tag_jump *j, *tj; 132 | TAILQ_FOREACH_REVERSE_SAFE(j, &editor->window->tag_stack, tag_list, pointers, tj) { 133 | if (j == editor->window->tag) { 134 | break; 135 | } 136 | TAILQ_REMOVE(&editor->window->tag_stack, j, pointers); 137 | free(j); 138 | } 139 | } 140 | TAILQ_INSERT_TAIL(&editor->window->tag_stack, jump, pointers); 141 | editor->window->tag = jump; 142 | 143 | editor_open(editor, tag->path); 144 | editor_jump_to_match(editor, tag->cmd + 1, 0, SEARCH_FORWARDS); 145 | window_center_cursor(editor->window); 146 | } 147 | 148 | void editor_tag_stack_prev(struct editor *editor) { 149 | if (TAILQ_EMPTY(&editor->window->tag_stack)) { 150 | editor_status_err(editor, "tag stack empty"); 151 | } else if (!editor->window->tag) { 152 | editor_status_err(editor, "at bottom of tag stack"); 153 | } else { 154 | buf_clear(editor->status); 155 | editor->status_error = false; 156 | window_set_buffer(editor->window, editor->window->tag->buffer); 157 | window_set_cursor(editor->window, editor->window->tag->cursor); 158 | window_center_cursor(editor->window); 159 | editor->window->tag = TAILQ_PREV(editor->window->tag, tag_list, pointers); 160 | } 161 | } 162 | 163 | void editor_tag_stack_next(struct editor *editor) { 164 | if (TAILQ_EMPTY(&editor->window->tag_stack)) { 165 | editor_status_err(editor, "tag stack empty"); 166 | return; 167 | } 168 | 169 | struct tag_jump *next; 170 | if (!editor->window->tag) { 171 | next = TAILQ_FIRST(&editor->window->tag_stack); 172 | } else { 173 | next = TAILQ_NEXT(editor->window->tag, pointers); 174 | } 175 | if (!next) { 176 | editor_status_err(editor, "at top of tag stack"); 177 | return; 178 | } 179 | 180 | editor_open(editor, next->tag->path); 181 | editor_jump_to_match(editor, next->tag->cmd + 1, 0, SEARCH_FORWARDS); 182 | window_center_cursor(editor->window); 183 | editor->window->tag = next; 184 | } 185 | 186 | EDITOR_COMMAND_WITH_COMPLETION(tag, tag, COMPLETION_TAGS) { 187 | if (!arg) { 188 | editor_tag_stack_next(editor); 189 | } else { 190 | editor_jump_to_tag(editor, arg); 191 | } 192 | } 193 | 194 | EDITOR_COMMAND(tags, tags) { 195 | buf_clear(editor->message); 196 | buf_appendf(editor->message, " # TO tag FROM line in file/text"); 197 | int i = 1; 198 | // TODO(ibadawi): tag match list 199 | int match = 1; 200 | struct tag_jump *jump; 201 | bool active = editor->window->tag == NULL; 202 | TAILQ_FOREACH(jump, &editor->window->tag_stack, pointers) { 203 | buf_appendf(editor->message, "\n%c ", active ? '>': ' '); 204 | active = editor->window->tag == jump; 205 | 206 | size_t line, col; 207 | gb_pos_to_linecol(jump->buffer->text, jump->cursor, &line, &col); 208 | struct buf *text = gb_getline(jump->buffer->text, jump->cursor); 209 | buf_strip_whitespace(text); 210 | buf_appendf(editor->message, "%d %-2d %-11s %9zu %s", 211 | i++, match, jump->tag->name, line + 1, text->buf); 212 | buf_free(text); 213 | } 214 | if (active) { 215 | buf_append(editor->message, "\n> "); 216 | } 217 | editor_status_msg(editor, "Press ENTER to continue "); 218 | editor->status_cursor = editor->status->len - 1; 219 | } 220 | -------------------------------------------------------------------------------- /syntax.c: -------------------------------------------------------------------------------- 1 | #include "syntax.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "buffer.h" 8 | #include "gap.h" 9 | #include "util.h" 10 | 11 | char c_regex[] = 12 | // Types 13 | "(char|short|int|long|signed|unsigned|void|float|double|struct|enum|union|" 14 | "typedef|size_t|ssize_t|off_t|ptrdiff_t|sig_atomic_t|clock_t|time_t|va_list|" 15 | "jmp_buf|FILE|DIR|bool|_Bool|int8_t|uint8_t|int16_t|uint16_t|int32_t|" 16 | "uint32_t|int64_t|uint64_t|intptr_t|uintptr_t|" 17 | // These are not really types but just for the purposes of highlighting 18 | "auto|const|extern|inline|register|restrict|static|volatile)\\b|" 19 | // Preprocessor directives 20 | "(#include|#define|#undef|#pragma|#ifdef|#ifndef|#if|#else|#error|#endif)\\b|" 21 | // Constants 22 | "(true|false|NULL)\\b|" 23 | // Statements 24 | "(asm|break|case|continue|default|do|else|for|goto|if|return|sizeof|switch|while)\\b"; 25 | 26 | #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) 27 | 28 | static bool gb_startswith_at(struct gapbuf *gb, size_t pos, char *prefix) { 29 | size_t len = strlen(prefix); 30 | for (size_t i = 0; i < len; ++i) { 31 | if (gb_getchar(gb, pos + i) != prefix[i]) { 32 | return false; 33 | } 34 | } 35 | return true; 36 | } 37 | 38 | static bool is_word_char(char c) { 39 | return isalnum(c) || c == '_'; 40 | } 41 | 42 | static size_t gb_findstring(struct gapbuf *gb, size_t from, char *s) { 43 | size_t i = 0; 44 | size_t size = gb_size(gb); 45 | while (from + i < size && !gb_startswith_at(gb, from + i, s)) { 46 | i++; 47 | } 48 | return from + i; 49 | } 50 | 51 | static bool is_escaped(struct gapbuf *gb, size_t pos) { 52 | int backslashes = 0; 53 | while (gb_getchar(gb, --pos) == '\\') { 54 | ++backslashes; 55 | } 56 | return backslashes % 2 == 1; 57 | } 58 | 59 | static void c_next_token(struct syntax *syntax, struct syntax_token *token) { 60 | struct gapbuf *gb = syntax->buffer->text; 61 | size_t size = gb_size(gb); 62 | char ch = gb_getchar(gb, syntax->pos); 63 | 64 | #define RETURN_TOKEN(type, len_) \ 65 | token->pos = syntax->pos; \ 66 | token->kind = SYNTAX_TOKEN_##type; \ 67 | token->len = len_; \ 68 | syntax->pos += token->len; \ 69 | return; 70 | 71 | if (isspace(ch)) { 72 | if (ch == '\n' && gb_getchar(gb, syntax->pos - 1) != '\\') { 73 | syntax->state = STATE_INIT; 74 | } 75 | RETURN_TOKEN(IDENTIFIER, 1); 76 | } 77 | 78 | if (isdigit(ch)) { 79 | RETURN_TOKEN(LITERAL_NUMBER, 1); 80 | } 81 | 82 | char prefix[16 + 1]; 83 | gb_getstring_into(gb, syntax->pos, 16, prefix); 84 | 85 | int rc = pcre2_match(syntax->regex, 86 | (unsigned char*) prefix, PCRE2_ZERO_TERMINATED, 0, 0, syntax->groups, NULL); 87 | if (rc > 0) { 88 | PCRE2_SIZE *offsets = pcre2_get_ovector_pointer(syntax->groups); 89 | 90 | token->pos = syntax->pos; 91 | token->len = offsets[1] - offsets[0]; 92 | syntax->pos += token->len; 93 | 94 | if (offsets[2] != PCRE2_UNSET) { 95 | token->kind = SYNTAX_TOKEN_TYPE; 96 | } else if (offsets[4] != PCRE2_UNSET) { 97 | syntax->state = STATE_PREPROC; 98 | token->kind = SYNTAX_TOKEN_PREPROC; 99 | } else if (offsets[6] != PCRE2_UNSET) { 100 | token->kind = SYNTAX_TOKEN_LITERAL_NUMBER; 101 | } else if (offsets[8] != PCRE2_UNSET) { 102 | token->kind = SYNTAX_TOKEN_STATEMENT; 103 | } else { 104 | assert(0); 105 | } 106 | return; 107 | } 108 | 109 | if (gb_startswith_at(gb, syntax->pos, "//")) { 110 | size_t end = gb_indexof(gb, '\n', syntax->pos); 111 | RETURN_TOKEN(COMMENT, end - syntax->pos); 112 | } 113 | 114 | if (gb_startswith_at(gb, syntax->pos, "/*")) { 115 | size_t end = gb_findstring(gb, syntax->pos, "*/"); 116 | if (end != size) { 117 | end += 2; 118 | } 119 | RETURN_TOKEN(COMMENT, end - syntax->pos); 120 | } 121 | 122 | if (ch == '"') { 123 | size_t start = syntax->pos + 1; 124 | size_t end; 125 | do { 126 | end = gb_indexof(gb, '"', start); 127 | start = end + 1; 128 | } while (end < size && is_escaped(gb, end)); 129 | RETURN_TOKEN(LITERAL_STRING, end - syntax->pos + 1); 130 | } 131 | 132 | if (syntax->state == STATE_PREPROC && ch == '<') { 133 | size_t end = gb_indexof(gb, '>', syntax->pos + 1); 134 | size_t newline = gb_indexof(gb, '\n', syntax->pos + 1); 135 | if (end < newline) { 136 | RETURN_TOKEN(LITERAL_STRING, end - syntax->pos + 1); 137 | } 138 | } 139 | 140 | if (ch == '\'') { 141 | size_t start = syntax->pos + 1; 142 | size_t end; 143 | do { 144 | end = gb_indexof(gb, '\'', start); 145 | start = end + 1; 146 | } while (end < size && is_escaped(gb, end)); 147 | RETURN_TOKEN(LITERAL_CHAR, end - syntax->pos + 1); 148 | } 149 | 150 | if (ispunct(ch)) { 151 | if (syntax->state == STATE_PREPROC) { 152 | RETURN_TOKEN(PREPROC, 1); 153 | } else { 154 | RETURN_TOKEN(PUNCTUATION, 1); 155 | } 156 | } 157 | 158 | size_t end = syntax->pos + 1; 159 | while (is_word_char(gb_getchar(gb, end++))) {} 160 | if (syntax->state == STATE_PREPROC) { 161 | RETURN_TOKEN(PREPROC, end - syntax->pos - 1); 162 | } else { 163 | RETURN_TOKEN(IDENTIFIER, end - syntax->pos - 1); 164 | } 165 | } 166 | 167 | static void c_token_at(struct syntax *syntax, struct syntax_token *token, size_t pos) { 168 | do { 169 | c_next_token(syntax, token); 170 | } while (!(token->pos <= pos && pos < token->pos + token->len)); 171 | } 172 | 173 | static struct filetype { 174 | char *name; 175 | char *exts[2]; 176 | tokenizer_func tokenizer; 177 | char *regex; 178 | } supported_filetypes[] = { 179 | {"c", {"c", "h"}, c_token_at, c_regex}, 180 | }; 181 | 182 | char *syntax_detect_filetype(char *path) { 183 | char *dot = strrchr(path, '.'); 184 | if (!dot || dot == path) { 185 | return ""; 186 | } 187 | for (size_t i = 0; i < ARRAY_SIZE(supported_filetypes); ++i) { 188 | struct filetype *filetype = &supported_filetypes[i]; 189 | for (int i = 0; i < 2; ++i) { 190 | if (!strcmp(dot + 1, filetype->exts[i])) { 191 | return filetype->name; 192 | } 193 | } 194 | } 195 | return ""; 196 | } 197 | 198 | bool syntax_init(struct syntax *syntax, struct buffer *buffer) { 199 | syntax->buffer = buffer; 200 | syntax->tokenizer = NULL; 201 | unsigned char *regex = NULL; 202 | for (size_t i = 0; i < ARRAY_SIZE(supported_filetypes); ++i) { 203 | struct filetype *filetype = &supported_filetypes[i]; 204 | if (!strcmp(buffer->opt.filetype, filetype->name)) { 205 | syntax->tokenizer = filetype->tokenizer; 206 | regex = (unsigned char*) filetype->regex; 207 | } 208 | } 209 | 210 | if (!syntax->tokenizer) { 211 | return false; 212 | } 213 | syntax->state = STATE_INIT; 214 | syntax->pos = 0; 215 | 216 | int errorcode; 217 | PCRE2_SIZE erroroffset; 218 | syntax->regex = pcre2_compile(regex, 219 | PCRE2_ZERO_TERMINATED, PCRE2_ANCHORED, 220 | &errorcode, &erroroffset, NULL); 221 | assert(regex); 222 | syntax->groups = pcre2_match_data_create_from_pattern(syntax->regex, NULL); 223 | return true; 224 | } 225 | 226 | void syntax_deinit(struct syntax *syntax) { 227 | if (syntax->tokenizer) { 228 | pcre2_code_free(syntax->regex); 229 | pcre2_match_data_free(syntax->groups); 230 | } 231 | } 232 | 233 | void syntax_token_at(struct syntax *syntax, struct syntax_token *token, size_t pos) { 234 | syntax->tokenizer(syntax, token, pos); 235 | } 236 | -------------------------------------------------------------------------------- /buffer.c: -------------------------------------------------------------------------------- 1 | #include "buffer.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "buf.h" 13 | #include "gap.h" 14 | #include "util.h" 15 | 16 | static struct buffer *buffer_of(char *path, struct gapbuf *gb, bool dir) { 17 | struct buffer *buffer = xmalloc(sizeof(*buffer)); 18 | 19 | buffer->directory = dir; 20 | buffer->text = gb; 21 | buffer->path = path ? xstrdup(path) : NULL; 22 | memset(&buffer->opt, 0, sizeof(buffer->opt)); 23 | 24 | TAILQ_INIT(&buffer->undo_stack); 25 | TAILQ_INIT(&buffer->redo_stack); 26 | 27 | TAILQ_INIT(&buffer->marks); 28 | 29 | return buffer; 30 | } 31 | 32 | struct buffer *buffer_create(char *path) { 33 | return buffer_of(path, gb_create(), false); 34 | } 35 | 36 | static void action_list_clear(struct action_group_list *list) { 37 | struct edit_action_group *group, *tg; 38 | TAILQ_FOREACH_SAFE(group, list, pointers, tg) { 39 | struct edit_action *action, *ta; 40 | TAILQ_FOREACH_SAFE(action, &group->actions, pointers, ta) { 41 | buf_free(action->buf); 42 | free(action); 43 | } 44 | TAILQ_REMOVE(list, group, pointers); 45 | free(group); 46 | } 47 | } 48 | 49 | void buffer_free(struct buffer *buffer) { 50 | free(buffer->path); 51 | gb_free(buffer->text); 52 | action_list_clear(&buffer->undo_stack); 53 | action_list_clear(&buffer->redo_stack); 54 | buffer_free_options(buffer); 55 | free(buffer); 56 | } 57 | 58 | static struct buf *listdir(char *path) { 59 | struct dirent **namelist; 60 | int n; 61 | 62 | n = scandir(path, &namelist, NULL, alphasort); 63 | if (n < 0) { 64 | return NULL; 65 | } 66 | 67 | struct buf *buf = buf_create(100); 68 | for (int i = 0; i < n; ++i) { 69 | struct dirent *entry = namelist[i]; 70 | if (!strcmp(entry->d_name, ".") || 71 | !strcmp(entry->d_name, "..")) { 72 | free(entry); 73 | continue; 74 | } 75 | 76 | buf_append(buf, entry->d_name); 77 | if (entry->d_type == DT_DIR) { 78 | buf_append(buf, "/"); 79 | } 80 | buf_append(buf, "\n"); 81 | free(entry); 82 | } 83 | 84 | if (n == 2) { 85 | buf_append(buf, "\n"); 86 | } 87 | 88 | free(namelist); 89 | return buf; 90 | } 91 | 92 | struct buffer *buffer_open(char *path) { 93 | struct gapbuf *gb; 94 | bool directory = false; 95 | struct stat info; 96 | 97 | stat(path, &info); 98 | directory = S_ISDIR(info.st_mode); 99 | 100 | if (directory) { 101 | struct buf *listing = listdir(path); 102 | if (!listing) { 103 | return NULL; 104 | } 105 | gb = gb_fromstring(listing); 106 | buf_free(listing); 107 | } else { 108 | gb = gb_fromfile(path); 109 | if (!gb) { 110 | return NULL; 111 | } 112 | } 113 | 114 | return buffer_of(path, gb, directory); 115 | } 116 | 117 | bool buffer_write(struct buffer *buffer) { 118 | if (!buffer->path) { 119 | return false; 120 | } 121 | return buffer_saveas(buffer, buffer->path); 122 | } 123 | 124 | bool buffer_saveas(struct buffer *buffer, char *path) { 125 | FILE *fp = fopen(path, "w"); 126 | if (!fp) { 127 | return false; 128 | } 129 | 130 | gb_save(buffer->text, fp); 131 | buffer->opt.modified = false; 132 | fclose(fp); 133 | return true; 134 | } 135 | 136 | static void buffer_update_marks_after_insert( 137 | struct buffer *buffer, size_t pos, size_t n) { 138 | struct mark *mark; 139 | TAILQ_FOREACH(mark, &buffer->marks, pointers) { 140 | assert(mark->region.end - mark->region.start == 1); 141 | if (pos <= mark->region.start) { 142 | mark->region.start += n; 143 | mark->region.end += n; 144 | } 145 | } 146 | } 147 | 148 | static void buffer_update_marks_after_delete( 149 | struct buffer *buffer, size_t pos, size_t n) { 150 | struct mark *mark; 151 | TAILQ_FOREACH(mark, &buffer->marks, pointers) { 152 | assert(mark->region.end - mark->region.start == 1); 153 | if (pos <= mark->region.start) { 154 | size_t diff = min(n, mark->region.start - pos); 155 | mark->region.start -= diff; 156 | mark->region.end -= diff; 157 | } 158 | } 159 | } 160 | 161 | void buffer_do_insert(struct buffer *buffer, struct buf *buf, size_t pos) { 162 | struct edit_action_group *group = TAILQ_FIRST(&buffer->undo_stack); 163 | if (group) { 164 | struct edit_action *action = xmalloc(sizeof(*action)); 165 | action->type = EDIT_ACTION_INSERT; 166 | action->pos = pos; 167 | action->buf = buf; 168 | TAILQ_INSERT_HEAD(&group->actions, action, pointers); 169 | } 170 | gb_putstring(buffer->text, buf->buf, buf->len, pos); 171 | buffer->opt.modified = true; 172 | 173 | buffer_update_marks_after_insert(buffer, pos, buf->len); 174 | 175 | if (!group) { 176 | buf_free(buf); 177 | } 178 | } 179 | 180 | void buffer_do_delete(struct buffer *buffer, size_t n, size_t pos) { 181 | struct edit_action_group *group = TAILQ_FIRST(&buffer->undo_stack); 182 | if (group) { 183 | struct edit_action *action = xmalloc(sizeof(*action)); 184 | action->type = EDIT_ACTION_DELETE; 185 | action->pos = pos; 186 | action->buf = gb_getstring(buffer->text, pos, n); 187 | TAILQ_INSERT_HEAD(&group->actions, action, pointers); 188 | } 189 | gb_del(buffer->text, n, pos + n); 190 | buffer->opt.modified = true; 191 | 192 | buffer_update_marks_after_delete(buffer, pos, n); 193 | } 194 | 195 | bool buffer_undo(struct buffer* buffer, size_t *cursor_pos) { 196 | struct edit_action_group *group = TAILQ_FIRST(&buffer->undo_stack); 197 | if (!group) { 198 | return false; 199 | } 200 | 201 | TAILQ_REMOVE(&buffer->undo_stack, group, pointers); 202 | 203 | struct gapbuf *gb = buffer->text; 204 | struct edit_action *action; 205 | TAILQ_FOREACH(action, &group->actions, pointers) { 206 | switch (action->type) { 207 | case EDIT_ACTION_INSERT: 208 | gb_del(gb, action->buf->len, action->pos + action->buf->len); 209 | buffer_update_marks_after_delete(buffer, action->pos, action->buf->len); 210 | break; 211 | case EDIT_ACTION_DELETE: 212 | gb_putstring(gb, action->buf->buf, action->buf->len, action->pos); 213 | buffer_update_marks_after_insert(buffer, action->pos, action->buf->len); 214 | break; 215 | } 216 | } 217 | 218 | TAILQ_INSERT_HEAD(&buffer->redo_stack, group, pointers); 219 | *cursor_pos = TAILQ_LAST(&group->actions, action_list)->pos; 220 | return true; 221 | } 222 | 223 | bool buffer_redo(struct buffer* buffer, size_t *cursor_pos) { 224 | struct edit_action_group *group = TAILQ_FIRST(&buffer->redo_stack); 225 | if (!group) { 226 | return false; 227 | } 228 | 229 | TAILQ_REMOVE(&buffer->redo_stack, group, pointers); 230 | 231 | struct gapbuf *gb = buffer->text; 232 | struct edit_action *action; 233 | TAILQ_FOREACH_REVERSE(action, &group->actions, action_list, pointers) { 234 | switch (action->type) { 235 | case EDIT_ACTION_INSERT: 236 | gb_putstring(gb, action->buf->buf, action->buf->len, action->pos); 237 | buffer_update_marks_after_insert(buffer, action->pos, action->buf->len); 238 | break; 239 | case EDIT_ACTION_DELETE: 240 | gb_del(gb, action->buf->len, action->pos + action->buf->len); 241 | buffer_update_marks_after_delete(buffer, action->pos, action->buf->len); 242 | break; 243 | } 244 | } 245 | 246 | TAILQ_INSERT_HEAD(&buffer->undo_stack, group, pointers); 247 | *cursor_pos = TAILQ_LAST(&group->actions, action_list)->pos; 248 | return true; 249 | } 250 | 251 | void buffer_start_action_group(struct buffer *buffer) { 252 | action_list_clear(&buffer->redo_stack); 253 | 254 | struct edit_action_group *group = xmalloc(sizeof(*group)); 255 | TAILQ_INIT(&group->actions); 256 | 257 | TAILQ_INSERT_HEAD(&buffer->undo_stack, group, pointers); 258 | } 259 | -------------------------------------------------------------------------------- /tests/testdata/utf8.txt: -------------------------------------------------------------------------------- 1 | Original by Markus Kuhn, adapted for HTML by Martin Dürst. 2 | 3 | UTF-8 encoded sample plain-text file 4 | ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 5 | 6 | Markus Kuhn [ˈmaʳkʊs kuːn] — 1999-08-20 7 | 8 | 9 | The ASCII compatible UTF-8 encoding of ISO 10646 and Unicode 10 | plain-text files is defined in RFC 2279 and in ISO 10646-1 Annex R. 11 | 12 | 13 | Using Unicode/UTF-8, you can write in emails and source code things such as 14 | 15 | Mathematics and Sciences: 16 | 17 | ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β), 18 | 19 | ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ, ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (A ⇔ B), 20 | 21 | 2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm 22 | 23 | Linguistics and dictionaries: 24 | 25 | ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn 26 | Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ] 27 | 28 | APL: 29 | 30 | ((V⍳V)=⍳⍴V)/V←,V ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈ 31 | 32 | Nicer typography in plain text files: 33 | 34 | ╔══════════════════════════════════════════╗ 35 | ║ ║ 36 | ║ • ‘single’ and “double” quotes ║ 37 | ║ ║ 38 | ║ • Curly apostrophes: “We’ve been here” ║ 39 | ║ ║ 40 | ║ • Latin-1 apostrophe and accents: '´` ║ 41 | ║ ║ 42 | ║ • ‚deutsche‘ „Anführungszeichen“ ║ 43 | ║ ║ 44 | ║ • †, ‡, ‰, •, 3–4, —, −5/+5, ™, … ║ 45 | ║ ║ 46 | ║ • ASCII safety test: 1lI|, 0OD, 8B ║ 47 | ║ ╭─────────╮ ║ 48 | ║ • the euro symbol: │ 14.95 € │ ║ 49 | ║ ╰─────────╯ ║ 50 | ╚══════════════════════════════════════════╝ 51 | 52 | Greek (in Polytonic): 53 | 54 | The Greek anthem: 55 | 56 | Σὲ γνωρίζω ἀπὸ τὴν κόψη 57 | τοῦ σπαθιοῦ τὴν τρομερή, 58 | σὲ γνωρίζω ἀπὸ τὴν ὄψη 59 | ποὺ μὲ βία μετράει τὴ γῆ. 60 | 61 | ᾿Απ᾿ τὰ κόκκαλα βγαλμένη 62 | τῶν ῾Ελλήνων τὰ ἱερά 63 | καὶ σὰν πρῶτα ἀνδρειωμένη 64 | χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά! 65 | 66 | From a speech of Demosthenes in the 4th century BC: 67 | 68 | Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι, 69 | ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς 70 | λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ 71 | τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿ 72 | εἰς τοῦτο προήκοντα, ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ 73 | πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν 74 | οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι, 75 | οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν 76 | ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον 77 | τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι 78 | γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν 79 | προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους 80 | σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ 81 | τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ 82 | τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς 83 | τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον. 84 | 85 | Δημοσθένους, Γ´ ᾿Ολυνθιακὸς 86 | 87 | Georgian: 88 | 89 | From a Unicode conference invitation: 90 | 91 | გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო 92 | კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს, 93 | ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს 94 | ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი, 95 | ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება 96 | ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში, 97 | ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში. 98 | 99 | Russian: 100 | 101 | From a Unicode conference invitation: 102 | 103 | Зарегистрируйтесь сейчас на Десятую Международную Конференцию по 104 | Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии. 105 | Конференция соберет широкий круг экспертов по вопросам глобального 106 | Интернета и Unicode, локализации и интернационализации, воплощению и 107 | применению Unicode в различных операционных системах и программных 108 | приложениях, шрифтах, верстке и многоязычных компьютерных системах. 109 | 110 | Thai (UCS Level 2): 111 | 112 | Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese 113 | classic 'San Gua'): 114 | 115 | [----------------------------|------------------------] 116 | ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช พระปกเกศกองบู๊กู้ขึ้นใหม่ 117 | สิบสองกษัตริย์ก่อนหน้าแลถัดไป สององค์ไซร้โง่เขลาเบาปัญญา 118 | ทรงนับถือขันทีเป็นที่พึ่ง บ้านเมืองจึงวิปริตเป็นนักหนา 119 | โฮจิ๋นเรียกทัพทั่วหัวเมืองมา หมายจะฆ่ามดชั่วตัวสำคัญ 120 | เหมือนขับไสไล่เสือจากเคหา รับหมาป่าเข้ามาเลยอาสัญ 121 | ฝ่ายอ้องอุ้นยุแยกให้แตกกัน ใช้สาวนั้นเป็นชนวนชื่นชวนใจ 122 | พลันลิฉุยกุยกีกลับก่อเหตุ ช่างอาเพศจริงหนาฟ้าร้องไห้ 123 | ต้องรบราฆ่าฟันจนบรรลัย ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ 124 | 125 | (The above is a two-column text. If combining characters are handled 126 | correctly, the lines of the second column should be aligned with the 127 | | character above.) 128 | 129 | Ethiopian: 130 | 131 | Proverbs in the Amharic language: 132 | 133 | ሰማይ አይታረስ ንጉሥ አይከሰስ። 134 | ብላ ካለኝ እንደአባቴ በቆመጠኝ። 135 | ጌጥ ያለቤቱ ቁምጥና ነው። 136 | ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው። 137 | የአፍ ወለምታ በቅቤ አይታሽም። 138 | አይጥ በበላ ዳዋ ተመታ። 139 | ሲተረጉሙ ይደረግሙ። 140 | ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል። 141 | ድር ቢያብር አንበሳ ያስር። 142 | ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም። 143 | እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም። 144 | የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ። 145 | ሥራ ከመፍታት ልጄን ላፋታት። 146 | ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል። 147 | የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ። 148 | ተንጋሎ ቢተፉ ተመልሶ ባፉ። 149 | ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው። 150 | እግርህን በፍራሽህ ልክ ዘርጋ። 151 | 152 | Runes: 153 | 154 | ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ 155 | 156 | (Old English, which transcribed into Latin reads 'He cwaeth that he 157 | bude thaem lande northweardum with tha Westsae.' and means 'He said 158 | that he lived in the northern land near the Western Sea.') 159 | 160 | Braille: 161 | 162 | ⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌ 163 | 164 | ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞ 165 | ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎ 166 | ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂ 167 | ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙ 168 | ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑ 169 | ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲ 170 | 171 | ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ 172 | 173 | ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹ 174 | ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞ 175 | ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕ 176 | ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹ 177 | ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎ 178 | ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎ 179 | ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳ 180 | ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞ 181 | ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ 182 | 183 | (The first couple of paragraphs of "A Christmas Carol" by Dickens) 184 | 185 | Compact font selection example text: 186 | 187 | ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789 188 | abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ 189 | –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд 190 | ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა 191 | 192 | Greetings in various languages: 193 | 194 | Hello world, Καλημέρα κόσμε, コンニチハ 195 | 196 | Box drawing alignment tests: █ 197 | ▉ 198 | ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ 199 | ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ 200 | ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ 201 | ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ 202 | ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ 203 | ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ 204 | ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ 205 | -------------------------------------------------------------------------------- /normal_mode.c: -------------------------------------------------------------------------------- 1 | #include "mode.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "attrs.h" 14 | #include "buf.h" 15 | #include "buffer.h" 16 | #include "editor.h" 17 | #include "gap.h" 18 | #include "motion.h" 19 | #include "search.h" 20 | #include "tags.h" 21 | #include "util.h" 22 | #include "window.h" 23 | 24 | static bool is_last_line(struct gapbuf *gb, size_t pos) { 25 | return pos > gb_size(gb) - gb->lines->buf[gb->lines->len - 1]; 26 | } 27 | 28 | static void editor_join_lines(struct editor *editor) { 29 | struct buffer *buffer = editor->window->buffer; 30 | struct gapbuf *gb = buffer->text; 31 | size_t cursor = window_cursor(editor->window); 32 | if (is_last_line(gb, cursor)) { 33 | return; 34 | } 35 | 36 | size_t newline = gb_indexof(gb, '\n', cursor); 37 | size_t next_newline = newline + 1; 38 | if (gb_getchar(gb, next_newline) != '\n') { 39 | next_newline = gb_indexof(gb, '\n', next_newline); 40 | } 41 | size_t first_non_blank; 42 | for (first_non_blank = newline + 1; 43 | first_non_blank < next_newline && 44 | isspace(gb_getchar(gb, first_non_blank)); 45 | first_non_blank++) {} 46 | 47 | buffer_start_action_group(buffer); 48 | buffer_do_delete(buffer, first_non_blank - newline, newline); 49 | buffer_do_insert(buffer, buf_from_char(' '), newline); 50 | window_set_cursor(editor->window, newline); 51 | } 52 | 53 | void normal_mode_entered(struct editor *editor ATTR_UNUSED) { 54 | return; 55 | } 56 | 57 | void normal_mode_exited(struct editor *editor ATTR_UNUSED) { 58 | return; 59 | } 60 | 61 | void normal_mode_key_pressed(struct editor* editor, struct tb_event* ev) { 62 | if (editor->message->len) { 63 | if (ev->key == TB_KEY_ENTER) { 64 | buf_clear(editor->message); 65 | buf_clear(editor->status); 66 | editor->status_cursor = 0; 67 | } 68 | return; 69 | } 70 | 71 | if (ev->ch != '0' && isdigit((int) ev->ch)) { 72 | editor->count = 0; 73 | while (isdigit((int) ev->ch)) { 74 | editor->count *= 10; 75 | editor->count += ev->ch - '0'; 76 | editor_waitkey(editor, ev); 77 | } 78 | } 79 | 80 | #define casemod(ch) case ch: if (!editor_try_modify(editor)) { break; } 81 | 82 | switch (ev->key) { 83 | casemod(TB_KEY_CTRL_R) editor_redo(editor); return; 84 | case TB_KEY_CTRL_B: window_page_up(editor->window); return; 85 | case TB_KEY_CTRL_F: window_page_down(editor->window); return; 86 | case TB_KEY_CTRL_T: editor_tag_stack_prev(editor); return; 87 | case TB_KEY_CTRL_C: 88 | editor_status_msg(editor, "Type :q to exit badavi"); 89 | return; 90 | case TB_KEY_CTRL_RSQ_BRACKET: { 91 | struct buf *word = motion_word_under_cursor(editor->window); 92 | editor_jump_to_tag(editor, word->buf); 93 | buf_free(word); 94 | return; 95 | } 96 | case TB_KEY_CTRL_6: 97 | if (editor->window->alternate_path) { 98 | editor_open(editor, editor->window->alternate_path); 99 | } else { 100 | editor_status_err(editor, "No alternate file"); 101 | } 102 | return; 103 | case TB_KEY_ENTER: { 104 | struct buffer *buffer = editor->window->buffer; 105 | if (!buffer->directory) { 106 | return; 107 | } 108 | size_t cursor = window_cursor(editor->window); 109 | struct buf *line = gb_getline(buffer->text, cursor); 110 | struct buf *path = buf_from_cstr(buffer->path); 111 | buf_append(path, "/"); 112 | buf_append(path, line->buf); 113 | editor_open(editor, path->buf); 114 | buf_free(line); 115 | buf_free(path); 116 | return; 117 | } 118 | case TB_KEY_CTRL_W: { 119 | editor_waitkey(editor, ev); 120 | struct window *next = NULL; 121 | switch (ev->key) { 122 | case TB_KEY_CTRL_H: next = window_left(editor->window); break; 123 | case TB_KEY_CTRL_L: next = window_right(editor->window); break; 124 | case TB_KEY_CTRL_K: next = window_up(editor->window); break; 125 | case TB_KEY_CTRL_J: next = window_down(editor->window); break; 126 | } 127 | 128 | int count = max(1, (int)editor->count); 129 | 130 | switch (ev->ch) { 131 | case 'h': next = window_left(editor->window); break; 132 | case 'l': next = window_right(editor->window); break; 133 | case 'k': next = window_up(editor->window); break; 134 | case 'j': next = window_down(editor->window); break; 135 | case '<': 136 | window_resize(editor->window, -count, 0); 137 | editor->count = 0; 138 | break; 139 | case '>': 140 | window_resize(editor->window, count, 0); 141 | editor->count = 0; 142 | break; 143 | case '-': 144 | window_resize(editor->window, 0, -count); 145 | editor->count = 0; 146 | break; 147 | case '+': 148 | window_resize(editor->window, 0, count); 149 | editor->count = 0; 150 | break; 151 | case '=': 152 | window_equalize(editor->window, WINDOW_SPLIT_HORIZONTAL); 153 | window_equalize(editor->window, WINDOW_SPLIT_VERTICAL); 154 | break; 155 | } 156 | 157 | if (next) { 158 | editor_set_window(editor, next); 159 | } 160 | 161 | return; 162 | } 163 | } 164 | 165 | struct gapbuf *gb = editor->window->buffer->text; 166 | size_t cursor = window_cursor(editor->window); 167 | switch (ev->ch) { 168 | case 0: 169 | break; 170 | case '-': { 171 | char *path = editor->window->buffer->path; 172 | if (path && !strcmp(path, "/")) { 173 | break; 174 | } 175 | 176 | if (!path) { 177 | editor_open(editor, "."); 178 | break; 179 | } 180 | 181 | bool dir = editor->window->buffer->directory; 182 | 183 | char buf[PATH_MAX]; 184 | snprintf(buf, sizeof(buf), "%s/..", path); 185 | editor_open(editor, buf); 186 | 187 | snprintf(buf, sizeof(buf), "^%s%s$", basename(path), dir ? "/" : ""); 188 | editor_jump_to_match(editor, buf, 0, SEARCH_FORWARDS); 189 | break; 190 | } 191 | case '"': { 192 | editor_waitkey(editor, ev); 193 | char name = (char) tolower((int) ev->ch); 194 | if (editor_get_register(editor, name)) { 195 | editor->register_ = name; 196 | } 197 | break; 198 | } 199 | casemod('i') editor_push_insert_mode(editor, 0); break; 200 | case 'v': editor_push_visual_mode(editor, VISUAL_MODE_CHARACTERWISE); break; 201 | case 'V': editor_push_visual_mode(editor, VISUAL_MODE_LINEWISE); break; 202 | case ':': editor_push_cmdline_mode(editor, ':'); break; 203 | casemod('p') { 204 | struct editor_register *r = editor_get_register(editor, editor->register_); 205 | char *text = r->read(r); 206 | size_t where = gb_getchar(gb, cursor) == '\n' ? cursor : cursor + 1; 207 | buffer_start_action_group(editor->window->buffer); 208 | buffer_do_insert(editor->window->buffer, buf_from_cstr(text), where); 209 | editor->register_ = '"'; 210 | free(text); 211 | break; 212 | } 213 | casemod('u') editor_undo(editor); break; 214 | casemod('a') editor_send_keys(editor, "li"); break; 215 | casemod('I') editor_send_keys(editor, "0i"); break; 216 | casemod('A') editor_send_keys(editor, "$i"); break; 217 | casemod('o') editor_send_keys(editor, "A"); break; 218 | casemod('O') 219 | if (cursor < gb->lines->buf[0]) { 220 | editor_send_keys(editor, "0i0\"zy^k\"zpi"); 221 | } else { 222 | editor_send_keys(editor, "ko"); 223 | } 224 | break; 225 | case 'x': editor_send_keys(editor, "dl"); break; 226 | case 'D': editor_send_keys(editor, "d$"); break; 227 | case 'C': editor_send_keys(editor, "c$"); break; 228 | casemod('J') editor_join_lines(editor); break; 229 | case 'g': { 230 | struct tb_event next; 231 | editor_waitkey(editor, &next); 232 | if (next.ch == 'f') { 233 | struct buf *file = motion_filename_under_cursor(editor->window); 234 | if (file) { 235 | char *path = editor_find_in_path(editor, file->buf); 236 | if (!path) { 237 | editor_status_err(editor, "Can't find file \"%s\" in path", file->buf); 238 | } else { 239 | editor_open(editor, path); 240 | free(path); 241 | } 242 | buf_free(file); 243 | } 244 | break; 245 | } 246 | editor_push_event(editor, &next); 247 | ATTR_FALLTHROUGH; 248 | } 249 | default: { 250 | struct motion *motion = motion_get(editor, ev); 251 | if (motion) { 252 | window_set_cursor(editor->window, motion_apply(motion, editor)); 253 | break; 254 | } 255 | editor_push_operator_pending_mode(editor, ev->ch); 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /insert_mode.c: -------------------------------------------------------------------------------- 1 | #include "mode.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "buf.h" 9 | #include "buffer.h" 10 | #include "editor.h" 11 | #include "gap.h" 12 | #include "motion.h" 13 | #include "window.h" 14 | 15 | static void editor_show_mode(struct editor *editor) { 16 | if (editor->opt.showmode) { 17 | editor_status_msg(editor, "-- INSERT --"); 18 | } 19 | } 20 | 21 | void insert_mode_entered(struct editor *editor) { 22 | struct insert_mode *mode = editor_get_insert_mode(editor); 23 | mode->completion_prefix = NULL; 24 | mode->completions = NULL; 25 | mode->completion = NULL; 26 | 27 | editor_show_mode(editor); 28 | buffer_start_action_group(editor->window->buffer); 29 | } 30 | 31 | void insert_mode_exited(struct editor *editor) { 32 | // If we exit insert mode without making changes, let's not add a 33 | // useless undo action. 34 | // TODO(isbadawi): Maybe this belongs in an "editor_end_action_group" 35 | struct action_group_list *undo_stack = &editor->window->buffer->undo_stack; 36 | struct edit_action_group *group = TAILQ_FIRST(undo_stack); 37 | if (TAILQ_EMPTY(&group->actions)) { 38 | TAILQ_REMOVE(undo_stack, group, pointers); 39 | } 40 | 41 | buf_clear(editor->status); 42 | 43 | struct insert_mode *mode = editor_get_insert_mode(editor); 44 | free(mode->completion_prefix); 45 | mode->completion_prefix = NULL; 46 | mode->completion = NULL; 47 | editor->popup.visible = false; 48 | if (mode->completions) { 49 | history_deinit(mode->completions); 50 | free(mode->completions); 51 | mode->completions = NULL; 52 | } 53 | } 54 | 55 | static void insert_indent(struct buffer *buffer, size_t cursor) { 56 | if (!buffer->opt.autoindent) { 57 | return; 58 | } 59 | 60 | struct gapbuf *gb = buffer->text; 61 | ssize_t newline = gb_lastindexof(gb, '\n', cursor - 1); 62 | ssize_t nonblank = newline; 63 | char ch; 64 | do { 65 | ++nonblank; 66 | ch = gb_getchar(gb, nonblank); 67 | } while (isspace(ch) && ch != '\n'); 68 | 69 | struct buf *indent = gb_getstring(gb, newline + 1, nonblank - newline - 1); 70 | 71 | if (buffer->opt.smartindent) { 72 | struct buf *line = gb_getline(gb, cursor); 73 | buf_strip_whitespace(line); 74 | 75 | bool shift = buf_endswith(line, "{"); 76 | if (!shift) { 77 | char *words = xstrdup(buffer->opt.cinwords); 78 | char *word = strtok(words, ","); 79 | do { 80 | if (buf_startswith(line, word)) { 81 | shift = true; 82 | break; 83 | } 84 | } while ((word = strtok(NULL, ","))); 85 | 86 | free(words); 87 | } 88 | buf_free(line); 89 | 90 | if (shift) { 91 | buf_appendf(indent, "%*s", buffer->opt.shiftwidth, ""); 92 | } 93 | } 94 | 95 | buffer_do_insert(buffer, indent, cursor + 1); 96 | } 97 | 98 | static void editor_exit_completion(struct editor *editor) { 99 | struct editor_popup *popup = &editor->popup; 100 | struct insert_mode *mode = editor_get_insert_mode(editor); 101 | if (popup->visible) { 102 | popup->visible = false; 103 | popup->len = 0; 104 | popup->selected = 0; 105 | free(popup->lines); 106 | popup->lines = NULL; 107 | mode->completion = NULL; 108 | } 109 | editor_show_mode(editor); 110 | } 111 | 112 | static void editor_select_completion(struct editor *editor) { 113 | struct insert_mode *mode = editor_get_insert_mode(editor); 114 | struct buffer *buffer = editor->window->buffer; 115 | size_t cursor = window_cursor(editor->window); 116 | struct editor_popup *popup = &editor->popup; 117 | buffer_do_delete(buffer, cursor - popup->pos, popup->pos); 118 | buffer_do_insert(buffer, buf_copy(mode->completion->buf), popup->pos); 119 | if (popup->len == 1) { 120 | editor_exit_completion(editor); 121 | editor_status_msg(editor, "the only match"); 122 | return; 123 | } 124 | editor_status_msg(editor, "match %d of %d", popup->selected + 1, popup->len); 125 | } 126 | 127 | #define FIRST_COMPLETION() \ 128 | history_first(mode->completions, buf_startswith, mode->completion_prefix) 129 | 130 | #define LAST_COMPLETION() \ 131 | history_last(mode->completions, buf_startswith, mode->completion_prefix) 132 | 133 | #define PREV_COMPLETION(entry) \ 134 | history_prev(entry, buf_startswith, mode->completion_prefix) 135 | 136 | #define NEXT_COMPLETION(entry) \ 137 | history_next(entry, buf_startswith, mode->completion_prefix) 138 | 139 | static void editor_refresh_completions(struct editor *editor) { 140 | struct insert_mode *mode = editor_get_insert_mode(editor); 141 | struct editor_popup *popup = &editor->popup; 142 | if (!popup->visible) { 143 | return; 144 | } 145 | 146 | if (window_cursor(editor->window) == popup->pos) { 147 | editor_exit_completion(editor); 148 | return; 149 | } 150 | 151 | struct buf *word = motion_word_before_cursor(editor->window); 152 | mode->completion_prefix = word->buf; 153 | free(word); 154 | mode->completion = FIRST_COMPLETION(); 155 | if (!mode->completion) { 156 | editor_exit_completion(editor); 157 | return; 158 | } 159 | 160 | popup->len = 0; 161 | popup->selected = 0; 162 | struct history_entry *entry; 163 | for (entry = mode->completion; entry; entry = NEXT_COMPLETION(entry)) { 164 | popup->len++; 165 | } 166 | 167 | free(popup->lines); 168 | popup->lines = xmalloc(popup->len * sizeof(*popup->lines)); 169 | int i = 0; 170 | for (entry = mode->completion; entry; entry = NEXT_COMPLETION(entry)) { 171 | popup->lines[i++] = entry->buf->buf; 172 | } 173 | } 174 | 175 | static void editor_init_completion(struct editor *editor) { 176 | struct insert_mode *mode = editor_get_insert_mode(editor); 177 | struct buffer *buffer = editor->window->buffer; 178 | size_t cursor = window_cursor(editor->window); 179 | struct editor_popup *popup = &editor->popup; 180 | if (!mode->completions) { 181 | mode->completions = xmalloc(sizeof(*mode->completions)); 182 | history_init(mode->completions, NULL); 183 | editor_load_completions(editor, COMPLETION_TAGS, mode->completions); 184 | } 185 | 186 | popup->visible = true; 187 | editor_refresh_completions(editor); 188 | if (!popup->visible) { 189 | return; 190 | } 191 | 192 | popup->pos = cursor; 193 | for (char *p = mode->completion_prefix; *p; ++p) { 194 | popup->pos = gb_utf8prev(buffer->text, popup->pos); 195 | } 196 | 197 | } 198 | 199 | static void editor_prev_completion(struct editor *editor) { 200 | struct insert_mode *mode = editor_get_insert_mode(editor); 201 | struct editor_popup *popup = &editor->popup; 202 | if (!popup->visible) { 203 | editor_init_completion(editor); 204 | } 205 | if (!popup->visible) { 206 | return; 207 | } 208 | 209 | popup->selected--; 210 | mode->completion = PREV_COMPLETION(mode->completion); 211 | if (popup->selected < 0) { 212 | popup->selected = popup->len - 1; 213 | mode->completion = LAST_COMPLETION(); 214 | } 215 | editor_select_completion(editor); 216 | } 217 | 218 | static void editor_next_completion(struct editor *editor) { 219 | struct insert_mode *mode = editor_get_insert_mode(editor); 220 | struct editor_popup *popup = &editor->popup; 221 | if (!popup->visible) { 222 | editor_init_completion(editor); 223 | if (popup->visible) { 224 | mode->completion = FIRST_COMPLETION(); 225 | editor_select_completion(editor); 226 | } 227 | return; 228 | } 229 | popup->selected++; 230 | mode->completion = NEXT_COMPLETION(mode->completion); 231 | if (popup->selected == popup->len) { 232 | popup->selected = 0; 233 | mode->completion = FIRST_COMPLETION(); 234 | } 235 | editor_select_completion(editor); 236 | } 237 | 238 | void insert_mode_key_pressed(struct editor* editor, struct tb_event* ev) { 239 | struct buffer *buffer = editor->window->buffer; 240 | size_t cursor = window_cursor(editor->window); 241 | uint32_t ch; 242 | switch (ev->key) { 243 | case TB_KEY_ESC: case TB_KEY_CTRL_C: editor_pop_mode(editor); return; 244 | case TB_KEY_CTRL_Y: editor_exit_completion(editor); return; 245 | case TB_KEY_CTRL_P: editor_prev_completion(editor); return; 246 | case TB_KEY_CTRL_N: editor_next_completion(editor); return; 247 | case TB_KEY_BACKSPACE: 248 | if (cursor > 0) { 249 | size_t prev = gb_utf8prev(buffer->text, cursor); 250 | buffer_do_delete(buffer, gb_utf8len(buffer->text, prev), prev); 251 | editor_refresh_completions(editor); 252 | } 253 | return; 254 | case TB_KEY_ENTER: ch = '\n'; break; 255 | case TB_KEY_SPACE: ch = ' '; break; 256 | case TB_KEY_TAB: ch = '\t'; break; 257 | default: ch = ev->ch; break; 258 | } 259 | 260 | if (isspace(ch)) { 261 | editor_exit_completion(editor); 262 | } 263 | 264 | struct buf *insertion; 265 | if (ch == '\t' && buffer->opt.expandtab) { 266 | insertion = buf_create(buffer->opt.shiftwidth); 267 | buf_printf(insertion, "%*s", buffer->opt.shiftwidth, ""); 268 | } else { 269 | insertion = buf_from_utf8(ch); 270 | } 271 | 272 | buffer_do_insert(buffer, insertion, cursor); 273 | if (ch == '\n') { 274 | // TODO(ibadawi): If we add indent then leave insert mode, remove it 275 | insert_indent(buffer, cursor); 276 | } 277 | 278 | editor_refresh_completions(editor); 279 | } 280 | -------------------------------------------------------------------------------- /gap.c: -------------------------------------------------------------------------------- 1 | #include "gap.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "buf.h" 11 | #include "util.h" 12 | 13 | #define GAPSIZE 1024 14 | 15 | struct gapbuf *gb_create(void) { 16 | struct gapbuf *gb = xmalloc(sizeof(*gb)); 17 | 18 | gb->bufstart = xmalloc(1 + GAPSIZE); 19 | gb->gapstart = gb->bufstart; 20 | gb->gapend = gb->gapstart + GAPSIZE; 21 | gb->bufend = gb->bufstart + GAPSIZE + 1; 22 | gb->bufend[-1] = '\n'; 23 | 24 | gb->lines = intbuf_create(10); 25 | intbuf_add(gb->lines, 0); 26 | return gb; 27 | } 28 | 29 | static struct gapbuf *gb_load(FILE *fp, size_t filesize) { 30 | struct gapbuf *gb = xmalloc(sizeof(*gb)); 31 | size_t bufsize = filesize + GAPSIZE; 32 | 33 | gb->bufstart = xmalloc(bufsize + 1); 34 | gb->gapstart = gb->bufstart; 35 | gb->gapend = gb->bufstart + GAPSIZE; 36 | gb->bufend = gb->bufstart + bufsize; 37 | 38 | fread(gb->gapend, 1, filesize, fp); 39 | fclose(fp); 40 | 41 | if (gb->bufend[-1] != '\n') { 42 | gb->bufend[0] = '\n'; 43 | gb->bufend++; 44 | filesize++; 45 | } 46 | 47 | gb->lines = intbuf_create(10); 48 | ssize_t last = -1; 49 | for (ssize_t i = 0; (size_t) i < filesize; ++i) { 50 | if (gb->gapend[i] == '\n') { 51 | unsigned int len = (unsigned int) (i - last) - 1; 52 | intbuf_add(gb->lines, len); 53 | last = (ssize_t) i; 54 | } 55 | } 56 | 57 | return gb; 58 | } 59 | 60 | struct gapbuf *gb_fromfile(char *path) { 61 | FILE *fp = fopen(path, "r"); 62 | if (!fp) { 63 | return NULL; 64 | } 65 | 66 | struct stat info; 67 | fstat(fileno(fp), &info); 68 | size_t filesize = (size_t) info.st_size; 69 | return gb_load(fp, filesize); 70 | } 71 | 72 | struct gapbuf *gb_fromstring(struct buf *buf) { 73 | FILE *fp = fmemopen(buf->buf, buf->len, "r"); 74 | size_t len = buf->len; 75 | return gb_load(fp, len); 76 | } 77 | 78 | void gb_free(struct gapbuf *gb) { 79 | free(gb->bufstart); 80 | intbuf_free(gb->lines); 81 | free(gb); 82 | } 83 | 84 | size_t gb_size(struct gapbuf *gb) { 85 | return (size_t) ((gb->gapstart - gb->bufstart) + (gb->bufend - gb->gapend)); 86 | } 87 | 88 | size_t gb_nlines(struct gapbuf *gb) { 89 | return gb->lines->len; 90 | } 91 | 92 | void gb_save(struct gapbuf *gb, FILE *fp) { 93 | fwrite(gb->bufstart, 1, (size_t) (gb->gapstart - gb->bufstart), fp); 94 | fwrite(gb->gapend, 1, (size_t) (gb->bufend - gb->gapend), fp); 95 | } 96 | 97 | // Returns the real index of the logical offset pos. 98 | static size_t gb_index(struct gapbuf *gb, size_t pos) { 99 | ptrdiff_t gapsize = gb->gapend - gb->gapstart; 100 | return gb->bufstart + pos < gb->gapstart ? pos : pos + (size_t) gapsize; 101 | } 102 | 103 | char gb_getchar(struct gapbuf *gb, size_t pos) { 104 | return gb->bufstart[gb_index(gb, pos)]; 105 | } 106 | 107 | int gb_utf8len(struct gapbuf *gb, size_t pos) { 108 | return tb_utf8_char_length(gb_getchar(gb, pos)); 109 | } 110 | 111 | uint32_t gb_utf8(struct gapbuf *gb, size_t pos) { 112 | char buf[8]; 113 | int clen = gb_utf8len(gb, pos); 114 | for (int i = 0; i < clen; ++i) { 115 | buf[i] = gb_getchar(gb, pos + i); 116 | } 117 | uint32_t ch; 118 | int ulen = tb_utf8_char_to_unicode(&ch, buf); 119 | assert(clen == ulen); 120 | return ch; 121 | } 122 | 123 | struct buf *gb_getstring(struct gapbuf *gb, size_t pos, size_t n) { 124 | struct buf *strbuf = buf_create(n + 1); 125 | char *buf = strbuf->buf; 126 | gb_getstring_into(gb, pos, n, buf); 127 | strbuf->len = n; 128 | return strbuf; 129 | } 130 | 131 | void gb_getstring_into(struct gapbuf *gb, size_t pos, size_t n, char *buf) { 132 | char *start = gb->bufstart + gb_index(gb, pos); 133 | char *end = gb->bufstart + gb_index(gb, pos + n); 134 | if (end < gb->gapstart || start >= gb->gapend) { 135 | memcpy(buf, start, n); 136 | } else { 137 | assert(gb->gapstart >= start); 138 | assert(end >= gb->gapend); 139 | size_t l = (size_t)(gb->gapstart - start); 140 | size_t r = (size_t)(end - gb->gapend); 141 | memcpy(buf, start, l); 142 | memcpy(buf + l, gb->gapend, r); 143 | } 144 | buf[n] = '\0'; 145 | } 146 | 147 | struct buf *gb_getline(struct gapbuf *gb, size_t pos) { 148 | size_t line, column; 149 | gb_pos_to_linecol(gb, pos, &line, &column); 150 | return gb_getstring(gb, pos - column, gb->lines->buf[line]); 151 | } 152 | 153 | // Moves the gap so that gb->bufstart + pos == gb->gapstart. 154 | void gb_mvgap(struct gapbuf *gb, size_t pos) { 155 | char *point = gb->bufstart + gb_index(gb, pos); 156 | if (gb->gapend <= point) { 157 | size_t n = (size_t)(point - gb->gapend); 158 | memcpy(gb->gapstart, gb->gapend, n); 159 | gb->gapstart += n; 160 | gb->gapend += n; 161 | } else if (point < gb->gapstart) { 162 | size_t n = (size_t)(gb->gapstart - point); 163 | memcpy(gb->gapend - n, point, n); 164 | gb->gapstart -= n; 165 | gb->gapend -= n; 166 | } 167 | } 168 | 169 | // Ensure the gap fits at least n new characters. 170 | static void gb_growgap(struct gapbuf *gb, size_t n) { 171 | ptrdiff_t gapsize = gb->gapend - gb->gapstart; 172 | if (n <= (size_t) gapsize) { 173 | return; 174 | } 175 | 176 | size_t newgapsize = 0; 177 | while (newgapsize < 2*n) { 178 | newgapsize += GAPSIZE; 179 | } 180 | // Pointers will be obsoleted so remember offsets... 181 | ptrdiff_t leftsize = gb->gapstart - gb->bufstart; 182 | ptrdiff_t rightsize = gb->bufend - gb->gapend; 183 | 184 | size_t newsize = (size_t) (leftsize + rightsize) + newgapsize; 185 | 186 | gb->bufstart = xrealloc(gb->bufstart, newsize); 187 | gb->gapstart = gb->bufstart + leftsize; 188 | gb->gapend = gb->gapstart + newgapsize; 189 | assert(rightsize >= 0); 190 | // Move the bit after the gap right by gapsize positions... 191 | memcpy(gb->gapend, gb->gapstart + gapsize, (size_t)rightsize); 192 | gb->bufend = gb->bufstart + newsize; 193 | } 194 | 195 | void gb_putchar(struct gapbuf *gb, char c, size_t pos) { 196 | gb_putstring(gb, &c, 1, pos); 197 | } 198 | 199 | void gb_putstring(struct gapbuf *gb, char *buf, size_t n, size_t pos) { 200 | gb_growgap(gb, n); 201 | gb_mvgap(gb, pos); 202 | memcpy(gb->gapstart, buf, n); 203 | 204 | size_t line, col; 205 | gb_pos_to_linecol(gb, pos, &line, &col); 206 | 207 | // Adjust the line lengths. 208 | 209 | // Where we started inserting on this line 210 | size_t start = col; 211 | // Characters since last newline 212 | size_t last = 0; 213 | for (size_t i = 0; i < n; ++i) { 214 | if (gb->gapstart[i] == '\n') { 215 | size_t oldlen = gb->lines->buf[line]; 216 | size_t newlen = start + last; 217 | gb->lines->buf[line] = (unsigned int) newlen; 218 | intbuf_insert(gb->lines, (unsigned int) (oldlen - newlen), ++line); 219 | start = last = 0; 220 | } else { 221 | gb->lines->buf[line]++; 222 | last++; 223 | } 224 | } 225 | gb->gapstart += n; 226 | } 227 | 228 | void gb_del(struct gapbuf *gb, size_t n, size_t pos) { 229 | gb_mvgap(gb, pos); 230 | 231 | size_t line, col; 232 | gb_pos_to_linecol(gb, pos, &line, &col); 233 | 234 | for (size_t i = 0; i < n; ++i) { 235 | if (gb->gapstart[-i - 1] == '\n') { 236 | gb->lines->buf[line - 1] += gb->lines->buf[line]; 237 | intbuf_remove(gb->lines, line); 238 | line--; 239 | } else { 240 | gb->lines->buf[line]--; 241 | } 242 | } 243 | 244 | gb->gapstart -= n; 245 | 246 | // Empty files are tricky for us, so insert a newline if needed... 247 | if (!gb_size(gb)) { 248 | *(gb->gapstart++) = '\n'; 249 | intbuf_add(gb->lines, 0); 250 | } 251 | } 252 | 253 | size_t gb_indexof(struct gapbuf *gb, char c, size_t start) { 254 | size_t size = gb_size(gb); 255 | for (size_t i = start; i < size; ++i) { 256 | if (gb_getchar(gb, i) == c) { 257 | return i; 258 | } 259 | } 260 | return size; 261 | } 262 | 263 | ssize_t gb_lastindexof(struct gapbuf *gb, char c, size_t start) { 264 | for (ssize_t i = (ssize_t) start; i >= 0; --i) { 265 | if (gb_getchar(gb, (size_t) i) == c) { 266 | return i; 267 | } 268 | } 269 | return -1; 270 | } 271 | 272 | void gb_pos_to_linecol(struct gapbuf *gb, size_t pos, size_t *line, size_t *column) { 273 | size_t offset = 0; 274 | *line = *column = 0; 275 | for (size_t i = 0; i < gb->lines->len; ++i) { 276 | size_t len = gb->lines->buf[i]; 277 | *line = i; 278 | if (offset <= pos && pos <= offset + len) { 279 | break; 280 | } 281 | offset += len + 1; 282 | } 283 | for (size_t i = offset; i != pos; i = gb_utf8next(gb, i)) { 284 | (*column)++; 285 | } 286 | } 287 | 288 | size_t gb_linecol_to_pos(struct gapbuf *gb, size_t line, size_t column) { 289 | size_t offset = 0; 290 | for (size_t i = 0; i < line; ++i) { 291 | offset += gb->lines->buf[i] + 1; 292 | } 293 | for (size_t i = 0; i < column; ++i) { 294 | offset += gb_utf8len(gb, offset); 295 | } 296 | return offset; 297 | } 298 | 299 | size_t gb_utf8len_line(struct gapbuf *gb, size_t pos) { 300 | size_t len = 0; 301 | while (gb_getchar(gb, pos) != '\n') { 302 | pos += gb_utf8len(gb, pos); 303 | len++; 304 | } 305 | return len; 306 | } 307 | 308 | size_t gb_utf8next(struct gapbuf *gb, size_t pos) { 309 | return pos + gb_utf8len(gb, pos); 310 | } 311 | 312 | static bool isutf8start(char c) { 313 | return (c & 0xc0) != 0x80; 314 | } 315 | 316 | size_t gb_utf8prev(struct gapbuf *gb, size_t pos) { 317 | do { 318 | pos--; 319 | } while (!isutf8start(gb_getchar(gb, pos))); 320 | return pos; 321 | } 322 | -------------------------------------------------------------------------------- /cmdline_mode.c: -------------------------------------------------------------------------------- 1 | #include "mode.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "buf.h" 15 | #include "editor.h" 16 | #include "history.h" 17 | #include "search.h" 18 | #include "tags.h" 19 | #include "util.h" 20 | #include "window.h" 21 | 22 | void cmdline_mode_entered(struct editor *editor) { 23 | struct cmdline_mode *mode = editor_get_cmdline_mode(editor); 24 | mode->prompt = (char) mode->mode.arg; 25 | editor_status_msg(editor, "%c", mode->prompt); 26 | mode->cursor = window_cursor(editor->window); 27 | mode->history_entry = NULL; 28 | mode->history_prefix = buf_from_cstr(""); 29 | 30 | mode->completions = NULL; 31 | mode->completion = NULL; 32 | 33 | editor->status_cursor = 1; 34 | editor->status_silence = true; 35 | } 36 | 37 | void cmdline_mode_exited(struct editor *editor) { 38 | struct cmdline_mode *mode = editor_get_cmdline_mode(editor); 39 | buf_free(mode->history_prefix); 40 | if (!editor->message->len) { 41 | editor->status_cursor = 0; 42 | } 43 | editor->status_silence = false; 44 | editor->window->have_incsearch_match = false; 45 | 46 | mode->completion = NULL; 47 | if (mode->completions) { 48 | history_deinit(mode->completions); 49 | free(mode->completions); 50 | mode->completions = NULL; 51 | } 52 | } 53 | 54 | static void search_done_cb(struct editor *editor, char *command) { 55 | editor->status_silence = false; 56 | struct cmdline_mode *mode = editor_get_cmdline_mode(editor); 57 | enum search_direction direction = 58 | mode->prompt == '/' ? SEARCH_FORWARDS : SEARCH_BACKWARDS; 59 | if (*command) { 60 | editor_jump_to_match(editor, command, mode->cursor, direction); 61 | struct editor_register *lsp = editor_get_register(editor, '/'); 62 | lsp->write(lsp, command); 63 | history_add_item(&editor->search_history, command); 64 | } else { 65 | editor_jump_to_match(editor, NULL, mode->cursor, direction); 66 | } 67 | editor->highlight_search_matches = true; 68 | } 69 | 70 | EDITOR_COMMAND(nohlsearch, noh) { 71 | editor->highlight_search_matches = false; 72 | } 73 | 74 | static void search_char_cb(struct editor *editor, char *command) { 75 | if (!editor->opt.incsearch || !*command) { 76 | return; 77 | } 78 | struct cmdline_mode *mode = editor_get_cmdline_mode(editor); 79 | enum search_direction direction = 80 | mode->prompt == '/' ? SEARCH_FORWARDS : SEARCH_BACKWARDS; 81 | editor->window->have_incsearch_match = editor_search( 82 | editor, command, mode->cursor, direction, &editor->window->incsearch_match); 83 | if (editor->window->have_incsearch_match) { 84 | window_set_cursor(editor->window, editor->window->incsearch_match.start); 85 | } else { 86 | window_set_cursor(editor->window, mode->cursor); 87 | } 88 | } 89 | 90 | static void command_done_cb(struct editor *editor, char *command) { 91 | editor->status_silence = false; 92 | history_add_item(&editor->command_history, command); 93 | editor_execute_command(editor, command); 94 | } 95 | 96 | void editor_load_completions(struct editor *editor, 97 | enum completion_kind kind, struct history *history) { 98 | switch (kind) { 99 | case COMPLETION_NONE: assert(0); break; 100 | case COMPLETION_CMDS: { 101 | int len; 102 | char **completions = commands_get_sorted(&len); 103 | for (int i = len - 1; i >= 0; --i) { 104 | history_add_item(history, completions[i]); 105 | } 106 | free(completions); 107 | break; 108 | } 109 | case COMPLETION_OPTIONS: { 110 | int len; 111 | char **options = options_get_sorted(&len); 112 | for (int i = len - 1; i >= 0; --i) { 113 | history_add_item(history, options[i]); 114 | } 115 | free(options); 116 | break; 117 | } 118 | case COMPLETION_TAGS: 119 | for (size_t i = 0; i < editor->tags->len; ++i) { 120 | size_t j = editor->tags->len - 1 - i; 121 | history_add_item(history, editor->tags->tags[j].name); 122 | } 123 | break; 124 | case COMPLETION_PATHS: { 125 | // TODO(ibadawi): completion for subdirectories 126 | char buf[PATH_MAX]; 127 | struct dirent **namelist; 128 | int n; 129 | 130 | n = scandir(".", &namelist, NULL, alphasort); 131 | if (n <= 0) { 132 | break; 133 | } 134 | 135 | for (int i = n - 1; i >= 0; --i) { 136 | struct dirent *entry = namelist[i]; 137 | if (*entry->d_name == '.') { 138 | free(entry); 139 | continue; 140 | } 141 | 142 | strcpy(buf, entry->d_name); 143 | if (entry->d_type == DT_DIR) { 144 | strcat(buf, "/"); 145 | } 146 | history_add_item(history, buf); 147 | free(entry); 148 | } 149 | free(namelist); 150 | break; 151 | } 152 | } 153 | } 154 | 155 | void cmdline_mode_key_pressed(struct editor *editor, struct tb_event *ev) { 156 | char ch; 157 | struct cmdline_mode *mode = editor_get_cmdline_mode(editor); 158 | 159 | void (*done_cb)(struct editor*, char*); 160 | void (*char_cb)(struct editor*, char*); 161 | struct history *history; 162 | switch (mode->prompt) { 163 | case ':': 164 | history = &editor->command_history; 165 | done_cb = command_done_cb; 166 | char_cb = NULL; 167 | break; 168 | case '/': case '?': 169 | history = &editor->search_history; 170 | done_cb = search_done_cb; 171 | char_cb = search_char_cb; 172 | break; 173 | default: assert(0); 174 | } 175 | 176 | switch (ev->key) { 177 | case TB_KEY_TAB: { 178 | if (mode->prompt != ':') { 179 | return; 180 | } 181 | 182 | char *command = mode->history_prefix->buf; 183 | enum completion_kind kind = COMPLETION_NONE; 184 | char *arg = NULL; 185 | struct editor_command *cmd = command_parse(command, &arg, NULL); 186 | size_t skip = 0; 187 | if (cmd && arg && cmd->completion_kind != COMPLETION_NONE) { 188 | kind = cmd->completion_kind; 189 | skip = arg - command; 190 | } else if (!strchr(command, ' ')) { 191 | kind = COMPLETION_CMDS; 192 | } 193 | 194 | if (kind == COMPLETION_NONE) { 195 | return; 196 | } 197 | 198 | if (mode->completions && mode->completion_kind != kind) { 199 | history_deinit(mode->completions); 200 | free(mode->completions); 201 | mode->completions = NULL; 202 | mode->completion = NULL; 203 | } 204 | mode->completion_kind = kind; 205 | 206 | if (!mode->completions) { 207 | mode->completions = xmalloc(sizeof(*mode->completions)); 208 | history_init(mode->completions, NULL); 209 | editor_load_completions(editor, kind, mode->completions); 210 | } 211 | 212 | if (mode->completion) { 213 | mode->completion = history_next(mode->completion, 214 | buf_startswith, command + skip); 215 | } 216 | if (!mode->completion) { 217 | mode->completion = history_first(mode->completions, 218 | buf_startswith, command + skip); 219 | } 220 | 221 | if (mode->completion) { 222 | char c = command[skip]; 223 | command[skip] = '\0'; 224 | buf_printf(editor->status, "%c%s%s", 225 | mode->prompt, command, mode->completion->buf->buf); 226 | command[skip] = c; 227 | editor->status_cursor = editor->status->len; 228 | } 229 | return; 230 | } 231 | case TB_KEY_ESC: case TB_KEY_CTRL_C: 232 | buf_clear(editor->status); 233 | window_set_cursor(editor->window, mode->cursor); 234 | editor_pop_mode(editor); 235 | return; 236 | case TB_KEY_ARROW_LEFT: 237 | if (editor->status_cursor == 1) { 238 | return; 239 | } 240 | editor->status_cursor--; 241 | if (ev->meta == TB_META_SHIFT) { 242 | while (editor->status_cursor > 1 && 243 | !isspace(editor->status->buf[editor->status_cursor - 1])) { 244 | editor->status_cursor--; 245 | } 246 | } 247 | return; 248 | case TB_KEY_ARROW_RIGHT: 249 | if (editor->status_cursor == editor->status->len) { 250 | return; 251 | } 252 | editor->status_cursor++; 253 | if (ev->meta == TB_META_SHIFT) { 254 | while (editor->status_cursor < editor->status->len && 255 | !isspace(editor->status->buf[editor->status_cursor])) { 256 | editor->status_cursor++; 257 | } 258 | } 259 | return; 260 | case TB_KEY_ARROW_UP: 261 | if (!mode->history_entry) { 262 | mode->history_entry = history_first( 263 | history, buf_startswith, mode->history_prefix->buf); 264 | } else { 265 | struct history_entry *next = history_next( 266 | mode->history_entry, buf_startswith, mode->history_prefix->buf); 267 | if (!next) { 268 | return; 269 | } 270 | mode->history_entry = next; 271 | } 272 | if (mode->history_entry) { 273 | struct buf *command = mode->history_entry->buf; 274 | buf_printf(editor->status, "%c%s", mode->prompt, command->buf); 275 | editor->status_cursor = command->len + 1; 276 | } 277 | return; 278 | case TB_KEY_ARROW_DOWN: 279 | if (mode->history_entry) { 280 | mode->history_entry = history_prev( 281 | mode->history_entry, buf_startswith, mode->history_prefix->buf); 282 | } 283 | if (mode->history_entry) { 284 | struct buf *command = mode->history_entry->buf; 285 | buf_printf(editor->status, "%c%s", mode->prompt, command->buf); 286 | editor->status_cursor = command->len + 1; 287 | } else { 288 | buf_printf(editor->status, "%c%s", mode->prompt, mode->history_prefix->buf); 289 | editor->status_cursor = mode->history_prefix->len + 1; 290 | } 291 | return; 292 | case TB_KEY_CTRL_B: case TB_KEY_HOME: 293 | editor->status_cursor = 1; 294 | return; 295 | case TB_KEY_CTRL_E: case TB_KEY_END: 296 | editor->status_cursor = editor->status->len; 297 | return; 298 | case TB_KEY_BACKSPACE: 299 | buf_delete(editor->status, --editor->status_cursor, 1); 300 | buf_printf(mode->history_prefix, "%s", editor->status->buf + 1); 301 | if (editor->status->len == 0) { 302 | window_set_cursor(editor->window, mode->cursor); 303 | editor_pop_mode(editor); 304 | return; 305 | } else if (char_cb) { 306 | char *command = xstrdup(editor->status->buf + 1); 307 | char_cb(editor, command); 308 | free(command); 309 | } 310 | return; 311 | case TB_KEY_ENTER: { 312 | char *command = xstrdup(editor->status->buf + 1); 313 | done_cb(editor, command); 314 | editor_pop_mode(editor); 315 | free(command); 316 | return; 317 | } 318 | case TB_KEY_SPACE: 319 | ch = ' '; 320 | break; 321 | default: 322 | ch = (char) ev->ch; 323 | } 324 | char s[2] = {ch, '\0'}; 325 | buf_insert(editor->status, s, editor->status_cursor++); 326 | buf_printf(mode->history_prefix, "%s", editor->status->buf + 1); 327 | if (char_cb) { 328 | char *command = xstrdup(editor->status->buf + 1); 329 | char_cb(editor, command); 330 | free(command); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /options.c: -------------------------------------------------------------------------------- 1 | #include "options.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "buf.h" 11 | #include "buffer.h" 12 | #include "editor.h" 13 | #include "window.h" 14 | 15 | static inline void option_set_int(int *p, int v) { *p = v; } 16 | static inline void option_set_bool(bool *p, bool v) { *p = v; } 17 | static inline void option_set_string(string *p, string v) { *p = xstrdup(v); } 18 | 19 | static inline void option_free_int(int i ATTR_UNUSED) { return; } 20 | static inline void option_free_bool(bool b ATTR_UNUSED) { return; } 21 | static inline void option_free_string(string s) { free(s); } 22 | 23 | struct opt { 24 | char *name; 25 | enum opt_scope { 26 | OPTION_SCOPE_EDITOR, 27 | OPTION_SCOPE_WINDOW, 28 | OPTION_SCOPE_BUFFER, 29 | } scope; 30 | enum opt_type { 31 | OPTION_TYPE_bool, 32 | OPTION_TYPE_int, 33 | OPTION_TYPE_string, 34 | } type; 35 | union { 36 | int intval; 37 | bool boolval; 38 | string stringval; 39 | } defaultval; 40 | }; 41 | 42 | static struct opt opts_meta[] = { 43 | #define OPTION(name, type, defaultval) \ 44 | {#name, OPTION_SCOPE_BUFFER, OPTION_TYPE_##type, {.type##val = defaultval}}, 45 | BUFFER_OPTIONS 46 | #undef OPTION 47 | #define OPTION(name, type, defaultval) \ 48 | {#name, OPTION_SCOPE_WINDOW, OPTION_TYPE_##type, {.type##val = defaultval}}, 49 | WINDOW_OPTIONS 50 | #undef OPTION 51 | #define OPTION(name, type, defaultval) \ 52 | {#name, OPTION_SCOPE_EDITOR, OPTION_TYPE_##type, {.type##val = defaultval}}, 53 | EDITOR_OPTIONS 54 | #undef OPTION 55 | }; 56 | static int num_options = sizeof(opts_meta) / sizeof(opts_meta[0]); 57 | 58 | #define FOREACH_OPTION(varname) \ 59 | for (struct opt *varname = opts_meta; varname < opts_meta + num_options; ++varname) 60 | 61 | static struct opt *option_info(char *name) { 62 | FOREACH_OPTION(opt) { 63 | if (!strcmp(opt->name, name)) { 64 | return opt; 65 | } 66 | } 67 | return NULL; 68 | } 69 | 70 | static int pstrcmp(const void *a, const void *b) { 71 | return strcmp(*(char * const *)a, *(char * const *)b); 72 | } 73 | 74 | char **options_get_sorted(int *len) { 75 | char **options = xmalloc(sizeof(*options) * num_options); 76 | int i = 0; 77 | FOREACH_OPTION(opt) { 78 | options[i++] = opt->name; 79 | } 80 | 81 | qsort(options, num_options, sizeof(*options), pstrcmp); 82 | 83 | *len = num_options; 84 | return options; 85 | } 86 | 87 | void editor_init_options(struct editor *editor) { 88 | #define OPTION(name, type, defaultval) \ 89 | option_set_##type(&editor->opt.name, defaultval); 90 | BUFFER_OPTIONS 91 | EDITOR_OPTIONS 92 | #undef OPTION 93 | } 94 | 95 | void window_init_options(struct window *window) { 96 | #define OPTION(name, type, defaultval) \ 97 | option_set_##type(&window->opt.name, defaultval); 98 | WINDOW_OPTIONS 99 | #undef OPTION 100 | } 101 | 102 | void window_free_options(struct window *window) { 103 | #define OPTION(name, type, _) \ 104 | option_free_##type(window->opt.name); 105 | WINDOW_OPTIONS 106 | #undef OPTION 107 | } 108 | 109 | void editor_free_options(struct editor *editor) { 110 | #define OPTION(name, type, _) \ 111 | option_free_##type(editor->opt.name); 112 | EDITOR_OPTIONS 113 | BUFFER_OPTIONS 114 | #undef OPTION 115 | } 116 | 117 | void buffer_free_options(struct buffer *buffer) { 118 | #define OPTION(name, type, _) \ 119 | option_free_##type(buffer->opt.name); 120 | BUFFER_OPTIONS 121 | #undef OPTION 122 | } 123 | 124 | void buffer_inherit_editor_options( 125 | struct buffer *buffer, struct editor *editor) { 126 | #define OPTION(name, type, _) \ 127 | option_set_##type(&buffer->opt.name, editor->opt.name); 128 | BUFFER_OPTIONS 129 | #undef OPTION 130 | 131 | if (buffer->directory) { 132 | buffer->opt.readonly = true; 133 | buffer->opt.modifiable = false; 134 | } 135 | 136 | if (buffer->path) { 137 | char *filetype = syntax_detect_filetype(buffer->path); 138 | if (*filetype) { 139 | free(buffer->opt.filetype); 140 | buffer->opt.filetype = xstrdup(filetype); 141 | } 142 | } 143 | } 144 | 145 | void window_inherit_parent_options(struct window *window) { 146 | #define OPTION(name, type, _) \ 147 | option_set_##type(&window->opt.name, window->parent->opt.name); 148 | WINDOW_OPTIONS 149 | #undef OPTION 150 | } 151 | 152 | static void editor_opt_vals( 153 | struct editor *editor, struct opt *info, void **global, void **local) { 154 | switch (info->scope) { 155 | case OPTION_SCOPE_WINDOW: 156 | #define OPTION(optname, _, __) \ 157 | if (!strcmp(info->name, #optname)) { \ 158 | *global = *local = &editor->window->opt.optname; \ 159 | return; \ 160 | } 161 | WINDOW_OPTIONS 162 | #undef OPTION 163 | break; 164 | case OPTION_SCOPE_BUFFER: 165 | #define OPTION(optname, _, __) \ 166 | if (!strcmp(info->name, #optname)) { \ 167 | *global = &editor->opt.optname; \ 168 | *local = &editor->window->buffer->opt.optname; \ 169 | return; \ 170 | } 171 | BUFFER_OPTIONS 172 | #undef OPTION 173 | break; 174 | case OPTION_SCOPE_EDITOR: 175 | #define OPTION(optname, _, __) \ 176 | if (!strcmp(info->name, #optname)) { \ 177 | *global = *local = &editor->opt.optname; \ 178 | return; \ 179 | } 180 | BUFFER_OPTIONS 181 | EDITOR_OPTIONS 182 | #undef OPTION 183 | } 184 | 185 | assert(0); 186 | } 187 | 188 | enum option_set_mode { 189 | OPTION_SET_LOCAL, 190 | OPTION_SET_GLOBAL, 191 | OPTION_SET_BOTH 192 | }; 193 | 194 | static bool csl_has(char *csl, char *word) { 195 | char *p, *words; 196 | p = words = xstrdup(csl); 197 | 198 | char *token; 199 | while ((token = strsep(&words, ","))) { 200 | if (!strcmp(token, word)) { 201 | free(p); 202 | return true; 203 | } 204 | } 205 | free(p); 206 | return false; 207 | } 208 | 209 | static void csl_append(char **csl, char *word) { 210 | size_t len = strlen(*csl); 211 | size_t wordlen = strlen(word); 212 | char *result = xmalloc(len + wordlen + 2); 213 | *result = '\0'; 214 | if (len) { 215 | strcat(result, *csl); 216 | if ((*csl)[len - 1] != ',') { 217 | strcat(result, ","); 218 | } 219 | } 220 | strcat(result, word); 221 | free(*csl); 222 | *csl = result; 223 | } 224 | 225 | static char *option_show(struct opt *info, void *val) { 226 | static char buf[512]; 227 | switch (info->type) { 228 | case OPTION_TYPE_int: 229 | snprintf(buf, sizeof(buf), "%s=%d", info->name, *(int*)val); 230 | break; 231 | case OPTION_TYPE_string: 232 | snprintf(buf, sizeof(buf), "%s=%s", info->name, *(string*)val); 233 | break; 234 | case OPTION_TYPE_bool: 235 | snprintf(buf, sizeof(buf), "%s%s", *(bool*)val ? "" : "no", info->name); 236 | break; 237 | } 238 | return buf; 239 | } 240 | 241 | static bool option_equals_default(struct opt *info, void *val) { 242 | switch (info->type) { 243 | case OPTION_TYPE_int: return *(int*)val == info->defaultval.intval; 244 | case OPTION_TYPE_bool: return *(bool*)val == info->defaultval.boolval; 245 | case OPTION_TYPE_string: return !strcmp(*(string*)val, info->defaultval.stringval); 246 | } 247 | assert(0); 248 | return false; 249 | } 250 | 251 | static void editor_command_set_impl( 252 | struct editor *editor, char *arg, enum option_set_mode which) { 253 | if (!arg || !strcmp(arg, "all")) { 254 | buf_printf(editor->message, "--- Options ---"); 255 | FOREACH_OPTION(opt) { 256 | void *unused, *val; 257 | editor_opt_vals(editor, opt, &unused, &val); 258 | if (arg || !option_equals_default(opt, val)) { 259 | buf_appendf(editor->message, "\n%s", option_show(opt, val)); 260 | } 261 | } 262 | editor_status_msg(editor, "Press ENTER to continue "); 263 | editor->status_cursor = editor->status->len - 1; 264 | return; 265 | } 266 | 267 | int errorcode = 0; 268 | PCRE2_SIZE erroroffset = 0; 269 | pcre2_code *regex = pcre2_compile( 270 | (unsigned char*) "(no)?([a-z]+)(\\+?=[0-9a-zA-Z,_]*|!|\\?|&)?", 271 | PCRE2_ZERO_TERMINATED, 272 | /* options */ 0, 273 | &errorcode, 274 | &erroroffset, 275 | NULL); 276 | assert(regex); 277 | 278 | pcre2_match_data *groups = pcre2_match_data_create_from_pattern(regex, NULL); 279 | int rc = pcre2_match(regex, (unsigned char*) arg, PCRE2_ZERO_TERMINATED, 0, 0, groups, NULL); 280 | 281 | #define ENSURE(condition, fmt, ...) \ 282 | if (!(condition)) { \ 283 | if (groups) pcre2_match_data_free(groups); \ 284 | if (regex) pcre2_code_free(regex); \ 285 | editor_status_err(editor, fmt, ##__VA_ARGS__); \ 286 | return; \ 287 | } 288 | 289 | #define INVALID(condition) ENSURE(!(condition), "Invalid argument: %s", arg) 290 | 291 | INVALID(rc < 0); 292 | 293 | char opt[32]; 294 | PCRE2_SIZE optlen = sizeof(opt); 295 | pcre2_substring_copy_bynumber(groups, 2, (unsigned char*)opt, &optlen); 296 | 297 | struct opt *info = option_info(opt); 298 | ENSURE(info, "Unknown option: %s", opt); 299 | 300 | PCRE2_SIZE *offsets = pcre2_get_ovector_pointer(groups); 301 | PCRE2_SIZE rhs_offset = offsets[6]; 302 | bool negate = offsets[3] != PCRE2_UNSET; 303 | pcre2_match_data_free(groups); 304 | groups = NULL; 305 | pcre2_code_free(regex); 306 | regex = NULL; 307 | 308 | INVALID(negate && info->type != OPTION_TYPE_bool); 309 | 310 | enum { 311 | OPTION_ACTION_NONE, 312 | OPTION_ACTION_SHOW, 313 | OPTION_ACTION_ENABLE, 314 | OPTION_ACTION_DISABLE, 315 | OPTION_ACTION_TOGGLE, 316 | OPTION_ACTION_RESET, 317 | OPTION_ACTION_ASSIGN, 318 | OPTION_ACTION_ASSIGN_ADD, 319 | } action = OPTION_ACTION_NONE; 320 | char *rhs = NULL; 321 | 322 | if (rhs_offset == PCRE2_UNSET) { 323 | switch (info->type) { 324 | case OPTION_TYPE_int: 325 | case OPTION_TYPE_string: 326 | action = OPTION_ACTION_SHOW; 327 | break; 328 | case OPTION_TYPE_bool: 329 | if (!negate) { 330 | action = OPTION_ACTION_ENABLE; 331 | } else { 332 | action = OPTION_ACTION_DISABLE; 333 | } 334 | } 335 | } else { 336 | switch (arg[rhs_offset]) { 337 | case '!': 338 | INVALID(info->type != OPTION_TYPE_bool); 339 | action = OPTION_ACTION_TOGGLE; 340 | break; 341 | case '?': action = OPTION_ACTION_SHOW; break; 342 | case '&': action = OPTION_ACTION_RESET; break; 343 | case '+': 344 | INVALID(info->type == OPTION_TYPE_bool); 345 | action = OPTION_ACTION_ASSIGN_ADD; 346 | rhs = arg + rhs_offset + 2; 347 | break; 348 | case '=': 349 | INVALID(info->type == OPTION_TYPE_bool); 350 | action = OPTION_ACTION_ASSIGN; 351 | rhs = arg + rhs_offset + 1; 352 | break; 353 | } 354 | } 355 | 356 | void *global, *local; 357 | editor_opt_vals(editor, info, &global, &local); 358 | assert(global == local || info->scope == OPTION_SCOPE_BUFFER); 359 | 360 | void *writes[2 + 1] = {0}; 361 | if (info->scope == OPTION_SCOPE_BUFFER && which == OPTION_SET_BOTH) { 362 | writes[0] = global; 363 | writes[1] = local; 364 | } else { 365 | writes[0] = which == OPTION_SET_GLOBAL ? global : local; 366 | } 367 | #define WRITE(val) for (void **_ = writes, *val = *_; val; ++_, val = *_) 368 | 369 | switch (action) { 370 | case OPTION_ACTION_NONE: assert(0); break; 371 | case OPTION_ACTION_SHOW: { 372 | void *val = which == OPTION_SET_GLOBAL ? global : local; 373 | editor_status_msg(editor, "%s", option_show(info, val)); 374 | break; 375 | } 376 | case OPTION_ACTION_ENABLE: 377 | assert(info->type == OPTION_TYPE_bool); 378 | WRITE(val) { *(bool*)val = true; } break; 379 | case OPTION_ACTION_DISABLE: 380 | assert(info->type == OPTION_TYPE_bool); 381 | WRITE(val) { *(bool*)val = false; } break; 382 | case OPTION_ACTION_TOGGLE: 383 | assert(info->type == OPTION_TYPE_bool); 384 | WRITE(val) { *(bool*)val = !*(bool*)val; } break; 385 | case OPTION_ACTION_RESET: 386 | switch (info->type) { 387 | case OPTION_TYPE_int: 388 | WRITE(val) { 389 | *(int*)val = info->defaultval.intval; 390 | } 391 | break; 392 | case OPTION_TYPE_bool: 393 | WRITE(val) { 394 | *(bool*)val = info->defaultval.boolval; 395 | } 396 | break; 397 | case OPTION_TYPE_string: 398 | WRITE(val) { 399 | free(*(string*)val); 400 | option_set_string((string*)val, info->defaultval.stringval); 401 | } 402 | break; 403 | } 404 | break; 405 | case OPTION_ACTION_ASSIGN: 406 | assert(rhs); 407 | switch (info->type) { 408 | case OPTION_TYPE_bool: assert(0); break; 409 | case OPTION_TYPE_int: 410 | WRITE(val) { 411 | ENSURE(strtoi(rhs, (int*)val), "Number required after =: %s", arg); 412 | } 413 | break; 414 | case OPTION_TYPE_string: 415 | WRITE(val) { 416 | free(*(string*)val); 417 | option_set_string((string*)val, rhs); 418 | } 419 | } 420 | break; 421 | case OPTION_ACTION_ASSIGN_ADD: 422 | assert(rhs); 423 | switch (info->type) { 424 | case OPTION_TYPE_bool: assert(0); break; 425 | case OPTION_TYPE_int: 426 | WRITE(val) { 427 | int increment; 428 | ENSURE(strtoi(rhs, &increment), "Number required after =: %s", arg); 429 | *(int*)val += increment; 430 | } 431 | break; 432 | case OPTION_TYPE_string: 433 | WRITE(val) { 434 | if (csl_has(*(string*)val, rhs)) { 435 | continue; 436 | } 437 | csl_append((string*)val, rhs); 438 | } 439 | break; 440 | } 441 | } 442 | } 443 | 444 | EDITOR_COMMAND_WITH_COMPLETION(setglobal, setg, COMPLETION_OPTIONS) { 445 | editor_command_set_impl(editor, arg, OPTION_SET_GLOBAL); 446 | } 447 | 448 | EDITOR_COMMAND_WITH_COMPLETION(setlocal, setl, COMPLETION_OPTIONS) { 449 | editor_command_set_impl(editor, arg, OPTION_SET_LOCAL); 450 | } 451 | 452 | EDITOR_COMMAND_WITH_COMPLETION(set, set, COMPLETION_OPTIONS) { 453 | editor_command_set_impl(editor, arg, OPTION_SET_BOTH); 454 | } 455 | -------------------------------------------------------------------------------- /motion.c: -------------------------------------------------------------------------------- 1 | #include "motion.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "buf.h" 10 | #include "buffer.h" 11 | #include "editor.h" 12 | #include "gap.h" 13 | #include "search.h" 14 | #include "window.h" 15 | #include "util.h" 16 | 17 | static bool is_line_start(struct gapbuf *gb, size_t pos) { 18 | return pos == 0 || gb_getchar(gb, pos - 1) == '\n'; 19 | } 20 | 21 | static bool is_line_end(struct gapbuf *gb, size_t pos) { 22 | return pos == gb_size(gb) - 1 || gb_getchar(gb, pos) == '\n'; 23 | } 24 | 25 | static bool is_blank_line(struct gapbuf *gb, size_t pos) { 26 | return gb_getchar(gb, pos) == '\n' && is_line_start(gb, pos); 27 | } 28 | 29 | static size_t left(struct motion_context ctx) { 30 | struct gapbuf *gb = ctx.window->buffer->text; 31 | return is_line_start(gb, ctx.pos) ? ctx.pos : gb_utf8prev(gb, ctx.pos); 32 | } 33 | 34 | static size_t right(struct motion_context ctx) { 35 | struct gapbuf *gb = ctx.window->buffer->text; 36 | return is_line_end(gb, ctx.pos) ? ctx.pos : gb_utf8next(gb, ctx.pos); 37 | } 38 | 39 | static size_t goto_line(struct motion_context ctx) { 40 | struct gapbuf *gb = ctx.editor->window->buffer->text; 41 | int line = gb_nlines(gb) - 1; 42 | if (ctx.editor->count) { 43 | line = min((int)ctx.editor->count - 1, line); 44 | } 45 | return gb_linecol_to_pos(gb, line, 0); 46 | } 47 | 48 | static size_t up(struct motion_context ctx) { 49 | struct gapbuf *gb = ctx.window->buffer->text; 50 | size_t x, y; 51 | gb_pos_to_linecol(gb, ctx.pos, &y, &x); 52 | int dy = ctx.editor->count ? ctx.editor->count : 1; 53 | int line = max((int)y - dy, 0); 54 | return gb_linecol_to_pos(gb, line, min(x, gb->lines->buf[line])); 55 | } 56 | 57 | static size_t down(struct motion_context ctx) { 58 | struct gapbuf *gb = ctx.window->buffer->text; 59 | int nlines = gb_nlines(gb); 60 | size_t x, y; 61 | gb_pos_to_linecol(gb, ctx.pos, &y, &x); 62 | int dy = ctx.editor->count ? ctx.editor->count : 1; 63 | int line = min((int)y + dy, nlines - 1); 64 | return gb_linecol_to_pos(gb, line, min(x, gb->lines->buf[line])); 65 | } 66 | 67 | static size_t line_start(struct motion_context ctx) { 68 | struct gapbuf *gb = ctx.window->buffer->text; 69 | if (is_line_start(gb, ctx.pos)) { 70 | return ctx.pos; 71 | } 72 | return (size_t) (gb_lastindexof(gb, '\n', ctx.pos - 1) + 1); 73 | } 74 | 75 | static size_t line_end(struct motion_context ctx) { 76 | struct gapbuf *gb = ctx.window->buffer->text; 77 | if (is_line_end(gb, ctx.pos)) { 78 | return ctx.pos; 79 | } 80 | return gb_indexof(gb, '\n', ctx.pos); 81 | } 82 | 83 | static size_t buffer_top(struct motion_context ctx ATTR_UNUSED) { 84 | return 0; 85 | } 86 | 87 | static size_t first_non_blank(struct motion_context ctx) { 88 | size_t start = line_start(ctx); 89 | size_t end = line_end(ctx); 90 | struct gapbuf *gb = ctx.window->buffer->text; 91 | while (start < end && isspace(gb_getchar(gb, start))) { 92 | start++; 93 | } 94 | return start; 95 | } 96 | 97 | static size_t last_non_blank(struct motion_context ctx) { 98 | size_t start = line_start(ctx); 99 | size_t end = line_end(ctx); 100 | struct gapbuf *gb = ctx.window->buffer->text; 101 | while (end > start && isspace(gb_getchar(gb, end))) { 102 | end--; 103 | } 104 | return end; 105 | } 106 | 107 | static bool is_word_char(char c) { 108 | // FIXME(ibadawi): Need a unicode-aware version of isalnum(). 109 | // As a quick hack to make word motions work, treat non-ascii as letters. 110 | return isalnum(c) || c == '_' || tb_utf8_char_length(c) > 1; 111 | } 112 | 113 | static bool is_word_start(struct motion_context ctx) { 114 | struct gapbuf *gb = ctx.window->buffer->text; 115 | if (is_line_start(gb, ctx.pos)) { 116 | return true; 117 | } 118 | char this = gb_getchar(gb, ctx.pos); 119 | char last = gb_getchar(gb, gb_utf8prev(gb, ctx.pos)); 120 | return !isspace(this) && (is_word_char(this) + is_word_char(last) == 1); 121 | } 122 | 123 | static bool is_WORD_start(struct motion_context ctx) { 124 | if (ctx.pos == 0) { 125 | return true; 126 | } 127 | struct gapbuf *gb = ctx.window->buffer->text; 128 | char this = gb_getchar(gb, ctx.pos); 129 | char last = gb_getchar(gb, gb_utf8prev(gb, ctx.pos)); 130 | return !isspace(this) && isspace(last); 131 | } 132 | 133 | static bool is_word_end(struct motion_context ctx) { 134 | struct gapbuf *gb = ctx.window->buffer->text; 135 | if (ctx.pos == gb_size(gb) - 1 || is_blank_line(gb, ctx.pos)) { 136 | return true; 137 | } 138 | char this = gb_getchar(gb, ctx.pos); 139 | char next = gb_getchar(gb, gb_utf8next(gb, ctx.pos)); 140 | return !isspace(this) && (is_word_char(this) + is_word_char(next) == 1); 141 | } 142 | 143 | static bool is_WORD_end(struct motion_context ctx) { 144 | struct gapbuf *gb = ctx.window->buffer->text; 145 | if (ctx.pos == gb_size(gb)) { 146 | return true; 147 | } 148 | char this = gb_getchar(gb, ctx.pos); 149 | char next = gb_getchar(gb, gb_utf8next(gb, ctx.pos)); 150 | return !isspace(this) && isspace(next); 151 | } 152 | 153 | static size_t prev_until(struct motion_context ctx, bool (*pred)(struct motion_context)) { 154 | struct gapbuf *gb = ctx.window->buffer->text; 155 | if (ctx.pos > 0) { 156 | do { 157 | ctx.pos = gb_utf8prev(gb, ctx.pos); 158 | } while (!pred(ctx) && ctx.pos > 0); 159 | } 160 | return ctx.pos; 161 | } 162 | 163 | static size_t next_until(struct motion_context ctx, bool (*pred)(struct motion_context)) { 164 | struct gapbuf *gb = ctx.window->buffer->text; 165 | size_t size = gb_size(ctx.window->buffer->text); 166 | if (ctx.pos < size - 1) { 167 | do { 168 | ctx.pos = gb_utf8next(gb, ctx.pos); 169 | } while (!pred(ctx) && ctx.pos < size - 1); 170 | } 171 | return ctx.pos; 172 | } 173 | 174 | static size_t next_word_start(struct motion_context ctx) { 175 | return next_until(ctx, is_word_start); 176 | } 177 | 178 | static size_t next_WORD_start(struct motion_context ctx) { 179 | return next_until(ctx, is_WORD_start); 180 | } 181 | 182 | static size_t prev_word_start(struct motion_context ctx) { 183 | return prev_until(ctx, is_word_start); 184 | } 185 | 186 | static size_t prev_WORD_start(struct motion_context ctx) { 187 | return prev_until(ctx, is_WORD_start); 188 | } 189 | 190 | static size_t next_word_end(struct motion_context ctx) { 191 | return next_until(ctx, is_word_end); 192 | } 193 | 194 | static size_t next_WORD_end(struct motion_context ctx) { 195 | return next_until(ctx, is_WORD_end); 196 | } 197 | 198 | static size_t prev_word_end(struct motion_context ctx) { 199 | return prev_until(ctx, is_word_end); 200 | } 201 | 202 | static size_t prev_WORD_end(struct motion_context ctx) { 203 | return prev_until(ctx, is_WORD_end); 204 | } 205 | 206 | static bool is_paragraph_start(struct motion_context ctx) { 207 | if (ctx.pos == 0) { 208 | return true; 209 | } 210 | struct gapbuf *gb = ctx.window->buffer->text; 211 | return is_blank_line(gb, ctx.pos) && !is_blank_line(gb, ctx.pos + 1); 212 | } 213 | 214 | static bool is_paragraph_end(struct motion_context ctx) { 215 | struct gapbuf *gb = ctx.window->buffer->text; 216 | if (ctx.pos == gb_size(gb) - 1) { 217 | return true; 218 | } 219 | return is_blank_line(gb, ctx.pos) && !is_blank_line(gb, ctx.pos - 1); 220 | } 221 | 222 | static size_t paragraph_start(struct motion_context ctx) { 223 | return prev_until(ctx, is_paragraph_start); 224 | } 225 | 226 | static size_t paragraph_end(struct motion_context ctx) { 227 | return next_until(ctx, is_paragraph_end); 228 | } 229 | 230 | static size_t till_forward(struct motion_context ctx, bool inclusive) { 231 | struct gapbuf *gb = ctx.window->buffer->text; 232 | if (is_line_end(gb, ctx.pos)) { 233 | return ctx.pos; 234 | } 235 | 236 | char arg = editor_getchar(ctx.editor); 237 | size_t newline = gb_indexof(gb, '\n', ctx.pos + 1); 238 | size_t target = gb_indexof(gb, arg, ctx.pos + 1); 239 | if (target < newline) { 240 | return target - (inclusive ? 0 : 1); 241 | } 242 | return ctx.pos; 243 | } 244 | 245 | static size_t till_backward(struct motion_context ctx, bool inclusive) { 246 | struct gapbuf *gb = ctx.window->buffer->text; 247 | if (is_line_start(gb, ctx.pos)) { 248 | return ctx.pos; 249 | } 250 | 251 | char arg = editor_getchar(ctx.editor); 252 | ssize_t newline = gb_lastindexof(gb, '\n', ctx.pos - 1); 253 | ssize_t target = gb_lastindexof(gb, arg, ctx.pos - 1); 254 | if (target > newline) { 255 | return (size_t) target + (inclusive ? 0 : 1); 256 | } 257 | return ctx.pos; 258 | } 259 | 260 | static size_t till_forward_inclusive(struct motion_context ctx) { 261 | return till_forward(ctx, true); 262 | } 263 | 264 | static size_t till_forward_exclusive(struct motion_context ctx) { 265 | return till_forward(ctx, false); 266 | } 267 | 268 | static size_t till_backward_inclusive(struct motion_context ctx) { 269 | return till_backward(ctx, true); 270 | } 271 | 272 | static size_t till_backward_exclusive(struct motion_context ctx) { 273 | return till_backward(ctx, false); 274 | } 275 | 276 | static size_t search_motion(struct motion_context ctx, char direction) { 277 | editor_push_cmdline_mode(ctx.editor, direction); 278 | struct editing_mode *mode = ctx.editor->mode; 279 | editor_draw(ctx.editor); 280 | while (ctx.editor->mode == mode) { 281 | struct tb_event event; 282 | editor_waitkey(ctx.editor, &event); 283 | editor_handle_key_press(ctx.editor, &event); 284 | editor_draw(ctx.editor); 285 | } 286 | size_t result = window_cursor(ctx.editor->window); 287 | window_set_cursor(ctx.editor->window, ctx.pos); 288 | return result; 289 | } 290 | 291 | static size_t forward_search(struct motion_context ctx) { 292 | return search_motion(ctx, '/'); 293 | } 294 | 295 | static size_t backward_search(struct motion_context ctx) { 296 | return search_motion(ctx, '?'); 297 | } 298 | 299 | static size_t search_cycle(struct motion_context ctx, 300 | enum search_direction direction) { 301 | struct region match; 302 | if (editor_search(ctx.editor, NULL, ctx.pos, direction, &match)) { 303 | return match.start; 304 | } 305 | return ctx.pos; 306 | } 307 | 308 | static size_t search_next(struct motion_context ctx) { 309 | return search_cycle(ctx, SEARCH_FORWARDS); 310 | } 311 | 312 | static size_t search_prev(struct motion_context ctx) { 313 | return search_cycle(ctx, SEARCH_BACKWARDS); 314 | } 315 | 316 | static size_t word_under_cursor(struct motion_context ctx, size_t start, 317 | enum search_direction direction) { 318 | struct buf *word = motion_word_under_cursor(ctx.editor->window); 319 | struct buf *pattern = buf_create(1); 320 | buf_printf(pattern, "[[:<:]]%s[[:>:]]", word->buf); 321 | history_add_item(&ctx.editor->search_history, pattern->buf); 322 | struct region match; 323 | bool found = editor_search(ctx.editor, pattern->buf, start, direction, &match); 324 | buf_free(word); 325 | buf_free(pattern); 326 | return found ? match.start : ctx.pos; 327 | } 328 | 329 | static size_t word_under_cursor_next(struct motion_context ctx) { 330 | return word_under_cursor(ctx, ctx.pos, SEARCH_FORWARDS); 331 | } 332 | 333 | static size_t word_under_cursor_prev(struct motion_context ctx) { 334 | size_t start = is_word_start(ctx) ? ctx.pos : prev_word_start(ctx); 335 | return word_under_cursor(ctx, start, SEARCH_BACKWARDS); 336 | } 337 | 338 | static size_t matching_paren(struct motion_context ctx) { 339 | struct gapbuf *gb = ctx.window->buffer->text; 340 | char a = '\0'; 341 | size_t start; 342 | for (start = ctx.pos; !is_line_end(gb, start); ++start) { 343 | a = gb_getchar(gb, start); 344 | if (strchr("()[]{}", a)) { 345 | break; 346 | } 347 | } 348 | if (!strchr("()[]{}", a)) { 349 | return ctx.pos; 350 | } 351 | 352 | char b = *(strchr("()([][{}{", a) + 1); 353 | bool forwards = strchr("([{", a) != NULL; 354 | 355 | int nested = -1; 356 | size_t end; 357 | for (end = start; end < gb_size(gb) - 1; forwards ? end++ : end--) { 358 | char c = gb_getchar(gb, end); 359 | if (c == a) { 360 | nested++; 361 | } else if (c == b) { 362 | if (!nested) { 363 | break; 364 | } 365 | nested--; 366 | } 367 | } 368 | 369 | return gb_getchar(gb, end) == b ? end : ctx.pos; 370 | } 371 | 372 | #define LINEWISE true, false 373 | #define EXCLUSIVE false, true 374 | #define INCLUSIVE false, false 375 | #define REPEAT true 376 | #define DIRECT false 377 | 378 | static struct motion motion_table[] = { 379 | {'h', left, EXCLUSIVE, REPEAT}, 380 | {'j', down, LINEWISE, DIRECT}, 381 | {'k', up, LINEWISE, DIRECT}, 382 | {'l', right, EXCLUSIVE, REPEAT}, 383 | {'0', line_start, EXCLUSIVE, REPEAT}, 384 | {'$', line_end, INCLUSIVE, REPEAT}, 385 | {'^', first_non_blank, EXCLUSIVE, REPEAT}, 386 | {'{', paragraph_start, EXCLUSIVE, REPEAT}, 387 | {'}', paragraph_end, EXCLUSIVE, REPEAT}, 388 | {'b', prev_word_start, EXCLUSIVE, REPEAT}, 389 | {'B', prev_WORD_start, EXCLUSIVE, REPEAT}, 390 | {'w', next_word_start, EXCLUSIVE, REPEAT}, 391 | {'W', next_WORD_start, EXCLUSIVE, REPEAT}, 392 | {'e', next_word_end, INCLUSIVE, REPEAT}, 393 | {'E', next_WORD_end, INCLUSIVE, REPEAT}, 394 | {'G', goto_line, LINEWISE, DIRECT}, 395 | {'t', till_forward_exclusive, INCLUSIVE, REPEAT}, 396 | {'f', till_forward_inclusive, INCLUSIVE, REPEAT}, 397 | {'T', till_backward_exclusive, INCLUSIVE, REPEAT}, 398 | {'F', till_backward_inclusive, INCLUSIVE, REPEAT}, 399 | {'/', forward_search, EXCLUSIVE, REPEAT}, 400 | {'?', backward_search, EXCLUSIVE, REPEAT}, 401 | {'n', search_next, EXCLUSIVE, REPEAT}, 402 | {'N', search_prev, EXCLUSIVE, REPEAT}, 403 | {'*', word_under_cursor_next, EXCLUSIVE, REPEAT}, 404 | {'#', word_under_cursor_prev, EXCLUSIVE, REPEAT}, 405 | {'%', matching_paren, INCLUSIVE, REPEAT}, 406 | {-1, NULL, false, false, false} 407 | }; 408 | 409 | static struct motion g_motion_table[] = { 410 | {'_', last_non_blank, INCLUSIVE, REPEAT}, 411 | {'e', prev_word_end, INCLUSIVE, REPEAT}, 412 | {'E', prev_WORD_end, INCLUSIVE, REPEAT}, 413 | {'g', buffer_top, LINEWISE, REPEAT}, 414 | {-1, NULL, false, false, false} 415 | }; 416 | 417 | static struct motion *motion_find(struct motion *table, char name) { 418 | for (int i = 0; table[i].name != -1; ++i) { 419 | if (table[i].name == name) { 420 | return &table[i]; 421 | } 422 | } 423 | return NULL; 424 | } 425 | 426 | struct motion *motion_get(struct editor *editor, struct tb_event *ev) { 427 | struct motion *table = motion_table; 428 | if (ev->ch == 'g') { 429 | table = g_motion_table; 430 | editor_waitkey(editor, ev); 431 | } 432 | return motion_find(table, (char) ev->ch); 433 | } 434 | 435 | size_t motion_apply(struct motion *motion, struct editor *editor) { 436 | size_t cursor = window_cursor(editor->window); 437 | struct motion_context ctx = {cursor, editor->window, editor}; 438 | 439 | if (!motion->repeat) { 440 | size_t pos = motion->op(ctx); 441 | editor->count = 0; 442 | return pos; 443 | } 444 | 445 | unsigned int n = editor->count ? editor->count : 1; 446 | for (unsigned int i = 0; i < n; ++i) { 447 | ctx.pos = motion->op(ctx); 448 | } 449 | 450 | editor->count = 0; 451 | return ctx.pos; 452 | } 453 | 454 | struct buf *motion_word_under_cursor(struct window *window) { 455 | struct motion_context ctx = {window_cursor(window), window, NULL}; 456 | size_t start = is_word_start(ctx) ? ctx.pos : prev_word_start(ctx); 457 | size_t end = is_word_end(ctx) ? ctx.pos : next_word_end(ctx); 458 | return gb_getstring(window->buffer->text, start, end - start + 1); 459 | } 460 | 461 | struct buf *motion_word_before_cursor(struct window *window) { 462 | size_t cursor = window_cursor(window); 463 | struct motion_context ctx = {cursor, window, NULL}; 464 | size_t start = is_word_start(ctx) ? ctx.pos : prev_word_start(ctx); 465 | size_t end = gb_utf8prev(window->buffer->text, cursor); 466 | return gb_getstring(window->buffer->text, start, end - start + 1); 467 | } 468 | 469 | static bool isfname(char c) { 470 | return isalnum(c) || strchr("_-./", c) != NULL; 471 | } 472 | 473 | static bool is_fname_start(struct motion_context ctx) { 474 | struct gapbuf *gb = ctx.window->buffer->text; 475 | if (is_line_start(gb, ctx.pos)) { 476 | return true; 477 | } 478 | 479 | char this = gb_getchar(gb, ctx.pos); 480 | char last = gb_getchar(gb, ctx.pos - 1); 481 | return isfname(this) && !isfname(last); 482 | } 483 | 484 | static bool is_fname_end(struct motion_context ctx) { 485 | struct gapbuf *gb = ctx.window->buffer->text; 486 | if (ctx.pos == gb_size(gb) - 1 || is_blank_line(gb, ctx.pos)) { 487 | return true; 488 | } 489 | 490 | char this = gb_getchar(gb, ctx.pos); 491 | char next = gb_getchar(gb, ctx.pos + 1); 492 | return isfname(this) && !isfname(next); 493 | } 494 | 495 | struct buf *motion_filename_under_cursor(struct window *window) { 496 | struct motion_context ctx = {window_cursor(window), window, NULL}; 497 | size_t start = is_fname_start(ctx) ? ctx.pos : prev_until(ctx, is_fname_start); 498 | size_t end = is_fname_end(ctx) ? ctx.pos : next_until(ctx, is_fname_end); 499 | return gb_getstring(window->buffer->text, start, end - start + 1); 500 | } 501 | -------------------------------------------------------------------------------- /window.c: -------------------------------------------------------------------------------- 1 | #include "window.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "buffer.h" 9 | #include "editor.h" 10 | #include "gap.h" 11 | #include "options.h" 12 | #include "tags.h" 13 | #include "util.h" 14 | 15 | struct window *window_create(struct buffer *buffer, size_t w, size_t h) { 16 | struct window *window = xmalloc(sizeof(*window)); 17 | 18 | window->split_type = WINDOW_LEAF; 19 | window->parent = NULL; 20 | window->pwd = NULL; 21 | 22 | window->buffer = NULL; 23 | window->alternate_path = NULL; 24 | window->cursor = xmalloc(sizeof(*window->cursor)); 25 | if (buffer) { 26 | window_set_buffer(window, buffer); 27 | } 28 | window->visual_mode_selection = NULL; 29 | 30 | window->w = w; 31 | window->h = h; 32 | 33 | TAILQ_INIT(&window->tag_stack); 34 | window->tag = NULL; 35 | 36 | window_init_options(window); 37 | 38 | return window; 39 | } 40 | 41 | static struct window *window_sibling(struct window *window) { 42 | assert(window->parent); 43 | if (window == window->parent->split.first) { 44 | return window->parent->split.second; 45 | } 46 | assert(window == window->parent->split.second); 47 | return window->parent->split.first; 48 | } 49 | 50 | size_t window_w(struct window *window) { 51 | if (!window->parent) { 52 | return window->w; 53 | } 54 | 55 | if (window->parent->split_type == WINDOW_SPLIT_HORIZONTAL) { 56 | return window_w(window->parent); 57 | } 58 | 59 | if (window == window->parent->split.first) { 60 | return window->parent->split.point; 61 | } 62 | return window_w(window->parent) - window->parent->split.point; 63 | } 64 | 65 | size_t window_h(struct window *window) { 66 | if (!window->parent) { 67 | return window->h; 68 | } 69 | 70 | if (window->parent->split_type == WINDOW_SPLIT_VERTICAL) { 71 | return window_h(window->parent); 72 | } 73 | 74 | if (window == window->parent->split.first) { 75 | return window->parent->split.point; 76 | } 77 | return window_h(window->parent) - window->parent->split.point; 78 | } 79 | 80 | size_t window_x(struct window *window) { 81 | if (!window->parent) { 82 | return 0; 83 | } 84 | 85 | switch (window->parent->split_type) { 86 | case WINDOW_SPLIT_HORIZONTAL: 87 | return window_x(window->parent); 88 | case WINDOW_SPLIT_VERTICAL: 89 | if (window == window->parent->split.first) { 90 | return window_x(window->parent); 91 | } 92 | return window_x(window->parent) + window_w(window_sibling(window)); 93 | case WINDOW_LEAF: 94 | assert(0); 95 | } 96 | 97 | return 0; 98 | } 99 | 100 | size_t window_y(struct window *window) { 101 | if (!window->parent) { 102 | return 0; 103 | } 104 | 105 | switch (window->parent->split_type) { 106 | case WINDOW_SPLIT_VERTICAL: 107 | return window_y(window->parent); 108 | case WINDOW_SPLIT_HORIZONTAL: 109 | if (window == window->parent->split.first) { 110 | return window_y(window->parent); 111 | } 112 | return window_y(window->parent) + window_h(window_sibling(window)); 113 | case WINDOW_LEAF: 114 | assert(0); 115 | } 116 | 117 | return 0; 118 | } 119 | 120 | static size_t window_count_splits(struct window *window, 121 | enum window_split_type type) { 122 | if (window->split_type == WINDOW_LEAF) { 123 | return 0; 124 | } 125 | 126 | size_t lhs = window_count_splits(window->split.first, type); 127 | size_t rhs = window_count_splits(window->split.second, type); 128 | 129 | if (window->split_type == type) { 130 | return 1 + lhs + rhs; 131 | } 132 | return max(lhs, rhs); 133 | } 134 | 135 | static size_t window_count_leaves(struct window *window, 136 | enum window_split_type type) { 137 | 138 | if (window->split_type == WINDOW_LEAF) { 139 | return window->parent->split_type == type ? 1 : 0; 140 | } 141 | 142 | size_t lhs = window_count_leaves(window->split.first, type); 143 | size_t rhs = window_count_leaves(window->split.second, type); 144 | 145 | if (window->split_type == type) { 146 | return lhs + rhs; 147 | } 148 | return max(lhs, rhs); 149 | } 150 | 151 | static void window_set_split_size(struct window *window, 152 | enum window_split_type type, 153 | size_t size) { 154 | if (window->split_type == WINDOW_LEAF) { 155 | return; 156 | } 157 | 158 | if (window->split_type == type) { 159 | window->split.point = 160 | size * max(1, window_count_leaves(window->split.first, type)); 161 | } 162 | window_set_split_size(window->split.first, type, size); 163 | window_set_split_size(window->split.second, type, size); 164 | } 165 | 166 | void window_equalize(struct window *window, 167 | enum window_split_type type) { 168 | if (window->split_type == WINDOW_LEAF) { 169 | window = window->parent; 170 | } 171 | if (!window) { 172 | return; 173 | } 174 | 175 | struct window *root = window_root(window); 176 | 177 | size_t n = window_count_splits(root, type); 178 | size_t size; 179 | if (type == WINDOW_SPLIT_VERTICAL) { 180 | size = root->w; 181 | } else { 182 | size = root->h; 183 | } 184 | 185 | window_set_split_size(root, type, size / (n + 1)); 186 | } 187 | 188 | struct window *window_split(struct window *window, 189 | enum window_split_direction direction) { 190 | assert(window->split_type == WINDOW_LEAF); 191 | 192 | struct window *copy = xmalloc(sizeof(*window)); 193 | memcpy(copy, window, sizeof(*window)); 194 | 195 | struct window *sibling = window_create( 196 | window->buffer, window->w, window->h); 197 | 198 | copy->parent = window; 199 | window_inherit_parent_options(copy); 200 | 201 | sibling->parent = window; 202 | window_inherit_parent_options(sibling); 203 | 204 | if (window->pwd) { 205 | copy->pwd = window->pwd; 206 | sibling->pwd = xstrdup(window->pwd); 207 | window->pwd = NULL; 208 | } 209 | 210 | switch (direction) { 211 | case WINDOW_SPLIT_LEFT: 212 | window->split_type = WINDOW_SPLIT_VERTICAL; 213 | window->split.point = window_w(window) / 2; 214 | window->split.first = sibling; 215 | window->split.second = copy; 216 | break; 217 | case WINDOW_SPLIT_RIGHT: 218 | window->split_type = WINDOW_SPLIT_VERTICAL; 219 | window->split.point = window_w(window) / 2; 220 | window->split.first = copy; 221 | window->split.second = sibling; 222 | break; 223 | case WINDOW_SPLIT_ABOVE: 224 | window->split_type = WINDOW_SPLIT_HORIZONTAL; 225 | window->split.point = window_h(window) / 2; 226 | window->split.first = sibling; 227 | window->split.second = copy; 228 | break; 229 | case WINDOW_SPLIT_BELOW: 230 | window->split_type = WINDOW_SPLIT_HORIZONTAL; 231 | window->split.point = window_h(window) / 2; 232 | window->split.first = copy; 233 | window->split.second = sibling; 234 | } 235 | 236 | return sibling; 237 | } 238 | 239 | // FIXME(ibadawi): Enforce minimum size for a window 240 | void window_resize(struct window *window, int dw, int dh) { 241 | if (!window->parent) { 242 | return; 243 | } 244 | assert((dw && !dh) || (!dw && dh)); 245 | 246 | size_t *point = &window->parent->split.point; 247 | 248 | if (dw) { 249 | if (window->parent->split_type == WINDOW_SPLIT_VERTICAL) { 250 | if (window == window->parent->split.first) { 251 | *point = (size_t)((int)*point + dw); 252 | } else { 253 | assert(window == window->parent->split.second); 254 | *point = (size_t)((int)*point - dw); 255 | } 256 | } else { 257 | assert(window->parent->split_type == WINDOW_SPLIT_HORIZONTAL); 258 | struct window *parent = window->parent; 259 | while (parent && parent->split_type != WINDOW_SPLIT_VERTICAL) { 260 | window = parent; 261 | parent = parent->parent; 262 | } 263 | if (parent) { 264 | assert(parent->split_type == WINDOW_SPLIT_VERTICAL); 265 | window_resize(window, dw, 0); 266 | } 267 | } 268 | } 269 | 270 | if (dh) { 271 | if (window->parent->split_type == WINDOW_SPLIT_HORIZONTAL) { 272 | if (window == window->parent->split.first) { 273 | *point = (size_t)((int)*point + dh); 274 | } else { 275 | assert(window == window->parent->split.second); 276 | *point = (size_t)((int)*point - dh); 277 | } 278 | } else { 279 | assert(window->parent->split_type == WINDOW_SPLIT_VERTICAL); 280 | struct window *parent = window->parent; 281 | while (parent && parent->split_type != WINDOW_SPLIT_HORIZONTAL) { 282 | window = parent; 283 | parent = parent->parent; 284 | } 285 | if (parent) { 286 | assert(parent->split_type == WINDOW_SPLIT_HORIZONTAL); 287 | window_resize(window, 0, dh); 288 | } 289 | } 290 | } 291 | } 292 | 293 | void window_set_buffer(struct window *window, struct buffer* buffer) { 294 | if (window->buffer) { 295 | TAILQ_REMOVE(&window->buffer->marks, window->cursor, pointers); 296 | 297 | if (window->buffer->path) { 298 | free(window->alternate_path); 299 | window->alternate_path = xstrdup(window->buffer->path); 300 | } 301 | } 302 | 303 | window->buffer = buffer; 304 | window->top = 0; 305 | window->left = 0; 306 | region_set(&window->cursor->region, 0, 1); 307 | window->have_incsearch_match = false; 308 | TAILQ_INSERT_TAIL(&buffer->marks, window->cursor, pointers); 309 | } 310 | 311 | size_t window_cursor(struct window *window) { 312 | return window->cursor->region.start; 313 | } 314 | 315 | struct window *window_first_leaf(struct window *window) { 316 | while (window->split_type != WINDOW_LEAF) { 317 | window = window->split.first; 318 | } 319 | return window; 320 | } 321 | 322 | static struct window *window_last_leaf(struct window *window) { 323 | while (window->split_type != WINDOW_LEAF) { 324 | window = window->split.second; 325 | } 326 | return window; 327 | } 328 | 329 | struct window *window_root(struct window *window) { 330 | while (window->parent) { 331 | window = window->parent; 332 | } 333 | return window; 334 | } 335 | 336 | struct window *window_left(struct window *window) { 337 | if (!window || !window->parent) { 338 | return NULL; 339 | } 340 | 341 | switch (window->parent->split_type) { 342 | case WINDOW_SPLIT_HORIZONTAL: 343 | return window_left(window->parent); 344 | case WINDOW_SPLIT_VERTICAL: 345 | if (window == window->parent->split.second) { 346 | return window_last_leaf(window_sibling(window)); 347 | } 348 | return window_left(window->parent); 349 | case WINDOW_LEAF: 350 | assert(0); 351 | } 352 | 353 | return NULL; 354 | } 355 | 356 | struct window *window_right(struct window *window) { 357 | if (!window || !window->parent) { 358 | return NULL; 359 | } 360 | 361 | switch (window->parent->split_type) { 362 | case WINDOW_SPLIT_HORIZONTAL: 363 | return window_right(window->parent); 364 | case WINDOW_SPLIT_VERTICAL: 365 | if (window == window->parent->split.first) { 366 | return window_first_leaf(window_sibling(window)); 367 | } 368 | return window_right(window->parent); 369 | case WINDOW_LEAF: 370 | assert(0); 371 | } 372 | 373 | return NULL; 374 | } 375 | 376 | struct window *window_up(struct window *window) { 377 | if (!window || !window->parent) { 378 | return NULL; 379 | } 380 | 381 | switch (window->parent->split_type) { 382 | case WINDOW_SPLIT_VERTICAL: 383 | return window_up(window->parent); 384 | case WINDOW_SPLIT_HORIZONTAL: 385 | if (window == window->parent->split.second) { 386 | return window_last_leaf(window_sibling(window)); 387 | } 388 | return window_up(window->parent); 389 | case WINDOW_LEAF: 390 | assert(0); 391 | } 392 | 393 | return NULL; 394 | } 395 | 396 | struct window *window_down(struct window *window) { 397 | if (!window || !window->parent) { 398 | return NULL; 399 | } 400 | 401 | switch (window->parent->split_type) { 402 | case WINDOW_SPLIT_VERTICAL: 403 | return window_down(window->parent); 404 | case WINDOW_SPLIT_HORIZONTAL: 405 | if (window == window->parent->split.first) { 406 | return window_first_leaf(window_sibling(window)); 407 | } 408 | return window_down(window->parent); 409 | case WINDOW_LEAF: 410 | assert(0); 411 | } 412 | 413 | return NULL; 414 | } 415 | 416 | void window_free(struct window *window) { 417 | if (window->split_type == WINDOW_LEAF) { 418 | struct tag_jump *j, *tj; 419 | TAILQ_FOREACH_SAFE(j, &window->tag_stack, pointers, tj) { 420 | free(j); 421 | } 422 | if (window->buffer) { 423 | TAILQ_REMOVE(&window->buffer->marks, window->cursor, pointers); 424 | free(window->cursor); 425 | } 426 | free(window->pwd); 427 | free(window->alternate_path); 428 | } else { 429 | window_free(window->split.first); 430 | window_free(window->split.second); 431 | } 432 | window_free_options(window); 433 | free(window); 434 | } 435 | 436 | struct window *window_close(struct window *window) { 437 | assert(window->split_type == WINDOW_LEAF); 438 | 439 | struct window *parent = window->parent; 440 | struct window *sibling = window_sibling(window); 441 | bool was_left_child = window == window->parent->split.first; 442 | 443 | enum window_split_type old_parent_type = parent->split_type; 444 | struct window *grandparent = parent->parent; 445 | size_t w = parent->w; 446 | size_t h = parent->h; 447 | 448 | memcpy(parent, sibling, sizeof(*sibling)); 449 | parent->parent = grandparent; 450 | parent->w = w; 451 | parent->h = h; 452 | 453 | if (parent->split_type != WINDOW_LEAF) { 454 | parent->split.first->parent = parent; 455 | parent->split.second->parent = parent; 456 | } 457 | 458 | if (was_left_child && old_parent_type == parent->split_type) { 459 | if (parent->split_type == WINDOW_SPLIT_HORIZONTAL) { 460 | parent->split.point = window_h(parent) - parent->split.point; 461 | } else if (parent->split_type == WINDOW_SPLIT_VERTICAL) { 462 | parent->split.point = window_w(parent) - parent->split.point; 463 | } 464 | } 465 | 466 | // free, not window_free, because that would free parent's fields. 467 | free(sibling); 468 | 469 | return window_first_leaf(parent); 470 | } 471 | 472 | void window_set_cursor(struct window *window, size_t pos) { 473 | region_set(&window->cursor->region, pos, pos + 1); 474 | } 475 | 476 | void window_center_cursor(struct window *window) { 477 | size_t cursor = window_cursor(window); 478 | size_t line, col; 479 | gb_pos_to_linecol(window->buffer->text, cursor, &line, &col); 480 | 481 | size_t h = window_h(window); 482 | if (line > h/2) { 483 | window->top = line - h/2; 484 | } 485 | } 486 | 487 | void window_page_up(struct window *window) { 488 | if (window->top == 0) { 489 | return; 490 | } 491 | 492 | window_set_cursor(window, gb_linecol_to_pos( 493 | window->buffer->text, window->top + 1, 0)); 494 | 495 | if (window->top < window_h(window)) { 496 | window->top = 0; 497 | } else { 498 | window->top -= window_h(window); 499 | } 500 | } 501 | 502 | void window_page_down(struct window *window) { 503 | size_t nlines = gb_nlines(window->buffer->text); 504 | if (window->top == nlines - 1) { 505 | return; 506 | } 507 | 508 | if (window->top + window_h(window) > nlines - 3) { 509 | window->top = nlines - 1; 510 | } else { 511 | window->top += window_h(window) - 2; 512 | } 513 | 514 | window_set_cursor(window, gb_linecol_to_pos( 515 | window->buffer->text, window->top, 0)); 516 | } 517 | 518 | EDITOR_COMMAND_WITH_COMPLETION(split, sp, COMPLETION_PATHS) { 519 | editor_set_window(editor, window_split(editor->window, 520 | editor->opt.splitbelow ? WINDOW_SPLIT_BELOW : WINDOW_SPLIT_ABOVE)); 521 | 522 | if (editor->opt.equalalways) { 523 | window_equalize(editor->window, WINDOW_SPLIT_HORIZONTAL); 524 | } 525 | 526 | if (arg) { 527 | editor_open(editor, arg); 528 | } 529 | } 530 | 531 | EDITOR_COMMAND(splitfind, sfind) { 532 | if (!arg) { 533 | editor_status_err(editor, "No file name"); 534 | return; 535 | } 536 | 537 | char *path = editor_find_in_path(editor, arg); 538 | if (!path) { 539 | editor_status_err(editor, "Can't find file \"%s\" in path", arg); 540 | return; 541 | } 542 | 543 | editor_command_split(editor, path, false); 544 | free(path); 545 | } 546 | 547 | EDITOR_COMMAND_WITH_COMPLETION(vsplit, vsp, COMPLETION_PATHS) { 548 | editor_set_window(editor, window_split(editor->window, 549 | editor->opt.splitright ? WINDOW_SPLIT_RIGHT : WINDOW_SPLIT_LEFT)); 550 | 551 | if (editor->opt.equalalways) { 552 | window_equalize(editor->window, WINDOW_SPLIT_VERTICAL); 553 | } 554 | 555 | if (arg) { 556 | editor_open(editor, arg); 557 | } 558 | } 559 | 560 | void window_clear_working_directories(struct window *window) { 561 | if (window->split_type == WINDOW_LEAF) { 562 | free(window->pwd); 563 | window->pwd = NULL; 564 | return; 565 | } 566 | 567 | window_clear_working_directories(window->split.first); 568 | window_clear_working_directories(window->split.second); 569 | } 570 | --------------------------------------------------------------------------------