#ifdef _WIN32
#include <windows.h>
#include <windowsx.h>
#else
#include "../swell/swell.h"
#endif
#include <stdlib.h>
#include <string.h>
#ifndef CURSES_INSTANCE
#define CURSES_INSTANCE ((win32CursesCtx *)m_cursesCtx)
#endif
#include "curses.h"
#include "eel_edit.h"
#include "../wdlutf8.h"
#include "../win32_utf8.h"
#include "../wdlcstring.h"
#include "../eel2/ns-eel-int.h"

int g_eel_editor_max_vis_suggestions = 50;
int g_eel_editor_flags;

EEL_Editor::EEL_Editor(void *cursesCtx) : WDL_CursesEditor(cursesCtx)
{
  m_suggestion_curline_comment_state=0;
  m_suggestion_x=m_suggestion_y=-1;
  m_case_sensitive=false;
  m_comment_str="//"; // todo IsWithinComment() or something?
  m_function_prefix = "function ";

  m_suggestion_hwnd=NULL;
  m_suggestion_hwnd_scroll=0;
  m_suggestion_hwnd_sel=-1;
  m_code_func_cache_lines=0;
  m_code_func_cache_time=0;
}

EEL_Editor::~EEL_Editor()
{
  if (m_suggestion_hwnd) DestroyWindow(m_suggestion_hwnd);
  m_code_func_cache.Empty(true,free);
}

#define sh_func_ontoken(x,y)

int EEL_Editor::namedTokenHighlight(const char *tokStart, int len, int state)
{
  if (len == 4 && !strnicmp(tokStart,"this",4)) return SYNTAX_KEYWORD;
  if (len == 7 && !strnicmp(tokStart,"_global",7)) return SYNTAX_KEYWORD;
  if (len == 5 && !strnicmp(tokStart,"local",5)) return SYNTAX_KEYWORD;
  if (len == 8 && !strnicmp(tokStart,"function",8)) return SYNTAX_KEYWORD;
  if (len == 6 && !strnicmp(tokStart,"static",6)) return SYNTAX_KEYWORD;
  if (len == 8 && !strnicmp(tokStart,"instance",8)) return SYNTAX_KEYWORD;
  if (len == 6 && !strnicmp(tokStart,"global",6)) return SYNTAX_KEYWORD;
  if (len == 7 && !strnicmp(tokStart,"globals",7)) return SYNTAX_KEYWORD;

  if (len == 5 && !strnicmp(tokStart,"while",5)) return SYNTAX_KEYWORD;
  if (len == 4 && !strnicmp(tokStart,"loop",4)) return SYNTAX_KEYWORD;

  if (len == 17 && !strnicmp(tokStart,"__denormal_likely",17)) return SYNTAX_FUNC;
  if (len == 19 && !strnicmp(tokStart,"__denormal_unlikely",19)) return SYNTAX_FUNC;
  if (len == 3 && !strnicmp(tokStart,"pow",3)) return SYNTAX_FUNC;

  char buf[512];
  lstrcpyn_safe(buf,tokStart,wdl_min(sizeof(buf),len+1));
  NSEEL_VMCTX vm = peek_want_VM_funcs() ? peek_get_VM(buf) : NULL;
  if (nseel_getFunctionByName((compileContext*)vm,buf,NULL)) return SYNTAX_FUNC;

  return A_NORMAL;
}

int EEL_Editor::parse_format_specifier(const char *fmt_in, int *var_offs, int *var_len)
{
  const char *fmt = fmt_in+1;
  *var_offs = 0;
  *var_len = 0;
  if (fmt_in[0] != '%') return 0; // passed a non-specifier

  while (*fmt)
  {
    const char c = *fmt++;

    if (c>0 && isalpha_safe(c)) 
    {
      return (int) (fmt - fmt_in);
    }

    if (c == '.' || c == '+' || c == '-' || c == ' ' || (c>='0' && c<='9')) 
    {
    }
    else if (c == '{')
    {
      if (*var_offs!=0) return 0; // already specified
      *var_offs = (int)(fmt-fmt_in);
      if (*fmt == '.' || (*fmt >= '0' && *fmt <= '9')) return 0; // symbol name can't start with 0-9 or .

      while (*fmt != '}')
      {
        if ((*fmt >= 'a' && *fmt <= 'z') ||
            (*fmt >= 'A' && *fmt <= 'Z') ||
            (*fmt >= '0' && *fmt <= '9') ||
            *fmt == '_' || *fmt == '.' || *fmt == '#')
        {
          fmt++;
        }
        else
        {
          return 0; // bad character in variable name
        }
      }
      *var_len = (int)((fmt-fmt_in) - *var_offs);
      fmt++;
    }
    else
    {
      break;
    }
  }
  return 0;
}


void EEL_Editor::draw_string(int *skipcnt, const char *str, int amt, int *attr, int newAttr, int comment_string_state)
{
  if (amt > 0 && comment_string_state=='"')
  {
    while (amt > 0 && *str)
    {
      const char *str_scan = str;
      int varpos,varlen,l=0;

      while (!l && *str_scan)
      {
        while (*str_scan && *str_scan != '%' && str_scan < str+amt) str_scan++;
        if (str_scan >= str+amt) break;

        l = parse_format_specifier(str_scan,&varpos,&varlen);
        if (!l && *str_scan)  if (*++str_scan == '%') str_scan++;
      }
      if (!*str_scan || str_scan >= str+amt) break; // allow default processing to happen if we reached the end of the string

      if (l > amt) l=amt;

      if (str_scan > str) 
      {
        const int sz=wdl_min((int)(str_scan-str),amt);
        draw_string_urlchk(skipcnt,str,sz,attr,newAttr);
        str += sz;
        amt -= sz;
      }

      {
        const int sz=(varlen>0) ? wdl_min(varpos,amt) : wdl_min(l,amt);
        if (sz>0) 
        {
          draw_string_internal(skipcnt,str,sz,attr,SYNTAX_HIGHLIGHT2);
          str += sz;
          amt -= sz;
        }
      }

      if (varlen>0) 
      {
        int sz = wdl_min(varlen,amt);
        if (sz>0)
        {
          draw_string_internal(skipcnt,str,sz,attr,*str == '#' ? SYNTAX_STRINGVAR : SYNTAX_HIGHLIGHT1);
          amt -= sz;
          str += sz;
        }

        sz = wdl_min(l - varpos - varlen, amt);
        if (sz>0)
        {
          draw_string_internal(skipcnt,str,sz,attr,SYNTAX_HIGHLIGHT2);
          amt-=sz;
          str+=sz;
        }
      }
    }
  }
  draw_string_urlchk(skipcnt,str,amt,attr,newAttr);
}

void EEL_Editor::draw_string_urlchk(int *skipcnt, const char *str, int amt, int *attr, int newAttr)
{
  if (amt > 0 && (newAttr == SYNTAX_COMMENT || newAttr == SYNTAX_STRING))
  {
    const char *sstr=str;
    while (amt > 0 && *str)
    {
      const char *str_scan = str;
      int l=0;
      
      while (l < 10 && *str_scan)
      {
        str_scan += l;
        l=0;
        while (*str_scan &&
               (strncmp(str_scan,"http://",7) || (sstr != str_scan && str_scan[-1] > 0 && isalnum_safe(str_scan[-1]))) &&
               str_scan < str+amt) str_scan++;
        if (!*str_scan || str_scan >= str+amt) break;
        while (str_scan[l] && str_scan[l] != ')' && str_scan[l] != '\"' && str_scan[l] != ')' && str_scan[l] != ' ' && str_scan[l] != '\t') l++;
      }
      if (!*str_scan || str_scan >= str+amt) break; // allow default processing to happen if we reached the end of the string
      
      if (l > amt) l=amt;
      
      if (str_scan > str)
      {
        const int sz=wdl_min((int)(str_scan-str),amt);
        draw_string_internal(skipcnt,str,sz,attr,newAttr);
        str += sz;
        amt -= sz;
      }
      
      const int sz=wdl_min(l,amt);
      if (sz>0)
      {
        draw_string_internal(skipcnt,str,sz,attr,SYNTAX_HIGHLIGHT1);
        str += sz;
        amt -= sz;
      }
    }
  }
  draw_string_internal(skipcnt,str,amt,attr,newAttr);
}
  
void EEL_Editor::draw_string_internal(int *skipcnt, const char *str, int amt, int *attr, int newAttr)
{
  // *skipcnt is in characters, amt is in bytes
  while (*skipcnt > 0 && amt > 0)
  {
    const int clen = wdl_utf8_parsechar(str,NULL);
    str += clen;
    amt -= clen;
    *skipcnt -= 1;
  }

  if (amt>0)
  {
    if (*attr != newAttr) 
    {
      attrset(newAttr);
      *attr = newAttr;
    }
    addnstr(str,amt);
  }
}


WDL_TypedBuf<char> EEL_Editor::s_draw_parentokenstack;
bool EEL_Editor::sh_draw_parenttokenstack_pop(char c)
{
  int sz = s_draw_parentokenstack.GetSize();
  while (--sz >= 0)
  {
    char tc = s_draw_parentokenstack.Get()[sz];
    if (tc == c)
    {
      s_draw_parentokenstack.Resize(sz,false);
      return false;
    }

    switch (c)
    {
      case '?': 
        // any open paren or semicolon is enough to cause error for ?:

      return true;
      case '(':
        if (tc == 'P') return true;
        if (tc == '[') return true;
      break;
      case '[':
        if (tc == 'P') return true;
        if (tc == '(') return true;
      break;
      case 'P':
        if (tc != '?' && tc != ';') return true;
      break;
    }
  }

  return true;
}

static int is_preproc_token(const char *tok)
{
  if (tok[0] == '<') return tok[1] == '?' ? 1 : 0;
  if (tok[0] == '?') return tok[1] == '>' ? -1 : 0;
  return 0;
}

bool EEL_Editor::sh_draw_parentokenstack_update(const char *tok, int toklen)
{
  if (toklen == 1)
  {
    switch (*tok)
    {
      case '(':
      case '[':
      case ';':
      case '?':
        s_draw_parentokenstack.Add(*tok);
      break;
      case ':': return sh_draw_parenttokenstack_pop('?');
      case ')': return sh_draw_parenttokenstack_pop('(');
      case ']': return sh_draw_parenttokenstack_pop('[');
    }
  }
  else if (toklen == 2)
  {
    switch (is_preproc_token(tok))
    {
      case 1: s_draw_parentokenstack.Add('P'); break;
      case -1: return sh_draw_parenttokenstack_pop('P');
    }
  }
  return false;
}


