From dae006a942213f1c37ecfd20ac79d2f7fc462696 Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Sun, 4 Oct 2015 09:36:34 -0300 Subject: main: Extract `normal_check` from `main_loop` The new function contains logic that must be executed after handling input in normal mode and also before the first main loop iteration. Also rename `main_loop` to `normal_enter` and move it to normal.c --- src/nvim/main.c | 218 ++------------------------------------------------------ 1 file changed, 4 insertions(+), 214 deletions(-) (limited to 'src/nvim/main.c') diff --git a/src/nvim/main.c b/src/nvim/main.c index 60a242fae3..06fd116b86 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -528,228 +528,16 @@ int main(int argc, char **argv) } TIME_MSG("before starting main loop"); + ILOG("Starting Neovim main loop."); /* * Call the main command loop. This never returns. */ - main_loop(FALSE, FALSE); + normal_enter(false, false); return 0; } -/* - * Main loop: Execute Normal mode commands until exiting Vim. - * Also used to handle commands in the command-line window, until the window - * is closed. - * Also used to handle ":visual" command after ":global": execute Normal mode - * commands, return when entering Ex mode. "noexmode" is TRUE then. - */ -void -main_loop ( - int cmdwin, /* TRUE when working in the command-line window */ - int noexmode /* TRUE when return on entering Ex mode */ -) -{ - oparg_T oa; /* operator arguments */ - int previous_got_int = FALSE; /* "got_int" was TRUE */ - linenr_T conceal_old_cursor_line = 0; - linenr_T conceal_new_cursor_line = 0; - int conceal_update_lines = FALSE; - - ILOG("Starting Neovim main loop."); - - clear_oparg(&oa); - while (!cmdwin - || cmdwin_result == 0 - ) { - if (stuff_empty()) { - did_check_timestamps = FALSE; - if (need_check_timestamps) - check_timestamps(FALSE); - if (need_wait_return) /* if wait_return still needed ... */ - wait_return(FALSE); /* ... call it now */ - if (need_start_insertmode && goto_im() - && !VIsual_active - ) { - need_start_insertmode = FALSE; - stuffReadbuff((char_u *)"i"); /* start insert mode next */ - /* skip the fileinfo message now, because it would be shown - * after insert mode finishes! */ - need_fileinfo = FALSE; - } - } - - /* Reset "got_int" now that we got back to the main loop. Except when - * inside a ":g/pat/cmd" command, then the "got_int" needs to abort - * the ":g" command. - * For ":g/pat/vi" we reset "got_int" when used once. When used - * a second time we go back to Ex mode and abort the ":g" command. */ - if (got_int) { - if (noexmode && global_busy && !exmode_active && previous_got_int) { - /* Typed two CTRL-C in a row: go back to ex mode as if "Q" was - * used and keep "got_int" set, so that it aborts ":g". */ - exmode_active = EXMODE_NORMAL; - State = NORMAL; - } else if (!global_busy || !exmode_active) { - if (!quit_more) - (void)vgetc(); /* flush all buffers */ - got_int = FALSE; - } - previous_got_int = TRUE; - } else - previous_got_int = FALSE; - - if (!exmode_active) - msg_scroll = FALSE; - quit_more = FALSE; - - /* - * If skip redraw is set (for ":" in wait_return()), don't redraw now. - * If there is nothing in the stuff_buffer or do_redraw is TRUE, - * update cursor and redraw. - */ - if (skip_redraw || exmode_active) - skip_redraw = FALSE; - else if (do_redraw || stuff_empty()) { - /* Trigger CursorMoved if the cursor moved. */ - if (!finish_op && ( - has_cursormoved() - || - curwin->w_p_cole > 0 - ) - && !equalpos(last_cursormoved, curwin->w_cursor)) { - if (has_cursormoved()) - apply_autocmds(EVENT_CURSORMOVED, NULL, NULL, - FALSE, curbuf); - if (curwin->w_p_cole > 0) { - conceal_old_cursor_line = last_cursormoved.lnum; - conceal_new_cursor_line = curwin->w_cursor.lnum; - conceal_update_lines = TRUE; - } - last_cursormoved = curwin->w_cursor; - } - - /* Trigger TextChanged if b_changedtick differs. */ - if (!finish_op && has_textchanged() - && last_changedtick != curbuf->b_changedtick) { - if (last_changedtick_buf == curbuf) - apply_autocmds(EVENT_TEXTCHANGED, NULL, NULL, - FALSE, curbuf); - last_changedtick_buf = curbuf; - last_changedtick = curbuf->b_changedtick; - } - - /* Scroll-binding for diff mode may have been postponed until - * here. Avoids doing it for every change. */ - if (diff_need_scrollbind) { - check_scrollbind((linenr_T)0, 0L); - diff_need_scrollbind = FALSE; - } - /* Include a closed fold completely in the Visual area. */ - foldAdjustVisual(); - /* - * When 'foldclose' is set, apply 'foldlevel' to folds that don't - * contain the cursor. - * When 'foldopen' is "all", open the fold(s) under the cursor. - * This may mark the window for redrawing. - */ - if (hasAnyFolding(curwin) && !char_avail()) { - foldCheckClose(); - if (fdo_flags & FDO_ALL) - foldOpenCursor(); - } - - /* - * Before redrawing, make sure w_topline is correct, and w_leftcol - * if lines don't wrap, and w_skipcol if lines wrap. - */ - update_topline(); - validate_cursor(); - - if (VIsual_active) - update_curbuf(INVERTED); /* update inverted part */ - else if (must_redraw) - update_screen(0); - else if (redraw_cmdline || clear_cmdline) - showmode(); - redraw_statuslines(); - if (need_maketitle) - maketitle(); - /* display message after redraw */ - if (keep_msg != NULL) { - char_u *p; - - // msg_attr_keep() will set keep_msg to NULL, must free the string - // here. Don't reset keep_msg, msg_attr_keep() uses it to check for - // duplicates. - p = keep_msg; - msg_attr(p, keep_msg_attr); - xfree(p); - } - if (need_fileinfo) { /* show file info after redraw */ - fileinfo(FALSE, TRUE, FALSE); - need_fileinfo = FALSE; - } - - emsg_on_display = FALSE; /* can delete error message now */ - did_emsg = FALSE; - msg_didany = FALSE; /* reset lines_left in msg_start() */ - may_clear_sb_text(); /* clear scroll-back text on next msg */ - showruler(FALSE); - - if (conceal_update_lines - && (conceal_old_cursor_line != conceal_new_cursor_line - || conceal_cursor_line(curwin) - || need_cursor_line_redraw)) { - if (conceal_old_cursor_line != conceal_new_cursor_line - && conceal_old_cursor_line - <= curbuf->b_ml.ml_line_count) - update_single_line(curwin, conceal_old_cursor_line); - update_single_line(curwin, conceal_new_cursor_line); - curwin->w_valid &= ~VALID_CROW; - } - setcursor(); - - do_redraw = FALSE; - - /* Now that we have drawn the first screen all the startup stuff - * has been done, close any file for startup messages. */ - if (time_fd != NULL) { - TIME_MSG("first screen update"); - TIME_MSG("--- NVIM STARTED ---"); - fclose(time_fd); - time_fd = NULL; - } - } - - /* - * Update w_curswant if w_set_curswant has been set. - * Postponed until here to avoid computing w_virtcol too often. - */ - update_curswant(); - - /* - * May perform garbage collection when waiting for a character, but - * only at the very toplevel. Otherwise we may be using a List or - * Dict internally somewhere. - * "may_garbage_collect" is reset in vgetc() which is invoked through - * do_exmode() and normal_cmd(). - */ - may_garbage_collect = (!cmdwin && !noexmode); - /* - * If we're invoked as ex, do a round of ex commands. - * Otherwise, get and execute a normal mode command. - */ - if (exmode_active) { - if (noexmode) /* End of ":global/path/visual" commands */ - return; - do_exmode(exmode_active == EXMODE_VIM); - } else - normal_cmd(&oa, TRUE); - } -} - - /* Exit properly */ void getout(int exitval) { @@ -2075,3 +1863,5 @@ static void check_swap_exists_action(void) getout(1); handle_swap_exists(NULL); } + + -- cgit From 8d93621c6368fb6e3e3f7138bff50e08585f347b Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Mon, 5 Oct 2015 14:17:15 -0300 Subject: main: Start modeling Nvim as pushdown automaton From a very high level point of view, Vim/Nvim can be described as state machines following these instructions in a loop: - Read user input - Peform some action. The action is determined by the current state and can switch states. - Possibly display some feedback to the user. This is not immediately visible because these instructions spread across dozens of nested loops and function calls, making it very hard to modify the state machine(to support more event types, for example). So far, the approach Nvim has taken to allow more events is this: - At the very core function that blocks for input, poll for arbitrary events. - If the event received from the OS is user input, just return it normally to the callers. - If the event is not direct result of user input(possibly a vimscript function call coming from a msgpack-rpc socket or a job control callback), return a special key code(`K_EVENT`) that is handled by callers where it is safer to perform arbitrary actions. One problem with this approach is that the `K_EVENT` signal is being sent across multiple states that may be unaware of it. This was partially fixed with the `input_enable_events`/`input_disable_events` functions, which were added as a mechanism that the upper layers can use to tell the core input functions that it is ready to accept `K_EVENT`. Another problem is that the mapping engine is implemented in getchar.c which is called from every state, but the mapping engine is not aware of `K_EVENT` so events can break mappings. While it is theoretically possible to modify getchar.c to make it aware of `K_EVENT`, this commit fixes the problem with a different approach: Model Nvim as a pushdown automaton(https://en.wikipedia.org/wiki/Pushdown_automaton). This design has many advantages which include: - Decoupling the event loop from the states reponsible for handling events. - Better control of state transition with less dependency on global variable hacks(eg: 'restart_edit' global variable). - Easier removal of global variables and function splitting. That is because many variables are for state-specific information, and probably ended up being global to simplify communication between functions, which we fix by storing state-specific information in specialized structures. The final goal is to let Nvim have a single top-level event loop represented by the following pseudo-code: ``` while not quitting let event = read_event current_state(event) update_screen() ``` This closely mirrors the state machine description above and makes it easier to understand, extend and debug the program. Note that while the pseudo code suggests an explicit stack of states that doesn't rely on return addresses(as suggested by the principles of automata-based programming: https://en.wikipedia.org/wiki/Automata-based_programming), for now we'll use the call stack as a structure to manage state transitioning as it would be very difficult to refactor Nvim to use an explicit stack of states, and the benefits would be small. While this change may seem like an endless amount of work, it is possible to do it incrementally as was shown in the previous commits. The general procedure is: 1- Find a blocking `vgetc()`(or derivatives) call. This call represents an implicit state of the program. 2- Split the code before and after the `vgetc()` call into functions that match the signature of `state_check_callback` and `state_execute_callback. Only `state_execute_callback` is required. 3- Create a `VimState` "subclass" and a initializer function that sets the function pointers and performs any other required initialization steps. If the state has no local variables, just use `VimState` without subclassing. 4- Instead of calling the original function containing the `vgetc()`, initialize a stack-allocated `VimState` subclass, then call `state_enter` to begin processing events in the state. 5- The check/execute callbacks can return 1 to continue normally, 0 to break the loop or -1 to skip to the next iteration. These callbacks contain code that execute before and after the old `vgetc()` call. The functions created in step 2 may contain other `vgetc()` calls. These represent implicit sub-states of the current state, but it is fine to remove them later in smaller steps since we didn't break compatibility with existing code. --- src/nvim/main.c | 1 + 1 file changed, 1 insertion(+) (limited to 'src/nvim/main.c') diff --git a/src/nvim/main.c b/src/nvim/main.c index 06fd116b86..eb2a1567e7 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -54,6 +54,7 @@ #include "nvim/profile.h" #include "nvim/quickfix.h" #include "nvim/screen.h" +#include "nvim/state.h" #include "nvim/strings.h" #include "nvim/syntax.h" #include "nvim/ui.h" -- cgit