packages feed

isocline-1.0.3: src/editline.c

/* ----------------------------------------------------------------------------
  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 <stdio.h>
#include <string.h>

#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;
}