void EEL_Editor::draw_line_highlight(int y, const char *p, int *c_comment_state, int line_n)
{
  if (line_n == m_curs_y)
    m_suggestion_curline_comment_state = *c_comment_state;
  int last_attr = A_NORMAL;
  attrset(last_attr);
  move(y, 0);
  int rv = do_draw_line(p, c_comment_state, last_attr);
  attrset(rv< 0 ? SYNTAX_ERROR : A_NORMAL);
  clrtoeol();
  if (rv < 0) attrset(A_NORMAL);
}

#define STATE_PREPROC                         (0x20000000)
#define STATE_PREPROC_SINGLELINEMODE          (0x10000000)
#define STATE_PREPROC_MASK                    (0x3ff00000)
#define STATE_PREPROC_ENCODE_OLD(x) (((x)<<20)&0x0ff00000)
#define STATE_PREPROC_DECODE_OLD(x)      (((x)&0x0ff00000)>>20)


int EEL_Editor::do_draw_line(const char *p, int *c_comment_state, int last_attr)
{
  if (!(*c_comment_state & STATE_PREPROC) && is_code_start_line(p))
  {
    *c_comment_state=0;
    s_draw_parentokenstack.Resize(0,false);
  }

  int skipcnt = m_offs_x;
  int ignoreSyntaxState = 0;

  const int initial_state = *c_comment_state;
  if (!(initial_state & STATE_PREPROC))
    ignoreSyntaxState = overrideSyntaxDrawingForLine(&skipcnt, &p, c_comment_state, &last_attr);

  if (ignoreSyntaxState>0)
  {
    int len = (int)strlen(p);
    draw_string(&skipcnt,p,len,&last_attr,ignoreSyntaxState==100 ? SYNTAX_ERROR : 
        ignoreSyntaxState==2 ? SYNTAX_COMMENT : A_NORMAL);
    return len-m_offs_x < COLS;
  }
  if (ignoreSyntaxState<0 && initial_state == STATE_BEFORE_CODE) *c_comment_state=0;

  if ((initial_state & STATE_PREPROC) && (initial_state & STATE_PREPROC_SINGLELINEMODE))
    ignoreSyntaxState = -1;

  // syntax highlighting
  const char *endptr = p+strlen(p);
  const char *tok;
  const char *lp = p;
  int toklen=0;
  int last_comment_state=*c_comment_state & ~STATE_PREPROC_MASK;
  while (NULL != (tok = sh_tokenize(&p,endptr,&toklen,c_comment_state)) || lp < endptr)
  {
    if (initial_state == STATE_BEFORE_CODE && !(*c_comment_state & STATE_PREPROC) && !ignoreSyntaxState)
      *c_comment_state = initial_state;
    if (tok && *tok < 0 && toklen == 1)
    {
      while (tok[toklen] < 0) {p++; toklen++; } // utf-8 skip
    }
    if (ignoreSyntaxState == -2 && tok && tok[0] == '/' && toklen > 1 && (tok[1] == '*' || tok[1] == '/'))
    {
      p -= toklen-1;
      toklen = 1;
    }

    bool is_pp = toklen == 2 && is_preproc_token(tok);
    if (!is_pp && (last_comment_state>0 || *c_comment_state == STATE_BEFORE_CODE)) // if in a multi-line string or comment
    {
      // draw empty space between lp and p as a string. in this case, tok/toklen includes our string, so we quickly finish after
      draw_string(&skipcnt,lp,(int)(p-lp),&last_attr,
          last_comment_state == 1 ||
          last_comment_state == STATE_BEFORE_CODE ||
          *c_comment_state == STATE_BEFORE_CODE ? SYNTAX_COMMENT : SYNTAX_STRING,
          last_comment_state);
      last_comment_state=0;
      lp = p;
      continue;
    }
    sh_func_ontoken(tok,toklen);

    // draw empty space between lp and tok/endptr as normal
    const char *adv_to = tok ? tok : endptr;
    if (adv_to > lp) draw_string(&skipcnt,lp,(int)(adv_to-lp),&last_attr, A_NORMAL);

    if (adv_to >= endptr) break;

    last_comment_state=0;
    lp = p;
    if (adv_to == p) continue;

    // draw token
    int attr = A_NORMAL;
    int err_left=0;
    int err_right=0;
    int start_of_tok = 0;

    if (tok[0] == '/' && toklen > 1 && (tok[1] == '*' || tok[1] == '/'))
    {
      attr = SYNTAX_COMMENT;
    }
    else if (tok[0] > 0 && (isalpha_safe(tok[0]) || tok[0] == '_' || (ignoreSyntaxState != -2 && tok[0] == '#')))
    {
      if (ignoreSyntaxState == -2) goto dostr;
      int def_attr = A_NORMAL;
      bool isf=true;
      if (tok[0] == '#')
        def_attr = SYNTAX_STRINGVAR;
      while (toklen > 0)
      {
        // divide up by .s, if any, unless we're a string
        int this_len=def_attr == SYNTAX_STRINGVAR ? toklen : 0;
        while (this_len < toklen && tok[this_len] != '.') this_len++;
        if (this_len > 0)
        {
          int attr=isf?namedTokenHighlight(tok,this_len,*c_comment_state):def_attr;
          if (isf && attr == A_NORMAL)
          {
            int ntok_len=0, cc = *c_comment_state;
            const char *pp=lp,*ntok = sh_tokenize(&pp,endptr,&ntok_len,&cc);
            if (ntok && ntok_len>0 && *ntok == '(') def_attr = attr = SYNTAX_FUNC2;
          }

          draw_string(&skipcnt,tok,this_len,&last_attr,attr==A_NORMAL?def_attr:attr);
          tok += this_len;
          toklen -= this_len;
        }
        if (toklen > 0)
        {
          draw_string(&skipcnt,tok,1,&last_attr,SYNTAX_HIGHLIGHT1);
          tok++;
          toklen--;
        }
        isf=false;
      }
      continue;
    }
    else if (tok[0] == '.' ||
             (tok[0] >= '0' && tok[0] <= '9') ||
             (toklen > 1 && tok[0] == '$' && (tok[1] == 'x' || tok[1]=='X')))
    {
      attr = SYNTAX_HIGHLIGHT2;

      int x=1,mode=0;
      if (tok[0] == '.') mode=1;
      else if (toklen > 1 && (tok[0] == '$' || tok[0] == '0') && (tok[1] == 'x' || tok[1] == 'X')) { mode=2;  x++; }
      for(;x<toklen;x++)
      {
        if (tok[x] == '.'  && !mode) mode=1;
        else if (tok[x] < '0' || tok[x] > '9') 
        {
          if (mode != 2 || ((tok[x] < 'a' || tok[x] > 'f') && (tok[x] < 'A' || tok[x] > 'F')))
                break;
        }
      }
      if (x<toklen && ignoreSyntaxState != -2) err_right=toklen-x;
    }
    else if (ignoreSyntaxState == -2)
    {
      attr = SYNTAX_HIGHLIGHT1;
    }
    else if (tok[0] == '\'' || tok[0] == '\"')
    {
      start_of_tok = tok[0];
      attr = SYNTAX_STRING;
    }
    else if (tok[0] == '$')
    {
      attr = SYNTAX_HIGHLIGHT2;

      if (toklen >= 3 && !strnicmp(tok,"$pi",3)) err_right = toklen - 3;
      else if (toklen >= 2 && !strnicmp(tok,"$e",2)) err_right = toklen - 2;
      else if (toklen >= 4 && !strnicmp(tok,"$phi",4)) err_right = toklen - 4;
      else if (toklen == 4 && tok[1] == '\'' && tok[3] == '\'') { }
      else if (toklen > 1 && tok[1] == '~')
      {
        int x;
        for(x=2;x<toklen;x++) if (tok[x] < '0' || tok[x] > '9') break;
        if (x<toklen) err_right=toklen-x;
      }
      else err_right = toklen;
    }
    else if (ignoreSyntaxState==-1 && (tok[0] == '{' || tok[0] == '}'))
    {
      attr = SYNTAX_HIGHLIGHT1;
    }
    else if (is_pp)
    {
      if (tok[0] == '?')
        last_comment_state = *c_comment_state;
      if (sh_draw_parentokenstack_update(tok,toklen)) attr = SYNTAX_ERROR;
      else attr = SYNTAX_KEYWORD;
    }
    else
    {
      const char *h="()+*-=/,|&%;!<>?:^!~[]";
      while (*h && *h != tok[0]) h++;
      if (*h)
      {
        if (*c_comment_state != STATE_BEFORE_CODE && sh_draw_parentokenstack_update(tok,toklen))
          attr = SYNTAX_ERROR;
        else
          attr = SYNTAX_HIGHLIGHT1;
      }
      else 
      {
        err_left=1;
        if (tok[0] < 0) while (err_left < toklen && tok[err_left]<0) err_left++; // utf-8 skip
      }
    }
dostr:

    if (ignoreSyntaxState) err_left = err_right = 0;

    if (err_left > 0) 
    {
      if (err_left > toklen) err_left=toklen;
      draw_string(&skipcnt,tok,err_left,&last_attr,SYNTAX_ERROR);
      tok+=err_left;
      toklen -= err_left;
    }
    if (err_right > toklen) err_right=toklen;

    draw_string(&skipcnt, tok, toklen - err_right, &last_attr, attr, start_of_tok);

    if (err_right > 0)
      draw_string(&skipcnt,tok+toklen-err_right,err_right,&last_attr,SYNTAX_ERROR);

    if (tok[0] == '>' && (ignoreSyntaxState == -2 || ignoreSyntaxState == -1))
    {
      draw_string(&skipcnt,p,strlen(p),&last_attr,ignoreSyntaxState==2 ? SYNTAX_COMMENT : A_NORMAL);
      break;
    }
  }
  if (ignoreSyntaxState<0)
  {
    if (initial_state == STATE_BEFORE_CODE ||
        ((initial_state & STATE_PREPROC) &&
         (initial_state & STATE_PREPROC_SINGLELINEMODE))
        )
    {
      if (*c_comment_state & STATE_PREPROC)
        *c_comment_state |= STATE_PREPROC_SINGLELINEMODE;
      else
        *c_comment_state=STATE_BEFORE_CODE;
    }
  }
  return 1;
}

