/* ---------------------------------------------------------------------------- Copyright (c) 2021, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT License. A copy of the license can be found in the "LICENSE" file at the root of this distribution. -----------------------------------------------------------------------------*/ #include #include #include "common.h" #include "term.h" #include "tty.h" #include "env.h" #include "stringbuf.h" #include "history.h" #include "completions.h" #include "undo.h" #include "highlight.h" //------------------------------------------------------------- // The editor state //------------------------------------------------------------- // editor state typedef struct editor_s { stringbuf_t* input; // current user input stringbuf_t* extra; // extra displayed info (for completion menu etc) stringbuf_t* hint; // hint displayed as part of the input stringbuf_t* hint_help; // help for a hint. ssize_t pos; // current cursor position in the input ssize_t cur_rows; // current used rows to display our content (including extra content) ssize_t cur_row; // current row that has the cursor (0 based, relative to the prompt) ssize_t termw; bool modified; // has a modification happened? (used for history navigation for example) bool disable_undo; // temporarily disable auto undo (for history search) ssize_t history_idx; // current index in the history editstate_t* undo; // undo buffer editstate_t* redo; // redo buffer const char* prompt_text; // text of the prompt before the prompt marker alloc_t* mem; // allocator // caches attrbuf_t* attrs; // reuse attribute buffers attrbuf_t* attrs_extra; } editor_t; //------------------------------------------------------------- // Main edit line //------------------------------------------------------------- static char* edit_line( ic_env_t* env, const char* prompt_text ); // defined at bottom static void edit_refresh(ic_env_t* env, editor_t* eb); ic_private char* ic_editline(ic_env_t* env, const char* prompt_text) { tty_start_raw(env->tty); term_start_raw(env->term); char* line = edit_line(env,prompt_text); term_end_raw(env->term,false); tty_end_raw(env->tty); term_writeln(env->term,""); term_flush(env->term); return line; } //------------------------------------------------------------- // Undo/Redo //------------------------------------------------------------- // capture the current edit state static void editor_capture(editor_t* eb, editstate_t** es ) { if (!eb->disable_undo) { editstate_capture( eb->mem, es, sbuf_string(eb->input), eb->pos ); } } static void editor_undo_capture(editor_t* eb ) { editor_capture(eb, &eb->undo ); } static void editor_undo_forget(editor_t* eb) { if (eb->disable_undo) return; const char* input = NULL; ssize_t pos = 0; editstate_restore(eb->mem, &eb->undo, &input, &pos); mem_free(eb->mem, input); } static void editor_restore(editor_t* eb, editstate_t** from, editstate_t** to ) { if (eb->disable_undo) return; if (*from == NULL) return; const char* input; if (to != NULL) { editor_capture( eb, to ); } if (!editstate_restore( eb->mem, from, &input, &eb->pos )) return; sbuf_replace( eb->input, input ); mem_free(eb->mem, input); eb->modified = false; } static void editor_undo_restore(editor_t* eb, bool with_redo ) { editor_restore(eb, &eb->undo, (with_redo ? &eb->redo : NULL)); } static void editor_redo_restore(editor_t* eb ) { editor_restore(eb, &eb->redo, &eb->undo); eb->modified = false; } static void editor_start_modify(editor_t* eb ) { editor_undo_capture(eb); editstate_done(eb->mem, &eb->redo); // clear redo eb->modified = true; } static bool editor_pos_is_at_end(editor_t* eb ) { return (eb->pos == sbuf_len(eb->input)); } //------------------------------------------------------------- // Row/Column width and positioning //------------------------------------------------------------- static void edit_get_prompt_width( ic_env_t* env, editor_t* eb, bool in_extra, ssize_t* promptw, ssize_t* cpromptw ) { if (in_extra) { *promptw = 0; *cpromptw = 0; } else { // todo: cache prompt widths ssize_t textw = bbcode_column_width(env->bbcode, eb->prompt_text); ssize_t markerw = bbcode_column_width(env->bbcode, env->prompt_marker); ssize_t cmarkerw = bbcode_column_width(env->bbcode, env->cprompt_marker); *promptw = markerw + textw; *cpromptw = (env->no_multiline_indent || *promptw < cmarkerw ? cmarkerw : *promptw); } } static ssize_t edit_get_rowcol( ic_env_t* env, editor_t* eb, rowcol_t* rc ) { ssize_t promptw, cpromptw; edit_get_prompt_width(env, eb, false, &promptw, &cpromptw); return sbuf_get_rc_at_pos( eb->input, eb->termw, promptw, cpromptw, eb->pos, rc ); } static void edit_set_pos_at_rowcol( ic_env_t* env, editor_t* eb, ssize_t row, ssize_t col ) { ssize_t promptw, cpromptw; edit_get_prompt_width(env, eb, false, &promptw, &cpromptw); ssize_t pos = sbuf_get_pos_at_rc( eb->input, eb->termw, promptw, cpromptw, row, col ); if (pos < 0) return; eb->pos = pos; edit_refresh(env, eb); } static bool edit_pos_is_at_row_end( ic_env_t* env, editor_t* eb ) { rowcol_t rc; edit_get_rowcol( env, eb, &rc ); return rc.last_on_row; } static void edit_write_prompt( ic_env_t* env, editor_t* eb, ssize_t row, bool in_extra ) { if (in_extra) return; bbcode_style_open(env->bbcode, "ic-prompt"); if (row==0) { // regular prompt text bbcode_print( env->bbcode, eb->prompt_text ); } else if (!env->no_multiline_indent) { // multiline continuation indentation // todo: cache prompt widths ssize_t textw = bbcode_column_width(env->bbcode, eb->prompt_text ); ssize_t markerw = bbcode_column_width(env->bbcode, env->prompt_marker); ssize_t cmarkerw = bbcode_column_width(env->bbcode, env->cprompt_marker); if (cmarkerw < markerw + textw) { term_write_repeat(env->term, " ", markerw + textw - cmarkerw ); } } // the marker bbcode_print(env->bbcode, (row == 0 ? env->prompt_marker : env->cprompt_marker )); bbcode_style_close(env->bbcode,NULL); } //------------------------------------------------------------- // Refresh //------------------------------------------------------------- typedef struct refresh_info_s { ic_env_t* env; editor_t* eb; attrbuf_t* attrs; bool in_extra; ssize_t first_row; ssize_t last_row; } refresh_info_t; static bool edit_refresh_rows_iter( const char* s, ssize_t row, ssize_t row_start, ssize_t row_len, ssize_t startw, bool is_wrap, const void* arg, void* res) { ic_unused(res); ic_unused(startw); const refresh_info_t* info = (const refresh_info_t*)(arg); term_t* term = info->env->term; // debug_msg("edit: line refresh: row %zd, len: %zd\n", row, row_len); if (row < info->first_row) return false; if (row > info->last_row) return true; // should not occur // term_clear_line(term); edit_write_prompt(info->env, info->eb, row, info->in_extra); //' write output if (info->attrs == NULL || (info->env->no_highlight && info->env->no_bracematch)) { term_write_n( term, s + row_start, row_len ); } else { term_write_formatted_n( term, s + row_start, attrbuf_attrs(info->attrs, row_start + row_len) + row_start, row_len ); } // write line ending if (row < info->last_row) { if (is_wrap && tty_is_utf8(info->env->tty)) { #ifndef __APPLE__ bbcode_print( info->env->bbcode, "[ic-dim]\xE2\x86\x90"); // left arrow #else bbcode_print( info->env->bbcode, "[ic-dim]\xE2\x86\xB5" ); // return symbol #endif } term_clear_to_end_of_line(term); term_writeln(term, ""); } else { term_clear_to_end_of_line(term); } return (row >= info->last_row); } static void edit_refresh_rows(ic_env_t* env, editor_t* eb, stringbuf_t* input, attrbuf_t* attrs, ssize_t promptw, ssize_t cpromptw, bool in_extra, ssize_t first_row, ssize_t last_row) { if (input == NULL) return; refresh_info_t info; info.env = env; info.eb = eb; info.attrs = attrs; info.in_extra = in_extra; info.first_row = first_row; info.last_row = last_row; sbuf_for_each_row( input, eb->termw, promptw, cpromptw, &edit_refresh_rows_iter, &info, NULL); } static void edit_refresh(ic_env_t* env, editor_t* eb) { // calculate the new cursor row and total rows needed ssize_t promptw, cpromptw; edit_get_prompt_width( env, eb, false, &promptw, &cpromptw ); if (eb->attrs != NULL) { highlight( env->mem, env->bbcode, sbuf_string(eb->input), eb->attrs, (env->no_highlight ? NULL : env->highlighter), env->highlighter_arg ); } // highlight matching braces if (eb->attrs != NULL && !env->no_bracematch) { highlight_match_braces(sbuf_string(eb->input), eb->attrs, eb->pos, ic_env_get_match_braces(env), bbcode_style(env->bbcode,"ic-bracematch"), bbcode_style(env->bbcode,"ic-error")); } // insert hint if (sbuf_len(eb->hint) > 0) { if (eb->attrs != NULL) { attrbuf_insert_at( eb->attrs, eb->pos, sbuf_len(eb->hint), bbcode_style(env->bbcode, "ic-hint") ); } sbuf_insert_at(eb->input, sbuf_string(eb->hint), eb->pos ); } // render extra (like a completion menu) stringbuf_t* extra = NULL; if (sbuf_len(eb->extra) > 0) { extra = sbuf_new(eb->mem); if (extra != NULL) { if (sbuf_len(eb->hint_help) > 0) { bbcode_append(env->bbcode, sbuf_string(eb->hint_help), extra, eb->attrs_extra); } bbcode_append(env->bbcode, sbuf_string(eb->extra), extra, eb->attrs_extra); } } // calculate rows and row/col position rowcol_t rc = { 0 }; const ssize_t rows_input = sbuf_get_rc_at_pos( eb->input, eb->termw, promptw, cpromptw, eb->pos, &rc ); rowcol_t rc_extra = { 0 }; ssize_t rows_extra = 0; if (extra != NULL) { rows_extra = sbuf_get_rc_at_pos( extra, eb->termw, 0, 0, 0 /*pos*/, &rc_extra ); } const ssize_t rows = rows_input + rows_extra; debug_msg("edit: refresh: rows %zd, cursor: %zd,%zd (previous rows %zd, cursor row %zd)\n", rows, rc.row, rc.col, eb->cur_rows, eb->cur_row); // only render at most terminal height rows const ssize_t termh = term_get_height(env->term); ssize_t first_row = 0; // first visible row ssize_t last_row = rows - 1; // last visible row if (rows > termh) { first_row = rc.row - termh + 1; // ensure cursor is visible if (first_row < 0) first_row = 0; last_row = first_row + termh - 1; } assert(last_row - first_row < termh); // reduce flicker buffer_mode_t bmode = term_set_buffer_mode(env->term, BUFFERED); // back up to the first line term_start_of_line(env->term); term_up(env->term, (eb->cur_row >= termh ? termh-1 : eb->cur_row) ); // term_clear_lines_to_end(env->term); // gives flicker in old Windows cmd prompt // render rows edit_refresh_rows( env, eb, eb->input, eb->attrs, promptw, cpromptw, false, first_row, last_row ); if (rows_extra > 0) { assert(extra != NULL); const ssize_t first_rowx = (first_row > rows_input ? first_row - rows_input : 0); const ssize_t last_rowx = last_row - rows_input; assert(last_rowx >= 0); edit_refresh_rows(env, eb, extra, eb->attrs_extra, 0, 0, true, first_rowx, last_rowx); } // overwrite trailing rows we do not use anymore ssize_t rrows = last_row - first_row + 1; // rendered rows if (rrows < termh && rows < eb->cur_rows) { ssize_t clear = eb->cur_rows - rows; while (rrows < termh && clear > 0) { clear--; rrows++; term_writeln(env->term,""); term_clear_line(env->term); } } // move cursor back to edit position term_start_of_line(env->term); term_up(env->term, first_row + rrows - 1 - rc.row ); term_right(env->term, rc.col + (rc.row == 0 ? promptw : cpromptw)); // and refresh term_flush(env->term); // stop buffering term_set_buffer_mode(env->term, bmode); // restore input by removing the hint sbuf_delete_at(eb->input, eb->pos, sbuf_len(eb->hint)); sbuf_delete_at(eb->extra, 0, sbuf_len(eb->hint_help)); attrbuf_clear(eb->attrs); attrbuf_clear(eb->attrs_extra); sbuf_free(extra); // update previous eb->cur_rows = rows; eb->cur_row = rc.row; } // clear current output static void edit_clear(ic_env_t* env, editor_t* eb ) { term_attr_reset(env->term); term_up(env->term, eb->cur_row); // overwrite all rows for( ssize_t i = 0; i < eb->cur_rows; i++) { term_clear_line(env->term); term_writeln(env->term, ""); } // move cursor back term_up(env->term, eb->cur_rows - eb->cur_row ); } // clear screen and refresh static void edit_clear_screen(ic_env_t* env, editor_t* eb ) { ssize_t cur_rows = eb->cur_rows; eb->cur_rows = term_get_height(env->term) - 1; edit_clear(env,eb); eb->cur_rows = cur_rows; edit_refresh(env,eb); } // refresh after a terminal window resized (but before doing further edit operations!) static bool edit_resize(ic_env_t* env, editor_t* eb ) { // update dimensions term_update_dim(env->term); ssize_t newtermw = term_get_width(env->term); if (eb->termw == newtermw) return false; // recalculate the row layout assuming the hardwrapping for the new terminal width ssize_t promptw, cpromptw; edit_get_prompt_width( env, eb, false, &promptw, &cpromptw ); sbuf_insert_at(eb->input, sbuf_string(eb->hint), eb->pos); // insert used hint // render extra (like a completion menu) stringbuf_t* extra = NULL; if (sbuf_len(eb->extra) > 0) { extra = sbuf_new(eb->mem); if (extra != NULL) { if (sbuf_len(eb->hint_help) > 0) { bbcode_append(env->bbcode, sbuf_string(eb->hint_help), extra, NULL); } bbcode_append(env->bbcode, sbuf_string(eb->extra), extra, NULL); } } rowcol_t rc = { 0 }; const ssize_t rows_input = sbuf_get_wrapped_rc_at_pos( eb->input, eb->termw, newtermw, promptw, cpromptw, eb->pos, &rc ); rowcol_t rc_extra = { 0 }; ssize_t rows_extra = 0; if (extra != NULL) { rows_extra = sbuf_get_wrapped_rc_at_pos(extra, eb->termw, newtermw, 0, 0, 0 /*pos*/, &rc_extra); } ssize_t rows = rows_input + rows_extra; debug_msg("edit: resize: new rows: %zd, cursor row: %zd (previous: rows: %zd, cursor row %zd)\n", rows, rc.row, eb->cur_rows, eb->cur_row); // update the newly calculated row and rows eb->cur_row = rc.row; if (rows > eb->cur_rows) { eb->cur_rows = rows; } eb->termw = newtermw; edit_refresh(env,eb); // remove hint again sbuf_delete_at(eb->input, eb->pos, sbuf_len(eb->hint)); sbuf_free(extra); return true; } static void editor_append_hint_help(editor_t* eb, const char* help) { sbuf_clear(eb->hint_help); if (help != NULL) { sbuf_replace(eb->hint_help, "[ic-info]"); sbuf_append(eb->hint_help, help); sbuf_append(eb->hint_help, "[/ic-info]\n"); } } // refresh with possible hint static void edit_refresh_hint(ic_env_t* env, editor_t* eb) { if (env->no_hint || env->hint_delay > 0) { // refresh without hint first edit_refresh(env, eb); if (env->no_hint) return; } // and see if we can construct a hint (displayed after a delay) ssize_t count = completions_generate(env, env->completions, sbuf_string(eb->input), eb->pos, 2); if (count == 1) { const char* help = NULL; const char* hint = completions_get_hint(env->completions, 0, &help); if (hint != NULL) { sbuf_replace(eb->hint, hint); editor_append_hint_help(eb, help); // do auto-tabbing? if (env->complete_autotab) { stringbuf_t* sb = sbuf_new(env->mem); // temporary buffer for completion if (sb != NULL) { sbuf_replace( sb, sbuf_string(eb->input) ); ssize_t pos = eb->pos; const char* extra_hint = hint; do { ssize_t newpos = sbuf_insert_at( sb, extra_hint, pos ); if (newpos <= pos) break; pos = newpos; count = completions_generate(env, env->completions, sbuf_string(sb), pos, 2); if (count == 1) { const char* extra_help = NULL; extra_hint = completions_get_hint(env->completions, 0, &extra_help); if (extra_hint != NULL) { editor_append_hint_help(eb, extra_help); sbuf_append(eb->hint, extra_hint); } } } while(count == 1); sbuf_free(sb); } } } } if (env->hint_delay <= 0) { // refresh with hint directly edit_refresh(env, eb); } } //------------------------------------------------------------- // Edit operations //------------------------------------------------------------- static void edit_history_prev(ic_env_t* env, editor_t* eb); static void edit_history_next(ic_env_t* env, editor_t* eb); static void edit_undo_restore(ic_env_t* env, editor_t* eb) { editor_undo_restore(eb, true); edit_refresh(env,eb); } static void edit_redo_restore(ic_env_t* env, editor_t* eb) { editor_redo_restore(eb); edit_refresh(env,eb); } static void edit_cursor_left(ic_env_t* env, editor_t* eb) { ssize_t cwidth = 1; ssize_t prev = sbuf_prev(eb->input,eb->pos,&cwidth); if (prev < 0) return; rowcol_t rc; edit_get_rowcol( env, eb, &rc); eb->pos = prev; edit_refresh(env,eb); } static void edit_cursor_right(ic_env_t* env, editor_t* eb) { ssize_t cwidth = 1; ssize_t next = sbuf_next(eb->input,eb->pos,&cwidth); if (next < 0) return; rowcol_t rc; edit_get_rowcol( env, eb, &rc); eb->pos = next; edit_refresh(env,eb); } static void edit_cursor_line_end(ic_env_t* env, editor_t* eb) { ssize_t end = sbuf_find_line_end(eb->input,eb->pos); if (end < 0) return; eb->pos = end; edit_refresh(env,eb); } static void edit_cursor_line_start(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_line_start(eb->input,eb->pos); if (start < 0) return; eb->pos = start; edit_refresh(env,eb); } static void edit_cursor_next_word(ic_env_t* env, editor_t* eb) { ssize_t end = sbuf_find_word_end(eb->input,eb->pos); if (end < 0) return; eb->pos = end; edit_refresh(env,eb); } static void edit_cursor_prev_word(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_word_start(eb->input,eb->pos); if (start < 0) return; eb->pos = start; edit_refresh(env,eb); } static void edit_cursor_next_ws_word(ic_env_t* env, editor_t* eb) { ssize_t end = sbuf_find_ws_word_end(eb->input, eb->pos); if (end < 0) return; eb->pos = end; edit_refresh(env, eb); } static void edit_cursor_prev_ws_word(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_ws_word_start(eb->input, eb->pos); if (start < 0) return; eb->pos = start; edit_refresh(env, eb); } static void edit_cursor_to_start(ic_env_t* env, editor_t* eb) { eb->pos = 0; edit_refresh(env,eb); } static void edit_cursor_to_end(ic_env_t* env, editor_t* eb) { eb->pos = sbuf_len(eb->input); edit_refresh(env,eb); } static void edit_cursor_row_up(ic_env_t* env, editor_t* eb) { rowcol_t rc; edit_get_rowcol( env, eb, &rc); if (rc.row == 0) { edit_history_prev(env,eb); } else { edit_set_pos_at_rowcol( env, eb, rc.row - 1, rc.col ); } } static void edit_cursor_row_down(ic_env_t* env, editor_t* eb) { rowcol_t rc; ssize_t rows = edit_get_rowcol( env, eb, &rc); if (rc.row + 1 >= rows) { edit_history_next(env,eb); } else { edit_set_pos_at_rowcol( env, eb, rc.row + 1, rc.col ); } } static void edit_cursor_match_brace(ic_env_t* env, editor_t* eb) { ssize_t match = find_matching_brace( sbuf_string(eb->input), eb->pos, ic_env_get_match_braces(env), NULL ); if (match < 0) return; eb->pos = match; edit_refresh(env,eb); } static void edit_backspace(ic_env_t* env, editor_t* eb) { if (eb->pos <= 0) return; editor_start_modify(eb); eb->pos = sbuf_delete_char_before(eb->input,eb->pos); edit_refresh(env,eb); } static void edit_delete_char(ic_env_t* env, editor_t* eb) { if (eb->pos >= sbuf_len(eb->input)) return; editor_start_modify(eb); sbuf_delete_char_at(eb->input,eb->pos); edit_refresh(env,eb); } static void edit_delete_all(ic_env_t* env, editor_t* eb) { if (sbuf_len(eb->input) <= 0) return; editor_start_modify(eb); sbuf_clear(eb->input); eb->pos = 0; edit_refresh(env,eb); } static void edit_delete_to_end_of_line(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_line_start(eb->input,eb->pos); if (start < 0) return; ssize_t end = sbuf_find_line_end(eb->input,eb->pos); if (end < 0) return; editor_start_modify(eb); // if on an empty line, remove it completely if (start == end && sbuf_char_at(eb->input,end) == '\n') { end++; } else if (start == end && sbuf_char_at(eb->input,start - 1) == '\n') { eb->pos--; } sbuf_delete_from_to( eb->input, eb->pos, end ); edit_refresh(env,eb); } static void edit_delete_to_start_of_line(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_line_start(eb->input,eb->pos); if (start < 0) return; ssize_t end = sbuf_find_line_end(eb->input,eb->pos); if (end < 0) return; editor_start_modify(eb); // delete start newline if it was an empty line bool goright = false; if (start > 0 && sbuf_char_at(eb->input,start-1) == '\n' && start == end) { // if it is an empty line remove it start--; // afterwards, move to start of next line if it exists (so the cursor stays on the same row) goright = true; } sbuf_delete_from_to( eb->input, start, eb->pos ); eb->pos = start; if (goright) edit_cursor_right(env,eb); edit_refresh(env,eb); } static void edit_delete_line(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_line_start(eb->input,eb->pos); if (start < 0) return; ssize_t end = sbuf_find_line_end(eb->input,eb->pos); if (end < 0) return; editor_start_modify(eb); // delete newline as well so no empty line is left; bool goright = false; if (start > 0 && sbuf_char_at(eb->input,start-1) == '\n') { start--; // afterwards, move to start of next line if it exists (so the cursor stays on the same row) goright = true; } else if (sbuf_char_at(eb->input,end) == '\n') { end++; } sbuf_delete_from_to(eb->input,start,end); eb->pos = start; if (goright) edit_cursor_right(env,eb); edit_refresh(env,eb); } static void edit_delete_to_start_of_word(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_word_start(eb->input,eb->pos); if (start < 0) return; editor_start_modify(eb); sbuf_delete_from_to( eb->input, start, eb->pos ); eb->pos = start; edit_refresh(env,eb); } static void edit_delete_to_end_of_word(ic_env_t* env, editor_t* eb) { ssize_t end = sbuf_find_word_end(eb->input,eb->pos); if (end < 0) return; editor_start_modify(eb); sbuf_delete_from_to( eb->input, eb->pos, end ); edit_refresh(env,eb); } static void edit_delete_to_start_of_ws_word(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_ws_word_start(eb->input, eb->pos); if (start < 0) return; editor_start_modify(eb); sbuf_delete_from_to(eb->input, start, eb->pos); eb->pos = start; edit_refresh(env, eb); } static void edit_delete_to_end_of_ws_word(ic_env_t* env, editor_t* eb) { ssize_t end = sbuf_find_ws_word_end(eb->input, eb->pos); if (end < 0) return; editor_start_modify(eb); sbuf_delete_from_to(eb->input, eb->pos, end); edit_refresh(env, eb); } static void edit_delete_word(ic_env_t* env, editor_t* eb) { ssize_t start = sbuf_find_word_start(eb->input,eb->pos); if (start < 0) return; ssize_t end = sbuf_find_word_end(eb->input,eb->pos); if (end < 0) return; editor_start_modify(eb); sbuf_delete_from_to(eb->input,start,end); eb->pos = start; edit_refresh(env,eb); } static void edit_swap_char( ic_env_t* env, editor_t* eb ) { if (eb->pos <= 0 || eb->pos == sbuf_len(eb->input)) return; editor_start_modify(eb); eb->pos = sbuf_swap_char(eb->input,eb->pos); edit_refresh(env,eb); } static void edit_multiline_eol(ic_env_t* env, editor_t* eb) { if (eb->pos <= 0) return; if (sbuf_string(eb->input)[eb->pos-1] != env->multiline_eol) return; editor_start_modify(eb); // replace line continuation with a real newline sbuf_delete_at( eb->input, eb->pos-1, 1); sbuf_insert_at( eb->input, "\n", eb->pos-1); edit_refresh(env,eb); } static void edit_insert_unicode(ic_env_t* env, editor_t* eb, unicode_t u) { editor_start_modify(eb); ssize_t nextpos = sbuf_insert_unicode_at(eb->input, u, eb->pos); if (nextpos >= 0) eb->pos = nextpos; edit_refresh_hint(env, eb); } static void edit_auto_brace(ic_env_t* env, editor_t* eb, char c) { if (env->no_autobrace) return; const char* braces = ic_env_get_auto_braces(env); for (const char* b = braces; *b != 0; b += 2) { if (*b == c) { const char close = b[1]; //if (sbuf_char_at(eb->input, eb->pos) != close) { sbuf_insert_char_at(eb->input, close, eb->pos); bool balanced = false; find_matching_brace(sbuf_string(eb->input), eb->pos, braces, &balanced ); if (!balanced) { // don't insert if it leads to an unbalanced expression. sbuf_delete_char_at(eb->input, eb->pos); } //} return; } else if (b[1] == c) { // close brace, check if there we don't overwrite to the right if (sbuf_char_at(eb->input, eb->pos) == c) { sbuf_delete_char_at(eb->input, eb->pos); } return; } } } static void editor_auto_indent(editor_t* eb, const char* pre, const char* post ) { assert(eb->pos > 0 && sbuf_char_at(eb->input,eb->pos-1) == '\n'); ssize_t prelen = ic_strlen(pre); if (prelen > 0) { if (eb->pos - 1 < prelen) return; if (!ic_starts_with(sbuf_string(eb->input) + eb->pos - 1 - prelen, pre)) return; if (!ic_starts_with(sbuf_string(eb->input) + eb->pos, post)) return; eb->pos = sbuf_insert_at(eb->input, " ", eb->pos); sbuf_insert_char_at(eb->input, '\n', eb->pos); } } static void edit_insert_char(ic_env_t* env, editor_t* eb, char c) { editor_start_modify(eb); ssize_t nextpos = sbuf_insert_char_at( eb->input, c, eb->pos ); if (nextpos >= 0) eb->pos = nextpos; edit_auto_brace(env, eb, c); if (c=='\n') { editor_auto_indent(eb, "{", "}"); // todo: custom auto indent tokens? } edit_refresh_hint(env,eb); } //------------------------------------------------------------- // Help //------------------------------------------------------------- #include "editline_help.c" //------------------------------------------------------------- // History //------------------------------------------------------------- #include "editline_history.c" //------------------------------------------------------------- // Completion //------------------------------------------------------------- #include "editline_completion.c" //------------------------------------------------------------- // Edit line: main edit loop //------------------------------------------------------------- static char* edit_line( ic_env_t* env, const char* prompt_text ) { // set up an edit buffer editor_t eb; memset(&eb, 0, sizeof(eb)); eb.mem = env->mem; eb.input = sbuf_new(env->mem); eb.extra = sbuf_new(env->mem); eb.hint = sbuf_new(env->mem); eb.hint_help= sbuf_new(env->mem); eb.termw = term_get_width(env->term); eb.pos = 0; eb.cur_rows = 1; eb.cur_row = 0; eb.modified = false; eb.prompt_text = (prompt_text != NULL ? prompt_text : ""); eb.history_idx = 0; editstate_init(&eb.undo); editstate_init(&eb.redo); if (eb.input==NULL || eb.extra==NULL || eb.hint==NULL || eb.hint_help==NULL) { return NULL; } // caching if (!(env->no_highlight && env->no_bracematch)) { eb.attrs = attrbuf_new(env->mem); eb.attrs_extra = attrbuf_new(env->mem); } // show prompt edit_write_prompt(env, &eb, 0, false); // always a history entry for the current input history_push(env->history, ""); // process keys code_t c; // current key code while(true) { // read a character term_flush(env->term); if (env->hint_delay <= 0 || sbuf_len(eb.hint) == 0) { // blocking read c = tty_read(env->tty); } else { // timeout to display hint if (!tty_read_timeout(env->tty, env->hint_delay, &c)) { // timed-out if (sbuf_len(eb.hint) > 0) { // display hint edit_refresh(env, &eb); } c = tty_read(env->tty); } else { // clear the pending hint if we got input before the delay expired sbuf_clear(eb.hint); sbuf_clear(eb.hint_help); } } // update terminal in case of a resize if (tty_term_resize_event(env->tty)) { edit_resize(env,&eb); } // clear hint only after a potential resize (so resize row calculations are correct) const bool had_hint = (sbuf_len(eb.hint) > 0); sbuf_clear(eb.hint); sbuf_clear(eb.hint_help); // if the user tries to move into a hint with left-cursor or end, we complete it first if ((c == KEY_RIGHT || c == KEY_END) && had_hint) { edit_generate_completions(env, &eb, true); c = KEY_NONE; } // Operations that may return if (c == KEY_ENTER) { if (!env->singleline_only && eb.pos > 0 && sbuf_string(eb.input)[eb.pos-1] == env->multiline_eol && edit_pos_is_at_row_end(env,&eb)) { // replace line-continuation with newline edit_multiline_eol(env,&eb); } else { // otherwise done break; } } else if (c == KEY_CTRL_D) { if (eb.pos == 0 && editor_pos_is_at_end(&eb)) break; // ctrl+D on empty quits with NULL edit_delete_char(env,&eb); // otherwise it is like delete } else if (c == KEY_CTRL_C || c == KEY_EVENT_STOP) { break; // ctrl+C or STOP event quits with NULL } else if (c == KEY_ESC) { if (eb.pos == 0 && editor_pos_is_at_end(&eb)) break; // ESC on empty input returns with empty input edit_delete_all(env,&eb); // otherwise delete the current input // edit_delete_line(env,&eb); // otherwise delete the current line } else if (c == KEY_BELL /* ^G */) { edit_delete_all(env,&eb); break; // ctrl+G cancels (and returns empty input) } // Editing Operations else switch(c) { // events case KEY_EVENT_RESIZE: // not used edit_resize(env,&eb); break; case KEY_EVENT_AUTOTAB: edit_generate_completions(env, &eb, true); break; // completion, history, help, undo case KEY_TAB: case WITH_ALT('?'): edit_generate_completions(env,&eb,false); break; case KEY_CTRL_R: case KEY_CTRL_S: edit_history_search_with_current_word(env,&eb); break; case KEY_CTRL_P: edit_history_prev(env, &eb); break; case KEY_CTRL_N: edit_history_next(env, &eb); break; case KEY_CTRL_L: edit_clear_screen(env, &eb); break; case KEY_CTRL_Z: case WITH_CTRL('_'): edit_undo_restore(env, &eb); break; case KEY_CTRL_Y: edit_redo_restore(env, &eb); break; case KEY_F1: edit_show_help(env, &eb); break; // navigation case KEY_LEFT: case KEY_CTRL_B: edit_cursor_left(env,&eb); break; case KEY_RIGHT: case KEY_CTRL_F: if (eb.pos == sbuf_len(eb.input)) { edit_generate_completions( env, &eb, false ); } else { edit_cursor_right(env,&eb); } break; case KEY_UP: edit_cursor_row_up(env,&eb); break; case KEY_DOWN: edit_cursor_row_down(env,&eb); break; case KEY_HOME: case KEY_CTRL_A: edit_cursor_line_start(env,&eb); break; case KEY_END: case KEY_CTRL_E: edit_cursor_line_end(env,&eb); break; case KEY_CTRL_LEFT: case WITH_SHIFT(KEY_LEFT): case WITH_ALT('b'): edit_cursor_prev_word(env,&eb); break; case KEY_CTRL_RIGHT: case WITH_SHIFT(KEY_RIGHT): case WITH_ALT('f'): if (eb.pos == sbuf_len(eb.input)) { edit_generate_completions( env, &eb, false ); } else { edit_cursor_next_word(env,&eb); } break; case KEY_CTRL_HOME: case WITH_SHIFT(KEY_HOME): case KEY_PAGEUP: case WITH_ALT('<'): edit_cursor_to_start(env,&eb); break; case KEY_CTRL_END: case WITH_SHIFT(KEY_END): case KEY_PAGEDOWN: case WITH_ALT('>'): edit_cursor_to_end(env,&eb); break; case WITH_ALT('m'): edit_cursor_match_brace(env,&eb); break; // deletion case KEY_BACKSP: edit_backspace(env,&eb); break; case KEY_DEL: edit_delete_char(env,&eb); break; case WITH_ALT('d'): edit_delete_to_end_of_word(env,&eb); break; case KEY_CTRL_W: edit_delete_to_start_of_ws_word(env, &eb); break; case WITH_ALT(KEY_DEL): case WITH_ALT(KEY_BACKSP): edit_delete_to_start_of_word(env,&eb); break; case KEY_CTRL_U: edit_delete_to_start_of_line(env,&eb); break; case KEY_CTRL_K: edit_delete_to_end_of_line(env,&eb); break; case KEY_CTRL_T: edit_swap_char(env,&eb); break; // Editing case KEY_SHIFT_TAB: case KEY_LINEFEED: // '\n' (ctrl+J, shift+enter) if (!env->singleline_only) { edit_insert_char(env, &eb, '\n'); } break; default: { char chr; unicode_t uchr; if (code_is_ascii_char(c,&chr)) { edit_insert_char(env,&eb,chr); } else if (code_is_unicode(c, &uchr)) { edit_insert_unicode(env,&eb, uchr); } else { debug_msg( "edit: ignore code: 0x%04x\n", c); } break; } } } // goto end eb.pos = sbuf_len(eb.input); // refresh once more but without brace matching bool bm = env->no_bracematch; env->no_bracematch = true; edit_refresh(env,&eb); env->no_bracematch = bm; // save result char* res; if ((c == KEY_CTRL_D && sbuf_len(eb.input) == 0) || c == KEY_CTRL_C || c == KEY_EVENT_STOP) { res = NULL; } else if (!tty_is_utf8(env->tty)) { res = sbuf_strdup_from_utf8(eb.input); } else { res = sbuf_strdup(eb.input); } // update history history_update(env->history, sbuf_string(eb.input)); if (res == NULL || sbuf_len(eb.input) <= 1) { ic_history_remove_last(); } // no empty or single-char entries history_save(env->history); // free resources editstate_done(env->mem, &eb.undo); editstate_done(env->mem, &eb.redo); attrbuf_free(eb.attrs); attrbuf_free(eb.attrs_extra); sbuf_free(eb.input); sbuf_free(eb.extra); sbuf_free(eb.hint); sbuf_free(eb.hint_help); return res; }