int EEL_Editor::GetCommentStateForLineStart(int line)
{
  if (m_write_leading_tabs<=0) m_indent_size=2;

  const bool uses_code_start_lines = !!is_code_start_line(NULL);
  int state=uses_code_start_lines ? STATE_BEFORE_CODE : 0;

  s_draw_parentokenstack.Resize(0,false);

  int state_ret = state;
  bool need_code = uses_code_start_lines; // we're also searching for tabsize:
  for (int x=0;x<line || need_code;x++)
  {
    WDL_FastString *t = m_text.Get(x);
    if (!t) break;

    const char *p = t->Get();
    if (state == STATE_BEFORE_CODE && !strnicmp(p,"tabsize:",8))
    {
      int a = atoi(p+8);
      if (a>0 && a < 32) m_indent_size = a;
    }
    if (!(state & STATE_PREPROC) && uses_code_start_lines && is_code_start_line(p))
    {
      need_code = false;
      if (x>=line) break;
      s_draw_parentokenstack.Resize(0,false);
      state=0; 
    }
    else
    {
      const int ll=t?t->GetLength():0;
      const char *endp = p+ll;
      int toklen;
      const char *tok;
      const int initial_state = state;
      // to be maximally correct here with the preprocessor here, we need to call overrideSyntaxDrawingForLine()
      // without drawing to detect special lines, but meh corner cases
      while (NULL != (tok=sh_tokenize(&p,endp,&toklen,&state))) // eat all tokens, updating state
      {
        if (initial_state == STATE_BEFORE_CODE && !(state & STATE_PREPROC))
          state = STATE_BEFORE_CODE;
        if (x<line && (state != STATE_BEFORE_CODE || (toklen == 2 && is_preproc_token(tok))))
        {
          sh_func_ontoken(tok,toklen);
          sh_draw_parentokenstack_update(tok,toklen);
        }
      }
    }
    if (x<line) state_ret = state;
  }
  return state_ret;
}

const char *EEL_Editor::sh_tokenize(const char **ptr, const char *endptr, int *lenOut, int *state)
{
  int in_preproc = 0, prev_state = state ? *state : 0;
  if (prev_state & STATE_PREPROC)
  {
    in_preproc = prev_state & STATE_PREPROC_MASK;
    *state &= ~STATE_PREPROC_MASK;
  }
  const char *p = nseel_simple_tokenizer(ptr, endptr, lenOut, state);
  if (in_preproc) *state |= in_preproc;

  if (!p || !state) return p;

  if (in_preproc)
  {
    if (*lenOut >= 1 && p+1 < endptr && is_preproc_token(p) < 0)
    {
      *ptr += 2 - *lenOut;
      *lenOut = 2;
      *state = STATE_PREPROC_DECODE_OLD(prev_state); // caller will have to be aware and highlight this token correctly
    }
    else
    {
      for (int x = 0; x < *lenOut - 1; x ++)
        if (is_preproc_token(p+x) < 0)
        {
          int adj = x - *lenOut;
          *ptr += adj;
          *lenOut += adj;
          break;
        }
    }
  }
  else
  {
    if (*state == STATE_BEFORE_CODE && *lenOut == 4 && p + 7 < endptr && !strnicmp(p,"http://",7))
    {
      int nl = 7;
      while (p+nl < endptr && p[nl] && p[nl] != ' ' && p[nl] != '\t') nl++;
      *ptr += nl-*lenOut;
      *lenOut = nl;
    }

    if (*lenOut >= 1 && p+1 < endptr && is_preproc_token(p) > 0)
    {
      *ptr += 2 - *lenOut;
      *lenOut = 2;
      *state = STATE_PREPROC_ENCODE_OLD(prev_state) | STATE_PREPROC;
    }
    else
    {
      for (int x = 0; x < *lenOut - 1; x ++)
        if (is_preproc_token(p+x) > 0)
        {
          int adj = x - *lenOut;
          *ptr += adj;
          *lenOut += adj;
          if (p[0] == '"' || p[0] == '\'') *state = p[0];
          break;
        }
    }
  }
  return p;
}


bool EEL_Editor::LineCanAffectOtherLines(const char *txt, int spos, int slen) // if multiline comment etc
{
  const char *special_start = txt + spos;
  const char *special_end = txt + spos + slen;
  while (*txt)
  {
    if (txt >= special_start-1 && txt < special_end)
    {
      const char c = txt[0];
      if (c == '*' && txt[1] == '/') return true;
      if (c == '/' && (txt[1] == '/' || txt[1] == '*')) return true;
      if (c == '\\' && (txt[1] == '\"' || txt[1] == '\'')) return true;
      if (is_preproc_token(txt)) return true;

      if (txt >= special_start)
      {
        if (c == '\"' || c == '\'') return true;
        if (c == '(' || c == '[' || c == ')' || c == ']' || c == ':' || c == ';' || c == '?') return true;
      }
    }
    txt++;
  }
  return false;
}


struct eel_sh_token
{
  int line, col, end_col; 
  unsigned int data; // packed char for token type, plus 24 bits for linecnt (0=single line, 1=extra line, etc)

  eel_sh_token(int _line, int _col, int toklen, unsigned char c)
  {
    line = _line;
    col = _col;
    end_col = col + toklen;
    data = c;
  }
  ~eel_sh_token() { }

  void add_linecnt(int endcol) { data += 256; end_col = endcol; }
  int get_linecnt() const { return (data >> 8); }
  char get_c() const { return (char) (data & 255); }

  bool is_comment() const {
    return get_c() == '/' && (get_linecnt() || end_col>col+1);
  };
};

static int eel_sh_get_token_for_pos(const WDL_TypedBuf<eel_sh_token> *toklist, int line, int col, bool *is_after)
{
  const int sz = toklist->GetSize();
  int x;
  for (x=0; x < sz; x ++)
  {
    const eel_sh_token *tok = toklist->Get()+x;
    const int first_line = tok->line;
    const int last_line = first_line+tok->get_linecnt(); // last affected line (usually same as first)

    if (last_line >= line) // if this token doesn't end before the line we care about
    {
      // check to see if the token starts after our position
      if (first_line > line || (first_line == line && tok->col > col)) break; 

      // token started before line/col, see if it ends after line/col
      if (last_line > line || tok->end_col > col) 
      {
        // direct hit
        *is_after = false;
        return x;
      } 
    }
  }
  *is_after = true;
  return x-1;
}

static void eel_sh_generate_token_list(const WDL_PtrList<WDL_FastString> *lines, WDL_TypedBuf<eel_sh_token> *toklist, int start_line, EEL_Editor *editor)
{
  toklist->Resize(0,false);
  int state=0;
  int l;
  int end_line = lines->GetSize();
  if (editor->is_code_start_line(NULL))
  {
    for (l = start_line; l < end_line; l ++)
    {
      WDL_FastString *s = lines->Get(l);
      if (s && editor->is_code_start_line(s->Get()))
      {
        end_line = l;
        break;
      }
    }
    for (; start_line >= 0; start_line--)
    {
      WDL_FastString *s = lines->Get(start_line);
      if (s && editor->is_code_start_line(s->Get())) break;
    }
    if (start_line < 0) return; // before any code
  
    start_line++;
  }
  else
  {
    start_line = 0;
  }
  

  int last_state_save = 0;
  for (l=start_line;l<end_line;l++)
  {
    WDL_FastString *t = lines->Get(l);
    const int ll = t?t->GetLength():0;
    const char *start_p = t?t->Get():"";
    const char *p = start_p;
    const char *endp = start_p+ll;
   
    const char *tok;
    int last_state=state & ~STATE_PREPROC;
    int toklen;
    while (NULL != (tok=editor->sh_tokenize(&p,endp,&toklen,&state))||last_state)
    {
      if (tok && toklen == 2 && is_preproc_token(tok) > 0)
      {
        bool ok = true;
        for (int x = toklist->GetSize()-1; x>=0; x--)
        {
          char c = toklist->Get()[x].get_c();
          if (c == 'p') break; // ?>
          if (c == 'P') { ok = false; break; } // <?
        }
        if (WDL_NORMALLY(ok))
        {
          eel_sh_token t(l,(int)(tok-start_p),2,'P');
          toklist->Add(t);
          last_state_save = STATE_PREPROC_DECODE_OLD(state);
          last_state = 0;
          continue;
        }
      }
      if (tok && toklen == 2 && is_preproc_token(tok) < 0)
      {
        bool ok = false;
        for (int x = toklist->GetSize()-1; x>=0; x--)
        {
          char c = toklist->Get()[x].get_c();
          if (c == 'p') break; // ?>
          if (c == 'P') { ok = true; break; } // <?
        }
        if (WDL_NORMALLY(ok))
        {
          eel_sh_token t(l,(int)(tok-start_p),2,'p');
          toklist->Add(t);
          last_state = last_state_save;
          last_state_save = 0;
          continue;
        }
      }
      if (last_state == '\'' || last_state == '"' || last_state==1)
      {
        const int sz=toklist->GetSize();
        // update last token to include this data
        if (sz)
        {
          char c = last_state == 1 ? '/' : last_state;
          if (toklist->Get()[sz-1].get_c() == c)
            toklist->Get()[sz-1].add_linecnt((int) ((tok ? p:endp) - start_p));
          else if (tok)
          {
            // usually indicates continuation after a preprocessor block
            eel_sh_token t(l,(int)(tok-start_p),toklen,c);
            toklist->Add(t);
          }
        }
      }
      else
      {
        if (tok) switch (tok[0])
        {
          case '{':
          case '}':
          case '?':
          case ':':
          case ';':
          case '(':
          case '[':
          case ')':
          case ']':
          case '\'':
          case '"':
          case '/': // comment
            {
              eel_sh_token t(l,(int)(tok-start_p),toklen,tok[0]);
              toklist->Add(t);
            }
          break;
        }
      }
      last_state=0;
    }
  }
}

static bool eel_sh_get_matching_pos_for_pos(WDL_PtrList<WDL_FastString> *text, int curx, int cury, int *newx, int *newy, const char **errmsg, EEL_Editor *editor)
{
  static WDL_TypedBuf<eel_sh_token> toklist;
  eel_sh_generate_token_list(text,&toklist, cury,editor);
  bool is_after;
  const int hit_tokidx = eel_sh_get_token_for_pos(&toklist, cury, curx, &is_after);
  const eel_sh_token *hit_tok = hit_tokidx >= 0 ? toklist.Get() + hit_tokidx : NULL;

  if (!is_after && hit_tok && (hit_tok->get_c() == '"' || hit_tok->get_c() == '\'' || hit_tok->is_comment()))
  {
    eel_sh_token tok = *hit_tok; // save a copy, toklist might get destroyed recursively here
    hit_tok = &tok;

    //if (tok.get_c() == '"')
    {
      // the user could be editing code in code, tokenize it and see if we can make sense of it
      WDL_FastString start, end;
      WDL_PtrList<WDL_FastString> tmplist;
      WDL_FastString *s = text->Get(tok.line);
      if (s && s->GetLength() > tok.col+1)
      {
        int maxl = tok.get_linecnt()>0 ? 0 : tok.end_col - tok.col - 2;
        start.Set(s->Get() + tok.col+1, maxl);
      }
      tmplist.Add(&start);
      const int linecnt = tok.get_linecnt();
      if (linecnt>0)
      {
        for (int a=1; a < linecnt; a ++)
        {
          s = text->Get(tok.line + a);
          if (s) tmplist.Add(s);
        }
        s = text->Get(tok.line + linecnt);
        if (s)
        {
          if (tok.end_col>1) end.Set(s->Get(), tok.end_col-1);
          tmplist.Add(&end);
        }
      }

      int lx = curx, ly = cury - tok.line;
      if (cury == tok.line) lx -= (tok.col+1);

      // this will destroy the token 
      if (eel_sh_get_matching_pos_for_pos(&tmplist, lx, ly, newx, newy, errmsg, editor))
      {
        *newy += tok.line;
        if (cury == tok.line) *newx += tok.col + 1;
        return true;
      }
    }

    // if within a string or comment, move to start, unless already at start, move to end
    if (cury == hit_tok->line && curx == hit_tok->col)
    {
      *newx=hit_tok->end_col-1;
      *newy=hit_tok->line + hit_tok->get_linecnt();
    }
    else
    {
      *newx=hit_tok->col;
      *newy=hit_tok->line;
    }
    return true;
  }

  if (!hit_tok) return false;

  const int toksz=toklist.GetSize();
  int tokpos = hit_tokidx;
  int pc1=0,pc2=0; // (, [ count
  int pc3=0; // : or ? count depending on mode
  int dir=-1, mode=0;  // default to scan to previous [( or <?
  if (!is_after)
  {
    switch (hit_tok->get_c())
    {
      case '(': mode=1; dir=1; break;
      case '[': mode=2; dir=1; break;
      case ')': mode=3; dir=-1; break;
      case ']': mode=4; dir=-1; break;
      case '?': mode=5; dir=1; break;
      case ':': mode=6; break;
      case ';': mode=7; break;
      case 'P': mode=8; dir=1; break;
      case 'p': mode=9; dir=-1; break;
    }
    // if hit a token, exclude this token from scanning
    tokpos += dir;
  }

  while (tokpos>=0 && tokpos<toksz)
  {
    const eel_sh_token *tok = toklist.Get() + tokpos;
    const char this_c = tok->get_c();
    if (mode != 8 && mode != 9)
    {
      if (dir < 0)
      {
        if (this_c == 'P') // <?
        {
          if (mode == 0)
          {
            *newx=tok->col;
            *newy=tok->line;
            return true;
          }
          break;
        }
        if (this_c == 'p')
        {
          while (--tokpos >= 0 && toklist.Get()[tokpos].get_c() != 'P');
          tokpos--;
          continue;
        }
      }
      else
      {
        if (this_c == 'p') break; // ?>
        if (this_c == 'P')
        {
          while (++tokpos < toksz && toklist.Get()[tokpos].get_c() != 'p');
          tokpos++;
          continue;
        }
      }
    }
    if ((!pc1 && !pc2) || mode == 8 || mode == 9)
    {
      bool match=false, want_abort=false;
      switch (mode)
      {
        case 0: match = this_c == '(' || this_c == '['; break;
        case 1: match = this_c == ')'; break;
        case 2: match = this_c == ']'; break;
        case 3: match = this_c == '('; break;
        case 4: match = this_c == '['; break;
        case 5: 
          // scan forward to nearest : or ;
          if (this_c == '?') pc3++;
          else if (this_c == ':')
          {
            if (pc3>0) pc3--;
            else match=true;
          }
          else if (this_c == ';') match=true;
          else if (this_c == ')' || this_c == ']') 
          {
            want_abort=true; // if you have "(x<y?z)", don't match for the ?
          }
        break;
        case 6:  // scanning back from : to ?, if any
        case 7:  // semicolon searches same as colon, effectively
          if (this_c == ':') pc3++;
          else if (this_c == '?')
          {
            if (pc3>0) pc3--;
            else match = true;
          }
          else if (this_c == ';' || this_c == '(' || this_c == '[') 
          {
            want_abort=true;
          }
        break;
        case 8: match = this_c == 'p'; break;
        case 9: match = this_c == 'P'; break;
      }

      if (want_abort) break;
      if (match)
      {
        *newx=tok->col;
        *newy=tok->line;
        return true;
      }
    }
    switch (this_c)
    {
      case '[': pc2++; break;
      case ']': pc2--; break;
      case '(': pc1++; break;
      case ')': pc1--; break;
    }
    tokpos+=dir;
  }

  if (errmsg)
  {
    if (!mode) *errmsg = "Could not find previous [ or (";
    else if (mode == 1) *errmsg = "Could not find matching )";
    else if (mode == 2) *errmsg = "Could not find matching ]";
    else if (mode == 3) *errmsg = "Could not find matching (";
    else if (mode == 4) *errmsg = "Could not find matching [";
    else if (mode == 5) *errmsg = "Could not find matching : or ; for ?";
    else if (mode == 6) *errmsg = "Could not find matching ? for :";
    else if (mode == 7) *errmsg = "Could not find matching ? for ;";
  }
  return false;
}


void EEL_Editor::doParenMatching()
{
  WDL_FastString *curstr;
  const char *errmsg = "";
  if (NULL != (curstr=m_text.Get(m_curs_y)))
  {
    int bytex = WDL_utf8_charpos_to_bytepos(curstr->Get(),m_curs_x);
    if (bytex >= curstr->GetLength()) bytex=curstr->GetLength()-1;
    if (bytex<0) bytex = 0;

    int new_x,new_y;
    if (eel_sh_get_matching_pos_for_pos(&m_text, bytex,m_curs_y,&new_x,&new_y,&errmsg,this))
    {
      curstr = m_text.Get(new_y);
      if (curstr) new_x = WDL_utf8_bytepos_to_charpos(curstr->Get(),new_x);

      m_curs_x=new_x;
      m_curs_y=new_y;
      m_want_x=-1;
      draw();
      setCursor(1);
    }
    else if (errmsg[0])
    {
      draw_message(errmsg);
      setCursor(0);
    }
  }
}

static int word_len(const char *p)
{
  int l = 0;
  if (*p >= 'a' && *p <='z')
  {
    l++;
    // lowercase word
    while (p[l] && p[l] != '_' && p[l] != '.' && !(p[l] >= 'A' && p[l] <='Z')) l++;
  }
  else if (*p >= 'A' && *p <= 'Z')
  {
    if (!strcmp(p,"MIDI")) return 4;
    l++;
    if (p[l] >= 'A'  && p[l] <='Z') // UPPERCASE word
      while (p[l] && p[l] != '_' && p[l] != '.' && !(p[l] >= 'a' && p[l] <='z')) l++;
    else // Titlecase word
      while (p[l] && p[l] != '_' && p[l] != '.' && !(p[l] >= 'A' && p[l] <='Z')) l++;
  }
  return l;
}

static int search_str_partial(const char *str, int reflen, const char *word, int wordlen)
{
  // find the longest leading segment of word in str
  int best_len = 0;
  for (int y = 0; y < reflen; y ++)
  {
    while (y < reflen && !strnicmp(str+y,word,best_len+1))
    {
      if (++best_len >= wordlen) return best_len;
      reflen--;
    }
  }
  return best_len;
}

static int fuzzy_match2(const char *codestr, int codelen, const char *refstr, int reflen)
{
  // codestr is user-typed, refstr is the reference function name
  // our APIs are gfx_blah_blah or TrackBlahBlah so breaking the API up by words
  // and searching the user-entered code should be effective
  int lendiff = reflen - codelen;
  if (lendiff < 0) lendiff = -lendiff;

  const char *word = refstr;
  int score = 0;
  for (;;)
  {
    while (*word == '_' || *word == '.') word++;
    const int wordlen = word_len(word);
    if (!wordlen) break;
    char word_buf[128];
    lstrcpyn_safe(word_buf,word,wdl_min(wordlen+1,sizeof(word_buf)));

    if (WDL_stristr(refstr,word_buf)==word)
    {
      int max_match_len = search_str_partial(codestr,codelen,word,wordlen);
      if (max_match_len < 2 && wordlen == 5 && !strnicmp(word,"Count",5))
      {
        max_match_len = search_str_partial(codestr,codelen,"Num",3);
      }
      if (max_match_len > (wordlen <= 2 ? 1 : 2))
        score += max_match_len;
    }
    word += wordlen;
  }

  if (!score) return 0;

  return score * 1000 + 1000 - wdl_clamp(lendiff*2,0,200);
}

int EEL_Editor::fuzzy_match(const char *codestr, const char *refstr)
{
  const int codestr_len = (int)strlen(codestr), refstr_len = (int)strlen(refstr);
  if (refstr_len >= codestr_len && !strnicmp(codestr,refstr,codestr_len)) return 1000000000;
  int score1 = fuzzy_match2(refstr,refstr_len,codestr,codestr_len);
  int score2 = fuzzy_match2(codestr,codestr_len,refstr,refstr_len);
  if (score2 > score1) return score2 | 1;
  return score1&~1;
}

static int eeledit_varenumfunc(const char *name, EEL_F *val, void *ctx)
{
  void **parms = (void **)ctx;
  int score = ((EEL_Editor*)parms[2])->fuzzy_match((const char *)parms[1], name);
  if (score > 0) ((suggested_matchlist*)parms[0])->add(name,score,suggested_matchlist::MODE_VAR);
  return 1;
}

void EEL_Editor::get_suggested_token_names(const char *fname, int chkmask, suggested_matchlist *list)
{
  int x;
  if (chkmask & (KEYWORD_MASK_BUILTIN_FUNC|KEYWORD_MASK_USER_VAR))
  {
    peek_lock();
    NSEEL_VMCTX vm = peek_get_VM("");
    compileContext *fvm = vm && peek_want_VM_funcs() ? (compileContext*)vm : NULL;
    if (chkmask&KEYWORD_MASK_BUILTIN_FUNC) for (x=0;;x++)
    {
      functionType *p = nseel_enumFunctions(fvm,x);
      if (!p) break;
      int score = fuzzy_match(fname,p->name);
      if (score>0) list->add(p->name,score);
    }
    if (vm && (chkmask&KEYWORD_MASK_USER_VAR))
    {
      const void *parms[3] = { list, fname, this };
      NSEEL_VM_enumallvars(vm, eeledit_varenumfunc, parms);
    }
    peek_unlock();
  }

  if (chkmask & KEYWORD_MASK_USER_FUNC)
  {
    ensure_code_func_cache_valid();
    for (int x=0;x< m_code_func_cache.GetSize();x++)
    {
      const char *k = m_code_func_cache.Get(x) + 4;
      if (WDL_NORMALLY(k))
      {
        int score = fuzzy_match(fname,k);
        if (score > 0) list->add(k,score,suggested_matchlist::MODE_USERFUNC);
      }
    }
  }
}

int EEL_Editor::peek_get_token_info(const char *name, char *sstr, size_t sstr_sz, int chkmask, int ignoreline)
{
  if (chkmask&KEYWORD_MASK_USER_FUNC)
  {
    ensure_code_func_cache_valid();
    for (int i = 0; i < m_code_func_cache.GetSize(); i ++)
    {
      const char *cacheptr = m_code_func_cache.Get(i);
      const char *nameptr = cacheptr + sizeof(int);
      if (!(m_case_sensitive ? strcmp(nameptr, name):stricmp(nameptr,name)) &&
          *(int *)cacheptr != ignoreline)
      {
        const char *parms = nameptr+strlen(nameptr)+1;
        const char *trail = parms+strlen(parms)+1;
        snprintf(sstr,sstr_sz,"%s%s%s%s",nameptr,parms,*trail?" " :"",trail);
        return 4;
      }
    }
  }

  if ((chkmask & KEYWORD_MASK_USER_VAR) && name[0] == '#')
  {
    char tmp[256];
    int v = peek_get_named_string_value(name+1,tmp,sizeof(tmp));
    if (v>=0)
    {
      snprintf(sstr,sstr_sz,"%s(%d)=\"%s\"",name,v,tmp);
      return KEYWORD_MASK_USER_VAR;
    }
  }

  if (chkmask & (KEYWORD_MASK_BUILTIN_FUNC|KEYWORD_MASK_USER_VAR))
  {
    int rv = 0;
    peek_lock();
    NSEEL_VMCTX vm = peek_want_VM_funcs() ? peek_get_VM("") : NULL;
    functionType *f = (chkmask&KEYWORD_MASK_BUILTIN_FUNC) ? nseel_getFunctionByName((compileContext*)vm,name,NULL) : NULL;
    functionType tmp;
    if (!f && (chkmask&KEYWORD_MASK_BUILTIN_FUNC) && !stricmp(name,"pow"))
    {
      memset(&tmp,0,sizeof(tmp));
      tmp.name = "pow";
      tmp.nParams = 2;
      f = &tmp;
    }
    double v;
    if (f)
    {
      snprintf(sstr,sstr_sz,"'%s' is a function that requires %d parameters", f->name,f->nParams&0xff);
      rv = KEYWORD_MASK_BUILTIN_FUNC;
    }
    else if (chkmask & KEYWORD_MASK_USER_VAR)
    {
      if (!vm) vm = peek_get_VM("");
      EEL_F *vptr=NSEEL_VM_getvar(vm,name);
      if (vptr)
      {
        v = *vptr;
        rv = KEYWORD_MASK_USER_VAR;
      }
    }
    peek_unlock();

    if (rv == KEYWORD_MASK_USER_VAR)
    {
      int good_len=-1;
      snprintf(sstr,sstr_sz,"%s=%.14f",name,v);
      WDL_remove_trailing_decimal_zeros(sstr,2);

      if (v > -1.0 && v < NSEEL_RAM_ITEMSPERBLOCK*NSEEL_RAM_BLOCKS)
      {
        const unsigned int w = (unsigned int) (v+NSEEL_CLOSEFACTOR);
        EEL_F *dv = NSEEL_VM_getramptr_noalloc(vm,w,NULL);
        if (dv)
        {
          snprintf_append(sstr,sstr_sz," [%d]=%.14f",w,*dv);
          WDL_remove_trailing_decimal_zeros(sstr,2);
        }
        else
        {
          good_len = strlen(sstr);
          snprintf_append(sstr,sstr_sz," [%d]=<0>",w);
        }
      }

      char buf[512];
      buf[0]=0;
      if (peek_get_numbered_string_value(v,buf,sizeof(buf)))
      {
        if (good_len>=0) sstr[good_len]=0; // remove [addr]=<uninit> if a string and no ram
        snprintf_append(sstr,sstr_sz," #=\"%s\"",buf);
      }
    }

    if (rv) return rv;
  }

  return 0;
}


void EEL_Editor::doWatchInfo(int c)
{
    // determine the word we are on, check its value in the effect
  char sstr[512], buf[512];
  lstrcpyn_safe(sstr,"Use this on a valid symbol name", sizeof(sstr));
  WDL_FastString *t=m_text.Get(m_curs_y);
  char curChar=0;
  if (t)
  {
    const char *p=t->Get();
    const int bytex = WDL_utf8_charpos_to_bytepos(p,m_curs_x);
    if (bytex >= 0 && bytex < t->GetLength()) curChar = p[bytex];
    if (c != KEY_F1 && (m_selecting || 
             curChar == '(' || 
             curChar == '[' ||
             curChar == ')' ||
             curChar == ']'
             ))
    {
      WDL_FastString code;
      int miny,maxy,minx,maxx;
      bool ok = false;
      if (!m_selecting)
      {
        if (eel_sh_get_matching_pos_for_pos(&m_text,minx=m_curs_x,miny=m_curs_y,&maxx, &maxy,NULL,this))
        {
          if (maxy==miny)
          {
            if (maxx < minx)
            {
              int tmp = minx;
              minx=maxx;
              maxx=tmp;
            }
          }
          else if (maxy < miny)
          {
            int tmp=maxy;
            maxy=miny;
            miny=tmp;
            tmp = minx;
            minx=maxx;
            maxx=tmp;
          }
          ok = true;
          minx++; // skip leading (
        }
      }
      else
      {
        ok=true; 
        getselectregion(minx,miny,maxx,maxy); 
        WDL_FastString *s;
        s = m_text.Get(miny);
        if (s) minx = WDL_utf8_charpos_to_bytepos(s->Get(),minx);
        s = m_text.Get(maxy);
        if (s) maxx = WDL_utf8_charpos_to_bytepos(s->Get(),maxx);
      }

      if (ok)
      {
        int x;
        for (x = miny; x <= maxy; x ++)
        {
          WDL_FastString *s=m_text.Get(x);
          if (s) 
          {
            const char *str=s->Get();
            int sx,ex;
            if (x == miny) sx=wdl_max(minx,0);
            else sx=0;
            int tmp=s->GetLength();
            if (sx > tmp) sx=tmp;
      
            if (x == maxy) ex=wdl_min(maxx,tmp);
            else ex=tmp;
      
            if (code.GetLength()) code.Append("\r\n");
            code.Append(ex-sx?str+sx:"",ex-sx);
          }
        }
      }
      if (code.Get()[0])
      {
        if (m_selecting && (GetAsyncKeyState(VK_SHIFT)&0x8000))
        {
          peek_lock();
          NSEEL_CODEHANDLE ch;
          NSEEL_VMCTX vm = peek_get_VM("");

          if (vm && (ch = NSEEL_code_compile_ex(vm,code.Get(),1,0)))
          {
            codeHandleType *p = (codeHandleType*)ch;
            code.Ellipsize(3,20);
            const char *errstr = "failed writing to";
            if (p->code)
            {
              buf[0]=0;
              GetTempPath(sizeof(buf)-64,buf);
              lstrcatn(buf,"jsfx-out",sizeof(buf));
              FILE *fp = fopen(buf,"wb");
              if (fp)
              {
                errstr="wrote to";
                fwrite(p->code,1,p->code_size,fp);
                fclose(fp);
              }
            }
            snprintf(sstr,sizeof(sstr),"Expression '%s' compiled to %d bytes, %s temp/jsfx-out",code.Get(),p->code_size, errstr);
            NSEEL_code_free(ch);
          }
          else
          {
            code.Ellipsize(3,20);
            snprintf(sstr,sizeof(sstr),"Expression '%s' could not compile",code.Get());
          }
          peek_unlock();
        }
        else
        {
          WDL_FastString code2;
          code2.Set("__debug_watch_value = (((((");
          code2.Append(code.Get());
          code2.Append(")))));");
      
          peek_lock();

          NSEEL_VMCTX vm = peek_get_VM(code.Get());

          EEL_F *vptr=NULL;
          double v=0.0;
          const char *err="Invalid context";
          if (vm)
          {
            NSEEL_CODEHANDLE ch = NSEEL_code_compile_ex(vm,code2.Get(),1,0);
            if (!ch) err = "Error parsing";
            else
            {
              NSEEL_code_execute(ch);
              NSEEL_code_free(ch);
              vptr = NSEEL_VM_getvar(vm,"__debug_watch_value");
              if (vptr) v = *vptr;
            }
          }

          peek_unlock();

          {
            // remove whitespace from code for display
            int x;
            bool lb=true;
            for (x=0;x<code.GetLength();x++)
            {
              if (code.Get()[x]>0 && isspace_safe(code.Get()[x]))
              {
                if (lb) code.DeleteSub(x--,1);
                lb=true;
              }
              else
              {
                lb=false;
              }
            }
            if (lb && code.GetLength()>0) code.SetLen(code.GetLength()-1);
          }

          code.Ellipsize(3,20);
          if (vptr)
          {
            snprintf(sstr,sizeof(sstr),"Expression '%s' evaluates to %.14f",code.Get(),v);
          }
          else
          {
            snprintf(sstr,sizeof(sstr),"Error evaluating '%s': %s",code.Get(),err?err:"Unknown error");
          }
        }
      }
      // compile+execute code within () as debug_watch_value = ( code )
      // show value (or err msg)
    }
    else if (curChar>0 && (isalnum_safe(curChar) || curChar == '_' || curChar == '.' || curChar == '#')) 
    {
      const int bytex = WDL_utf8_charpos_to_bytepos(p,m_curs_x);
      const char *lp=p+bytex;
      const char *rp=lp + WDL_utf8_charpos_to_bytepos(lp,1);
      while (lp >= p && *lp > 0 && (isalnum_safe(*lp) || *lp == '_' || (*lp == '.' && (lp==p || lp[-1]!='.')))) lp--;
      if (lp < p || *lp != '#') lp++;
      while (*rp && *rp > 0 && (isalnum_safe(*rp) || *rp == '_' || (*rp == '.' && rp[1] != '.'))) rp++;

      if (*lp == '#' && rp > lp+1)
      {
        WDL_FastString n;
        lp++;
        n.Set(lp,(int)(rp-lp));
        int idx=peek_get_named_string_value(n.Get(),buf,sizeof(buf));
        if (idx>=0)
        {
          snprintf(sstr,sizeof(sstr),"#%s(%d)=%s",n.Get(),idx,buf);
          lp="";
        }
      }
      if (*lp > 0 && (isalpha_safe(*lp) || *lp == '_') && rp > lp)
      {
        WDL_FastString n;
        n.Set(lp,(int)(rp-lp));

        if (c==KEY_F1)
        {
          if (m_suggestion.GetLength())
            goto help_from_sug;
          on_help(n.Get(),0);
          return;
        }

        int f = peek_get_token_info(n.Get(),sstr,sizeof(sstr),~0,-1);
        if (!f) snprintf(sstr,sizeof(sstr),"'%s' NOT FOUND",n.Get());
      }
    }
  }
  if (c==KEY_F1)
  {
help_from_sug:
    WDL_FastString t;
    if (m_suggestion.GetLength())
    {
      const char *p = m_suggestion.Get();
      int l;
      for (l = 0; isalnum_safe(p[l]) || p[l] == '_' || p[l] == '.'; l ++);
      if (l>0) t.Set(m_suggestion.Get(),l);
    }
    on_help(t.GetLength() > 2 ? t.Get() : NULL,(int)curChar);
    return;
  }

  setCursor();
  draw_message(sstr);
}


void EEL_Editor::draw_bottom_line()
{
#define BOLD(x) { attrset(COLOR_BOTTOMLINE|A_BOLD); addstr(x); attrset(COLOR_BOTTOMLINE&~A_BOLD); }
  addstr("ma"); BOLD("T"); addstr("ch");
  BOLD(" S"); addstr("ave");
  if (peek_get_VM(""))
  {
    addstr(" pee"); BOLD("K");
  }
  if (GetTabCount()>1)
  {
    addstr(" | tab: ");
    BOLD("[], F?"); addstr("=switch ");
    BOLD("W"); addstr("=close");
  }
#undef BOLD
}

#define CTRL_KEY_DOWN (GetAsyncKeyState(VK_CONTROL)&0x8000)
#define SHIFT_KEY_DOWN (GetAsyncKeyState(VK_SHIFT)&0x8000)
#define ALT_KEY_DOWN (GetAsyncKeyState(VK_MENU)&0x8000)

#ifndef WM_MOUSEWHEEL
#define WM_MOUSEWHEEL 0x20A
#endif

static const char *suggestion_help_text = "(up/down to select, tab to insert)";
static const char *suggestion_help_text2 = "(tab or return to insert)";
static LRESULT WINAPI suggestionProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
#ifdef _WIN32

  static int Scroll_Message;
  if (!Scroll_Message)
  {
    Scroll_Message = (int)RegisterWindowMessage("MSWHEEL_ROLLMSG");
    if (!Scroll_Message) Scroll_Message=-1;
  }
  if (Scroll_Message > 0 && uMsg == (UINT)Scroll_Message)
  {
    uMsg=WM_MOUSEWHEEL;
    wParam<<=16;
  }
#endif

  EEL_Editor *editor;
  switch (uMsg)
  {
    case WM_DESTROY:
      editor = (EEL_Editor *)GetWindowLongPtr(hwnd,GWLP_USERDATA);
      if (editor) editor->m_suggestion_hwnd = NULL;
    break;
    case WM_MOUSEWHEEL:
    {
      editor = (EEL_Editor *)GetWindowLongPtr(hwnd,GWLP_USERDATA);
      if (editor && editor->m_cursesCtx)
      {
        win32CursesCtx *ctx = (win32CursesCtx *)editor->m_cursesCtx;
        int a = (int)(short)HIWORD(wParam);
        if (a < 0) { a /= 120; if (a==0) a--; }
        else if (a>0) { a /= 120; if (a==0) a++; }
        else return 1;

        RECT r;
        GetClientRect(hwnd,&r);
        const int fonth = wdl_max(ctx->m_font_h,1);
        const int maxv = wdl_max(r.bottom / fonth - 1,1);
        const int ni = editor->m_suggestion_list.get_size();
        if (ni > maxv)
        {
          const int nv = wdl_clamp(editor->m_suggestion_hwnd_scroll - a, 0, ni - maxv);
          if (nv != editor->m_suggestion_hwnd_scroll)
          {
            editor->m_suggestion_hwnd_scroll = nv;
            editor->m_suggestion_hwnd_sel = wdl_clamp(editor->m_suggestion_hwnd_sel,nv,nv+maxv-1);
            InvalidateRect(hwnd,NULL,FALSE);
          }
        }
      }
    }
    return 1;
    case WM_LBUTTONDOWN:
    case WM_MOUSEMOVE:
      editor = (EEL_Editor *)GetWindowLongPtr(hwnd,GWLP_USERDATA);
      if (editor)
      {
        win32CursesCtx *ctx = (win32CursesCtx *)editor->m_cursesCtx;
        if (ctx && ctx->m_font_h)
        {
          HWND par = GetParent(hwnd);
          if (uMsg == WM_MOUSEMOVE)
          {
            HWND foc = GetFocus();
            if (!foc || (foc != hwnd && foc != par)) return 0;
          }

          RECT r;
          GetClientRect(hwnd,&r);
          SetForegroundWindow(par);
          SetFocus(par);

          const int max_vis = r.bottom / ctx->m_font_h - 1, sel = editor->m_suggestion_hwnd_sel;
          int hit = GET_Y_LPARAM(lParam) / ctx->m_font_h;
          if (hit >= max_vis) return 0;
          hit += editor->m_suggestion_hwnd_scroll;

          if (uMsg == WM_LBUTTONDOWN && !SHIFT_KEY_DOWN && !ALT_KEY_DOWN && !CTRL_KEY_DOWN)
          {
            editor->m_suggestion_hwnd_sel = hit;
            editor->onChar('\t');
          }
          else if (sel != hit)
          {
            POINT pt;
            GetCursorPos(&pt);
            if (wdl_abs(pt.x - editor->m_suggestion_hwnd_initmousepos.x) +
                wdl_abs(pt.y - editor->m_suggestion_hwnd_initmousepos.y) < ctx->m_font_h*3/4)
            {
              return 0;
            }
            editor->m_suggestion_hwnd_initmousepos.x = pt.x + 0x10000000;

            editor->m_suggestion_hwnd_sel = hit;
            InvalidateRect(hwnd,NULL,FALSE);

            char sug[512];
            sug[0]=0;
            const char *p = editor->m_suggestion_list.get(hit);
            if (p && editor->peek_get_token_info(p,sug,sizeof(sug),~0,-1))
            {
              editor->m_suggestion.Set(sug);
              editor->draw_top_line();
              editor->setCursor();
            }
          }
        }
      }
    return 0;
    case WM_PAINT:
      editor = (EEL_Editor *)GetWindowLongPtr(hwnd,GWLP_USERDATA);
      if (editor)
      {
        PAINTSTRUCT ps;
        if (BeginPaint(hwnd,&ps))
        {
          RECT r;
          GetClientRect(hwnd,&r);

          win32CursesCtx *ctx = (win32CursesCtx *)editor->m_cursesCtx;
          HBRUSH br = CreateSolidBrush(ctx->colortab[WDL_CursesEditor::COLOR_TOPLINE][1]);
          FillRect(ps.hdc,&r,br);
          DeleteObject(br);

          suggested_matchlist *ml = &editor->m_suggestion_list;

          HGDIOBJ oldObj = SelectObject(ps.hdc,ctx->mOurFont);
          SetBkMode(ps.hdc,TRANSPARENT);

          const int fonth = wdl_max(ctx->m_font_h,1);
          const int maxv = wdl_max(r.bottom / fonth - 1,1);
          const int selpos = wdl_max(editor->m_suggestion_hwnd_sel,0);
          int ypos = 0;

          editor->m_suggestion_hwnd_scroll = wdl_clamp(editor->m_suggestion_hwnd_scroll, selpos-maxv + 1,selpos);
          if (editor->m_suggestion_hwnd_scroll < 0)
            editor->m_suggestion_hwnd_scroll = 0;

          const bool show_scores = (GetAsyncKeyState(VK_SHIFT)&0x8000) && (GetAsyncKeyState(VK_CONTROL)&0x8000) && (GetAsyncKeyState(VK_MENU)&0x8000);

          for (int x = editor->m_suggestion_hwnd_scroll; x < ml->get_size() && ypos <= r.bottom-fonth*2; x ++)
          {
            int mode, score;
            const char *p = ml->get(x,&mode,&score);
            if (WDL_NORMALLY(p))
            {
              const bool sel = x == selpos;
              SetTextColor(ps.hdc,ctx->colortab[
                  (mode == suggested_matchlist::MODE_VAR ? WDL_CursesEditor::COLOR_TOPLINE :
                   mode == suggested_matchlist::MODE_USERFUNC ? WDL_CursesEditor::SYNTAX_FUNC2 :
                   mode == suggested_matchlist::MODE_REGVAR ? WDL_CursesEditor::SYNTAX_REGVAR :
                   WDL_CursesEditor::SYNTAX_KEYWORD)
                  | (sel ? A_BOLD:0)][0]);
              RECT tr = {4, ypos, r.right-4, ypos+fonth };
              DrawTextUTF8(ps.hdc,p,-1,&tr,DT_SINGLELINE|DT_NOPREFIX|DT_TOP|DT_LEFT);
              if (show_scores)
              {
                char tmp[128];
                snprintf(tmp,sizeof(tmp),"(%d)",score);
                DrawTextUTF8(ps.hdc,tmp,-1,&tr,DT_SINGLELINE|DT_NOPREFIX|DT_TOP|DT_RIGHT);
              }
            }
            ypos += fonth;
          }
          {
            const COLORREF mix = ((ctx->colortab[WDL_CursesEditor::COLOR_TOPLINE][1]&0xfefefe)>>1) +
                                 ((ctx->colortab[WDL_CursesEditor::COLOR_TOPLINE][0]&0xfefefe)>>1);
            SetTextColor(ps.hdc,mix);
            RECT tr = {4, r.bottom-fonth, r.right-4, r.bottom };
            DrawTextUTF8(ps.hdc,
                editor->m_suggestion_hwnd_sel >= 0 ? suggestion_help_text2 : suggestion_help_text,
                -1,&tr,DT_SINGLELINE|DT_NOPREFIX|DT_TOP|DT_CENTER);
          }
          SelectObject(ps.hdc,oldObj);

          EndPaint(hwnd,&ps);
        }
      }
    return 0;
  }
  return DefWindowProc(hwnd,uMsg,wParam,lParam);
}

void EEL_Editor::open_import_line()
{
  WDL_FastString *txtstr=m_text.Get(m_curs_y);
  const char *txt=txtstr?txtstr->Get():NULL;
  char fnp[2048];
  if (txt && line_has_openable_file(txt,WDL_utf8_charpos_to_bytepos(txt,m_curs_x),fnp,sizeof(fnp)))
  {
    WDL_CursesEditor::OpenFileInTab(fnp);
  }
}

int EEL_Editor::onChar(int c)
{
  if ((m_ui_state == UI_STATE_NORMAL || m_ui_state == UI_STATE_MESSAGE) && 
      (c == 'K'-'A'+1 || c == 'S'-'A'+1 || c == 'R'-'A'+1 || !SHIFT_KEY_DOWN) && !ALT_KEY_DOWN) switch (c)
  {
  case KEY_F1:
    if (CTRL_KEY_DOWN) break;
    WDL_FALLTHROUGH;
  case 'K'-'A'+1:
    doWatchInfo(c);
  return 0;
  case 'S'-'A'+1:
   {
     WDL_DestroyCheck chk(&destroy_check);
     if(updateFile())
     {
       if (chk.isOK())
         draw_message("Error writing file, changes not saved!");
     }
     if (chk.isOK())
       setCursorIfVisible();
   }
  return 0;

  // case 'I': note stupidly we map Ctrl+I to VK_TAB, bleh
  case 'R'-'A'+1:
    if (!SHIFT_KEY_DOWN) break;
    if (!m_selecting)
    {
      open_import_line();
    }
  return 0;
  case KEY_F4:
  case 'T'-'A'+1:
    doParenMatching();
  return 0;
  }


  int rv = 0;
  int do_sug = (g_eel_editor_max_vis_suggestions > 0 &&
                m_ui_state == UI_STATE_NORMAL &&
                !m_selecting && m_top_margin > 0 &&
                (c == 'L'-'A'+1 || (!CTRL_KEY_DOWN && !ALT_KEY_DOWN))) ? 1 : 0;
  bool did_fuzzy = false;
  char sug[1024];
  sug[0]=0;

  if (do_sug)
  {
    if (m_suggestion_hwnd)
    {
      // insert if tab or shift+enter or (enter if arrow-navigated)
      if ((c == '\t' && !SHIFT_KEY_DOWN) ||
          (c == '\r' && (m_suggestion_hwnd_sel>=0 || SHIFT_KEY_DOWN)))
      {
        char buf[512];
        int sug_mode = 0;
        const char *ptr = m_suggestion_list.get(wdl_max(m_suggestion_hwnd_sel,0), &sug_mode);
        lstrcpyn_safe(buf,ptr?ptr:"",sizeof(buf));

        DestroyWindow(m_suggestion_hwnd);

        WDL_FastString *l=m_text.Get(m_curs_y);
        if (buf[0] && l &&
            WDL_NORMALLY(m_suggestion_tokpos>=0 && m_suggestion_tokpos < l->GetLength()) &&
            WDL_NORMALLY(m_suggestion_toklen>0) &&
            WDL_NORMALLY(m_suggestion_tokpos+m_suggestion_toklen <= l->GetLength()))
        {
          preSaveUndoState();
          if (sug_mode == suggested_matchlist::MODE_USERFUNC)
          {
            const char *tok = l->Get() + m_suggestion_tokpos;
            for (int j = m_suggestion_toklen - 1; j >= 0; j --)
            {
              if (tok[j] == '.')
              {
                const char *be = buf;
                while (*be) be++;
                while (be > buf && *be != '.') be--;
                if (be > buf)
                {
                  // buf is foo.bar, lead_sz=3, if j>=3 check for common prefix
                  const int lead_sz = (int) (be-buf);
                  if (j == lead_sz || (j > lead_sz && tok[j-lead_sz-1] == '.'))
                  {
                    // prefixes match, ignore
                    if (!strnicmp(buf, tok + j - lead_sz, lead_sz+1)) continue;
                  }
                }

                m_suggestion_tokpos += j+1;
                m_suggestion_toklen -= j+1;
                break;
              }
            }
          }
          l->DeleteSub(m_suggestion_tokpos,m_suggestion_toklen);
          l->Insert(buf,m_suggestion_tokpos);
          int pos = m_suggestion_tokpos + strlen(buf);
          if (sug_mode == suggested_matchlist::MODE_FUNC || sug_mode == suggested_matchlist::MODE_USERFUNC)
          {
            if (pos >= l->GetLength() || l->Get()[pos] != '(')
              l->Insert("(",pos++);
          }
          m_curs_x = WDL_utf8_bytepos_to_charpos(l->Get(),pos);
          draw();
          saveUndoState();
          setCursor();
          goto run_suggest;
        }
        return 0;
      }
      if ((c == KEY_UP || c==KEY_DOWN || c == KEY_NPAGE || c == KEY_PPAGE) && !SHIFT_KEY_DOWN)
      {
        m_suggestion_hwnd_sel = wdl_max(m_suggestion_hwnd_sel,0) +
          (c==KEY_UP ? -1 : c==KEY_DOWN ? 1 : c==KEY_NPAGE ? 4 : -4);

        if (m_suggestion_hwnd_sel >= m_suggestion_list.get_size())
          m_suggestion_hwnd_sel = m_suggestion_list.get_size()-1;
        if (m_suggestion_hwnd_sel < 0)
          m_suggestion_hwnd_sel=0;

        InvalidateRect(m_suggestion_hwnd,NULL,FALSE);

        const char *p = m_suggestion_list.get(m_suggestion_hwnd_sel);
        if (p) peek_get_token_info(p,sug,sizeof(sug),~0,m_curs_y);
        goto finish_sug;
      }
    }

    if (c==27 ||
        c=='L'-'A'+1 ||
        c=='\r' ||
        c=='\t' ||
        (c>=KEY_DOWN && c<= KEY_F12 && c!=KEY_DC)) do_sug = 2; // no fuzzy window
    else if (c=='\b' && !m_suggestion_hwnd) do_sug=2; // backspace will update but won't show suggestions
  }

  rv = WDL_CursesEditor::onChar(c);

  if (!CURSES_INSTANCE)
    return rv;

run_suggest:
  if (do_sug)
  {
    WDL_FastString *l=m_text.Get(m_curs_y);
    if (l)
    {
      const int MAX_TOK=512;
      struct {
        const char *tok;
        int toklen;
      } token_list[MAX_TOK];

      const char *cursor = l->Get() + WDL_utf8_charpos_to_bytepos(l->Get(),m_curs_x);
      int ntok = 0;
      {
        const char *p = l->Get(), *endp = p + l->GetLength();
        int state = m_suggestion_curline_comment_state, toklen = 0, bcnt = 0, pcnt = 0;
        const char *tok;
                                       // if no parens/brackets are open, use a peekable token that starts at cursor
        while ((tok=sh_tokenize(&p,endp,&toklen,&state)) && cursor > tok-(!pcnt && !bcnt && (tok[0] < 0 || tok[0] == '_' || isalpha_safe(tok[0]) || tok[0] == '#')))
        {
          if (!state)
          {
            if (WDL_NOT_NORMALLY(ntok >= MAX_TOK))
            {
              memmove(token_list,token_list+1,sizeof(token_list) - sizeof(token_list[0]));
              ntok--;
            }
            switch (*tok)
            {
              case '[': bcnt++; break;
              case ']': bcnt--; break;
              case '(': pcnt++; break;
              case ')': pcnt--; break;
            }
            token_list[ntok].tok = tok;
            token_list[ntok].toklen = toklen;
            ntok++;
          }
        }
      }

      int t = ntok;
      int comma_cnt = 0;
      while (--t >= 0)
      {
        const char *tok = token_list[t].tok;
        int toklen = token_list[t].toklen;
        const int lc = tok[0], ac = lc==')' ? '(' : lc==']' ? '[' : 0;
        if (ac)
        {
          int cnt=1;
          while (--t>=0)
          {
            const int c = token_list[t].tok[0];
            if (c == lc) cnt++;
            else if (c == ac && !--cnt) break;
          }
          if (t > 0)
          {
            const int c = token_list[t-1].tok[0];
            if (c != ',' && c != ')' && c != ']') t--;
            continue;
          }
        }
        if (t<0) break;

        if (tok[0] == ',') comma_cnt++;
        else if ((tok[0] < 0 || tok[0] == '_' || isalpha_safe(tok[0]) || tok[0] == '#') &&
            (cursor <= tok + toklen || (t < ntok-1 && token_list[t+1].tok[0] == '(')))
        {
          // if cursor is within or at end of token, or is a function (followed by open-paren)
          char buf[512];
          lstrcpyn_safe(buf,tok,wdl_min(toklen+1, sizeof(buf)));
          if (peek_get_token_info(buf,sug,sizeof(sug),~0,m_curs_y))
          {
            if (comma_cnt > 0)
            {
              // hide previous parameters from sug's parameter fields so we know which parameters
              // we are (hopefully on)
              char *pstart = sug;
              while (*pstart && *pstart != '(') pstart++;
              if (*pstart == '(') pstart++;
              int comma_pos = 0;
              char *p = pstart;
              while (comma_pos < comma_cnt)
              {
                while (*p == ' ') p++;
                while (*p && *p != ',' && *p != ')') p++;
                if (*p == ')') break;
                if (*p) p++;
                comma_pos++;
              }

              if (*p && *p != ')')
              {
                *pstart=0;
                lstrcpyn_safe(buf,p,sizeof(buf));
                snprintf_append(sug,sizeof(sug), "... %s",buf);
              }
            }
            break;
          }

          // only use token up to cursor for suggestions
          if (cursor >= tok && cursor <= tok+toklen)
          {
            toklen = (int) (cursor-tok);
            lstrcpyn_safe(buf,tok,wdl_min(toklen+1, sizeof(buf)));
          }

          if (c == '\b' && cursor == tok)
          {
          }
          else if (do_sug != 2 && t == ntok-1 && toklen >= 3 && cursor <= tok + toklen)
          {
            m_suggestion_list.clear();
            get_suggested_token_names(buf,~0,&m_suggestion_list);

            win32CursesCtx *ctx = (win32CursesCtx *)m_cursesCtx;
            if (m_suggestion_list.get_size()>0 &&
                WDL_NORMALLY(ctx->m_hwnd) && WDL_NORMALLY(ctx->m_font_w) && WDL_NORMALLY(ctx->m_font_h))
            {
              m_suggestion_toklen = toklen;
              m_suggestion_tokpos = (int)(tok-l->Get());
              m_suggestion_hwnd_sel = -1;
              if (!m_suggestion_hwnd)
              {
#ifdef _WIN32
                static HINSTANCE last_inst;
                const char *classname = "eel_edit_predicto";
                HINSTANCE inst = (HINSTANCE)GetWindowLongPtr(ctx->m_hwnd,GWLP_HINSTANCE);
                if (inst != last_inst)
                {
                  last_inst = inst;
                  WNDCLASS wc={CS_DBLCLKS,suggestionProc,};
                  wc.lpszClassName=classname;
                  wc.hInstance=inst;
                  wc.hCursor=LoadCursor(NULL,IDC_ARROW);
                  RegisterClass(&wc);
                }
                m_suggestion_hwnd = CreateWindowEx(0,classname,"", WS_CHILD, 0,0,0,0, ctx->m_hwnd, NULL, inst, NULL);
#else
                m_suggestion_hwnd = CreateDialogParam(NULL,NULL,ctx->m_hwnd, suggestionProc, 0);
#endif
                if (m_suggestion_hwnd) SetWindowLongPtr(m_suggestion_hwnd,GWLP_USERDATA,(LPARAM)this);
              }
              if (m_suggestion_hwnd)
              {
                const int fontw = ctx->m_font_w, fonth = ctx->m_font_h;
                int xpos = (WDL_utf8_bytepos_to_charpos(l->Get(),m_suggestion_tokpos) - m_offs_x) * fontw;
                RECT par_cr;
                GetClientRect(ctx->m_hwnd,&par_cr);

                int use_w = (int)strlen(suggestion_help_text);
                int use_h = (wdl_min(g_eel_editor_max_vis_suggestions,m_suggestion_list.get_size()) + 1)*fonth;
                for (int x = 0; x < m_suggestion_list.get_size(); x ++)
                {
                  const char *p = m_suggestion_list.get(x);
                  if (WDL_NORMALLY(p!=NULL))
                  {
                    const int l = (int) strlen(p);
                    if (l > use_w) use_w=l;
                  }
                }

                use_w = 8 + use_w * fontw;
                if (use_w > par_cr.right - xpos)
                {
                  xpos = wdl_max(par_cr.right - use_w,0);
                  use_w = par_cr.right - xpos;
                }

                const int cursor_line_y = ctx->m_cursor_y * fonth;
                int ypos = cursor_line_y + fonth;
                if (ypos + use_h > par_cr.bottom)
                {
                  if (cursor_line_y-fonth > par_cr.bottom - ypos)
                  {
                    // more room at the top, but enough?
                    ypos = cursor_line_y - use_h;
                    if (ypos<fonth)
                    {
                      ypos = fonth;
                      use_h = cursor_line_y-fonth;
                    }
                  }
                  else
                    use_h = par_cr.bottom - ypos;
                }

                SetWindowPos(m_suggestion_hwnd,NULL,xpos,ypos,use_w,use_h, SWP_NOZORDER|SWP_NOACTIVATE);
                InvalidateRect(m_suggestion_hwnd,NULL,FALSE);
                ShowWindow(m_suggestion_hwnd,SW_SHOWNA);

                GetCursorPos(&m_suggestion_hwnd_initmousepos);
              }
              did_fuzzy = true;
              const char *p = m_suggestion_list.get(wdl_max(m_suggestion_hwnd_sel,0));
              if (p && peek_get_token_info(p,sug,sizeof(sug),~0,m_curs_y)) break;
            }
          }
        }
      }
    }
  }
  if (!did_fuzzy && m_suggestion_hwnd) DestroyWindow(m_suggestion_hwnd);

finish_sug:
  if (strcmp(sug,m_suggestion.Get()) && m_ui_state == UI_STATE_NORMAL)
  {
    m_suggestion.Set(sug);
    if (sug[0])
    {
      m_suggestion_x=m_curs_x;
      m_suggestion_y=m_curs_y;
      draw_top_line();
      setCursor();
    }
  }
  if (!sug[0] && m_suggestion_y>=0 && m_ui_state == UI_STATE_NORMAL)
  {
    m_suggestion_x=m_suggestion_y=-1;
    m_suggestion.Set("");
    if (m_top_margin>0) draw_top_line();
    else draw();
    setCursor();
  }

  return rv;
}

void EEL_Editor::draw_top_line()
{
  if (m_curs_x >= m_suggestion_x && m_curs_y == m_suggestion_y && m_suggestion.GetLength() && m_ui_state == UI_STATE_NORMAL)
  {
    const char *p=m_suggestion.Get();
    char str[512];
    if (WDL_utf8_get_charlen(m_suggestion.Get()) > COLS)
    {
      int l = WDL_utf8_charpos_to_bytepos(m_suggestion.Get(),COLS-4);
      if (l > sizeof(str)-6) l = sizeof(str)-6;
      lstrcpyn(str, m_suggestion.Get(), l+1);
      strcat(str, "...");
      p=str;
    }

    attrset(COLOR_TOPLINE|A_BOLD);
    bkgdset(COLOR_TOPLINE);
    move(0, 0);
    addstr(p);
    clrtoeol();
    attrset(0);
    bkgdset(0);
  }
  else
  {
    m_suggestion_x=m_suggestion_y=-1;
    if (m_suggestion.GetLength()) m_suggestion.Set("");
    WDL_CursesEditor::draw_top_line();
  }
}


void EEL_Editor::onRightClick(HWND hwnd)
{
  WDL_LogicalSortStringKeyedArray<int> flist(m_case_sensitive);
  int i;
  if (!(GetAsyncKeyState(VK_CONTROL)&0x8000))
  {
    m_code_func_cache_lines = -1; // invalidate cache
    ensure_code_func_cache_valid();
    for (i = 0; i < m_code_func_cache.GetSize(); i ++)
    {
      const char *p = m_code_func_cache.Get(i);
      const int line = *(int *)p;
      p += sizeof(int);

      const char *q = p+strlen(p)+1;
      char buf[512];
      snprintf(buf,sizeof(buf),"%s%s",p,q);
      flist.AddUnsorted(buf,line);
      p += 4;
    }
  }

  get_extra_filepos_names(&flist,0);

  if (flist.GetSize()>1)
  {
    flist.Resort();
    if (m_case_sensitive) flist.Resort(WDL_LogicalSortStringKeyedArray<int>::cmpistr);
  }

  get_extra_filepos_names(&flist,1);

  if (flist.GetSize())
  {
    HMENU hm=CreatePopupMenu();
    int pos=0;
    for (i=0; i < flist.GetSize(); ++i)
    {
      const char* fname=NULL;
      int line=flist.Enumerate(i, &fname);
      InsertMenu(hm, pos++, MF_STRING|MF_BYPOSITION, line+1, fname);
    }
    POINT p;
    GetCursorPos(&p);
    int ret=TrackPopupMenu(hm, TPM_NONOTIFY|TPM_RETURNCMD, p.x, p.y, 0, hwnd, NULL);
    DestroyMenu(hm);
    if (ret > 0)
    {
      GoToLine(ret-1,true);
    }
  }
  else
  {
    doWatchInfo(0);
  }
}

void EEL_Editor::ensure_code_func_cache_valid()
{
  const char *prefix = m_function_prefix;
  if (!prefix || !*prefix) return;

  const DWORD now = GetTickCount();
  if (m_text.GetSize()==m_code_func_cache_lines && (now-m_code_func_cache_time)<5000) return;

  m_code_func_cache_lines = m_text.GetSize();
  m_code_func_cache_time = now;

  m_code_func_cache.Empty(true,free);

  const int prefix_len = (int) strlen(m_function_prefix);
  for (int i=0; i < m_text.GetSize(); ++i)
  {
    WDL_FastString* s=m_text.Get(i);
    if (WDL_NORMALLY(s))
    {
      const char *p = s->Get();
      while (*p)
      {
        if (m_case_sensitive ? !strncmp(p,prefix,prefix_len) : !strnicmp(p,prefix,prefix_len))
        {
          p+=prefix_len;
          while (*p == ' ') p++;
          if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_')
          {
            const char *q = p+1;
            while ((*q >= '0' && *q <= '9') ||
                   (*q >= 'a' && *q <= 'z') ||
                   (*q >= 'A' && *q <= 'Z') ||
                   *q == ':' || // lua
                   *q == '_' || *q == '.') q++;

            const char *endp = q;
            while (*q == ' ') q++;
            if (*q == '(')
            {
              const char *endq = q;
              while (*endq && *endq != ')') endq++;
              if (*endq) endq++;
              const char *r = endq;
              while (*r == ' ') r++;

              const int p_len = (int) (endp - p);
              const int q_len = (int) (endq - q);
              const int r_len = (int) strlen(r);

              // add function
              char *v = (char *)malloc(sizeof(int) + p_len + q_len + r_len + 3), *wr = v;
              if (WDL_NORMALLY(v))
              {
                *(int *)wr = i;  wr += sizeof(int);
                lstrcpyn_safe(wr,p,p_len+1); wr += p_len+1;
                lstrcpyn_safe(wr,q,q_len+1); wr += q_len+1;
                lstrcpyn_safe(wr,r,r_len+1); wr += r_len+1;

                m_code_func_cache.Add(v);
              }
              p = r; // continue parsing after parentheses
            }
          }
        }
        if (*p) p++;
      }
    }
  }
}


#ifdef WDL_IS_FAKE_CURSES

LRESULT EEL_Editor::onMouseMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
  {
    case WM_LBUTTONDBLCLK:
      if (m_suggestion_hwnd) DestroyWindow(m_suggestion_hwnd);
      if (CURSES_INSTANCE && CURSES_INSTANCE->m_font_w && CURSES_INSTANCE->m_font_h)
      {
        const int y = ((short)HIWORD(lParam)) / CURSES_INSTANCE->m_font_h - m_top_margin;
        //const int x = ((short)LOWORD(lParam)) / CURSES_INSTANCE->m_font_w + m_offs_x;
        WDL_FastString *fs=m_text.Get(y + m_paneoffs_y[m_curpane]);
        if (fs && y >= 0)
        {
          if (!strncmp(fs->Get(),"import",6) && fs->Get()[6]>0 && isspace_safe(fs->Get()[6]))
          {
            open_import_line();
            return 1;
          }
        }

        // ctrl+doubleclicking a function goes to it
        if (!(g_eel_editor_flags&1) != !CTRL_KEY_DOWN)
        {
          WDL_FastString *l=m_text.Get(m_curs_y);
          if (l)
          {
            const char *p = l->Get(), *endp = p + l->GetLength(), *cursor = p + WDL_utf8_charpos_to_bytepos(p,m_curs_x);
            int state = 0, toklen = 0;
            const char *tok;
            while ((tok=sh_tokenize(&p,endp,&toklen,&state)) && cursor > tok+toklen);

            if (tok && cursor <= tok+toklen)
            {
              ensure_code_func_cache_valid();

              while (toklen > 0)
              {
                for (int i = 0; i < m_code_func_cache.GetSize(); i ++)
                {
                  const char *p = m_code_func_cache.Get(i);
                  int line = *(int *)p;
                  p+=sizeof(int);
                  if (line != m_curs_y && strlen(p) == toklen && (m_case_sensitive ? !strncmp(p,tok,toklen) : !strnicmp(p,tok,toklen)))
                  {
                    GoToLine(line,true);
                    return 0;
                  }
                }

                // try removing any foo. prefixes
                while (toklen > 0 && *tok != '.') { tok++; toklen--; }
                tok++;
                toklen--;
              }
            }
          }
        }
      }
    break;
    case WM_LBUTTONDOWN:
    case WM_RBUTTONDOWN:
      if (m_suggestion_hwnd) DestroyWindow(m_suggestion_hwnd);
    break;

  }
  return WDL_CursesEditor::onMouseMessage(hwnd,uMsg,wParam,lParam);
}
#endif
