packages feed

isocline-1.1.0: src/completers.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 <string.h>
#include <stdio.h>

#include "../include/isocline.h"
#include "common.h"
#include "env.h"
#include "stringbuf.h"
#include "completions.h"



//-------------------------------------------------------------
// Word completion 
//-------------------------------------------------------------

// free variables for word completion
typedef struct word_closure_s {
  long                  delete_before_adjust;
  void*                 prev_env;
  const char*           postfix;          // content after the cursor
  ic_completion_fun_t*  prev_complete;
} word_closure_t;


// word completion callback
static bool token_add_completion_ex(ic_env_t* env, void* closure, const char* replacement, const char* display, const char* help, long delete_before, long delete_after) {
  word_closure_t* wenv = (word_closure_t*)(closure);
  // call the previous completer with an adjusted delete-before
  const long delete_after_adjust = (long)ic_count_end_overlap(replacement, wenv->postfix);
  return (*wenv->prev_complete)(env, wenv->prev_env, replacement, display, help, wenv->delete_before_adjust + delete_before, delete_after_adjust + delete_after);
}


ic_public void ic_complete_word(ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t* fun,
                                    ic_is_char_class_fun_t* is_word_char) 
{
  if (is_word_char == NULL) is_word_char = &ic_char_is_nonseparator;
  
  ssize_t len = ic_strlen(prefix);
  ssize_t pos = len; // will be start of the 'word' (excluding a potential start quote)
  while (pos > 0) {
    // go back one code point
    ssize_t ofs = str_prev_ofs(prefix, pos, NULL);
    if (ofs <= 0) break;
    if (!(*is_word_char)(prefix + (pos - ofs), (long)ofs)) { 
      break;
    }
    pos -= ofs;
  }
  if (pos < 0) { pos = 0; }
  
  // stop if empty word
  // if (len == pos) return;
  
  // set up the closure
  word_closure_t wenv;
  wenv.delete_before_adjust = (long)(len - pos);
  wenv.prev_complete = cenv->complete;
  wenv.prev_env = cenv->env;
  wenv.postfix = cenv->input + cenv->cursor;
  cenv->complete = &token_add_completion_ex;
  cenv->closure = &wenv;

  // and call the user completion routine
  (*fun)(cenv, prefix + pos);

  // restore the original environment
  cenv->complete = wenv.prev_complete;
  cenv->closure = wenv.prev_env;
}


//-------------------------------------------------------------
// Quoted word completion (with escape characters)
//-------------------------------------------------------------



// free variables for word completion
typedef struct qword_closure_s {
  char         escape_char;
  char         quote;
  long         delete_before_adjust;
  stringbuf_t* sbuf;
  void*        prev_env;
  const char*  postfix;      // follows the current input position
  ic_is_char_class_fun_t* is_word_char;
  ic_completion_fun_t*    prev_complete;
} qword_closure_t;


// word completion callback
static bool qword_add_completion_ex(ic_env_t* env, void* closure, const char* replacement, const char* display, const char* help, 
                                       long delete_before, long delete_after) {
  qword_closure_t* wenv = (qword_closure_t*)(closure);
  sbuf_replace( wenv->sbuf, replacement );   
  if (wenv->quote != 0) {
    // add end quote
    sbuf_append_char( wenv->sbuf, wenv->quote);
  }
  else {
    // escape non-word characters if it was not quoted
    ssize_t pos = 0;
    ssize_t next;
    while ( (next = sbuf_next_ofs(wenv->sbuf, pos, NULL)) > 0 ) 
    {
      if (!(*wenv->is_word_char)(sbuf_string(wenv->sbuf) + pos, (long)next)) { // strchr(wenv->non_word_char, sbuf_char_at( wenv->sbuf, pos )) != NULL) {
        sbuf_insert_char_at( wenv->sbuf, wenv->escape_char, pos);
        pos++;
      }
      pos += next;
    }
  }
  // and call the previous completion function
  const long delete_after_adjust = (long)ic_count_end_overlap(sbuf_string(wenv->sbuf), wenv->postfix);
  return (*wenv->prev_complete)( env, wenv->prev_env, sbuf_string(wenv->sbuf), display, help, wenv->delete_before_adjust + delete_before, delete_after_adjust + delete_after );  
}


ic_public void ic_complete_qword( ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t* fun, ic_is_char_class_fun_t* is_word_char ) {
  ic_complete_qword_ex( cenv, prefix, fun, is_word_char, '\\', NULL);
}


ic_public void ic_complete_qword_ex( ic_completion_env_t* cenv, const char* prefix, ic_completer_fun_t* fun, 
                                        ic_is_char_class_fun_t* is_word_char, char escape_char, const char* quote_chars ) {
  if (is_word_char == NULL) is_word_char = &ic_char_is_nonseparator ;  
  if (quote_chars == NULL) quote_chars = "'\"";

  ssize_t len = ic_strlen(prefix);
  ssize_t pos; // will be start of the 'word' (excluding a potential start quote)
  char quote = 0;
  ssize_t quote_len = 0;
  
  // 1. look for a starting quote
  if (quote_chars[0] != 0) {
    // we go forward and count all quotes; if it is uneven, we need to complete quoted.
    ssize_t qpos_open = -1;
    ssize_t qpos_close = -1;
    ssize_t qcount = 0;
    pos = 0; 
    while(pos < len) {
      if (prefix[pos] == escape_char && prefix[pos+1] != 0 && 
           !(*is_word_char)(prefix + pos + 1, 1)) // strchr(non_word_char, prefix[pos+1]) != NULL
      {       
        pos++; // skip escape and next char
      }
      else if (qcount % 2 == 0 && strchr(quote_chars, prefix[pos]) != NULL) {
        // open quote 
        qpos_open = pos;
        quote = prefix[pos];
        qcount++;
      }
      else if (qcount % 2 == 1 && prefix[pos] == quote) {
        // close quote
        qpos_close = pos;
        qcount++;
      }
      else if (!(*is_word_char)(prefix + pos, 1)) { //  strchr(non_word_char, prefix[pos]) != NULL) {
        qpos_close = -1;
      }
      ssize_t ofs = str_next_ofs( prefix, len, pos, NULL );
      if (ofs <= 0) break;
      pos += ofs;
    }    
    if ((qcount % 2 == 0 && qpos_close >= 0) || // if the last quote is only followed by word chars, we still complete it
        (qcount % 2 == 1))                     // opening quote found
    {
      quote_len = (len - qpos_open - 1);
      pos = qpos_open + 1;  // pos points to the word start just after the quote.
    }
    else {
      quote = 0;
    }
  }

  // 2. if we did not find a quoted word, look for non-word-chars
  if (quote == 0) {
    pos = len;
    while(pos > 0) {
      // go back one code point
      ssize_t ofs = str_prev_ofs(prefix, pos, NULL );
      if (ofs <= 0) break;
      if (!(*is_word_char)(prefix + (pos - ofs), (long)ofs)) { // strchr(non_word_char, prefix[pos - ofs]) != NULL) {
        // non word char, break if it is not escaped
        if (pos <= ofs || prefix[pos - ofs - 1] != escape_char) break; 
        // otherwise go on
        pos--; // skip escaped char
      }
      pos -= ofs;
    }
  }

  // stop if empty word
  // if (len == pos) return;

  // allocate new unescaped word prefix
  char* word = mem_strndup( cenv->env->mem, prefix + pos, (quote==0 ? len - pos : quote_len));
  if (word == NULL) return;

  if (quote == 0) {
    // unescape prefix
    ssize_t wlen = len - pos;
    ssize_t wpos = 0;
    while (wpos < wlen) {
      ssize_t ofs = str_next_ofs(word, wlen, wpos, NULL);
      if (ofs <= 0) break;
      if (word[wpos] == escape_char && word[wpos+1] != 0 &&
           !(*is_word_char)(word + wpos + 1, (long)ofs)) // strchr(non_word_char, word[wpos+1]) != NULL) {
      {
        ic_memmove(word + wpos, word + wpos + 1, wlen - wpos /* including 0 */);
      }
      wpos += ofs;
    }
  }
  #ifdef _WIN32
  else {
    // remove inner quote: "c:\Program Files\"Win
    ssize_t wlen = len - pos;
    ssize_t wpos = 0;
    while (wpos < wlen) {
      ssize_t ofs = str_next_ofs(word, wlen, wpos, NULL);
      if (ofs <= 0) break;
      if (word[wpos] == escape_char && word[wpos+1] == quote) {
        word[wpos+1] = escape_char;
        ic_memmove(word + wpos, word + wpos + 1, wlen - wpos /* including 0 */);
      }
      wpos += ofs;
    }
  }
  #endif

  // set up the closure
  qword_closure_t wenv;
  wenv.quote          = quote;
  wenv.is_word_char   = is_word_char;
  wenv.escape_char    = escape_char;
  wenv.delete_before_adjust = (long)(len - pos);
  wenv.prev_complete  = cenv->complete;
  wenv.prev_env       = cenv->env;
  wenv.postfix        = cenv->input + cenv->cursor;
  wenv.sbuf = sbuf_new(cenv->env->mem);
  if (wenv.sbuf == NULL) { mem_free(cenv->env->mem, word); return; }
  cenv->complete = &qword_add_completion_ex;
  cenv->closure = &wenv;

  // and call the user completion routine
  (*fun)( cenv, word );

  // restore the original environment
  cenv->complete = wenv.prev_complete;
  cenv->closure = wenv.prev_env;

  sbuf_free(wenv.sbuf);
  mem_free(cenv->env->mem, word);  
}




//-------------------------------------------------------------
// Complete file names
// Listing files
//-------------------------------------------------------------
#include <stdlib.h>

typedef enum file_type_e {
  // must follow BSD style LSCOLORS order
  FT_DEFAULT = 0,
  FT_DIR,
  FT_SYM,
  FT_SOCK,
  FT_PIPE,
  FT_BLOCK,
  FT_CHAR,
  FT_SETUID,
  FT_SETGID,
  FT_DIR_OW_STICKY,
  FT_DIR_OW,
  FT_DIR_STICKY,
  FT_EXE,
  FT_LAST
} file_type_t;

static int         cli_color; // 1 enabled, 0 not initialized, -1 disabled
static const char* lscolors  = "exfxcxdxbxegedabagacad";  // default BSD setting
static const char* ls_colors;
static const char* ls_colors_names[] = { "no=","di=","ln=","so=","pi=","bd=","cd=","su=","sg=","tw=","ow=","st=","ex=", NULL };

static bool ls_colors_init(void) {
  if (cli_color != 0) return (cli_color >= 1);
  // colors enabled?
  const char* s = getenv("CLICOLOR");
  if (s==NULL || (strcmp(s, "1")!=0 && strcmp(s, "") != 0)) {
    cli_color = -1;
    return false;
  }
  cli_color = 1;
  s = getenv("LS_COLORS");
  if (s != NULL) { ls_colors = s;  }
  s = getenv("LSCOLORS");
  if (s != NULL) { lscolors = s; }  
  return true;
}

static bool ls_valid_esc(ssize_t c) {
  return ((c==0 || c==1 || c==4 || c==7 || c==22 || c==24  || c==27) ||
    (c >= 30 && c <= 37) || (c >= 40 && c <= 47) ||
    (c >= 90 && c <= 97) || (c >= 100 && c <= 107));
}

static bool ls_colors_from_key(stringbuf_t* sb, const char* key) {
  // find key
  ssize_t keylen = ic_strlen(key);
  if (keylen <= 0) return false;
  const char* p = strstr(ls_colors, key);
  if (p == NULL) return false;
  p += keylen;
  if (key[keylen-1] != '=') {
    if (*p != '=') return false;
    p++;
  }
  ssize_t len = 0;
  while (p[len] != 0 && p[len] != ':') {
    len++;
  }
  if (len <= 0) return false;
  sbuf_append(sb, "[ansi-sgr=\"" );
  sbuf_append_n(sb, p, len );
  sbuf_append(sb, "\"]");
  return true;
}

static int ls_colors_from_char(char c) {
  if (c >= 'a' && c <= 'h')      { return (c - 'a'); }
  else if (c >= 'A' && c <= 'H') { return (c - 'A') + 8; }
  else if (c == 'x')             { return 256; }
  else return 256; // default
}

static bool ls_colors_append(stringbuf_t* sb, file_type_t ft, const char* ext) {
  if (!ls_colors_init()) return false;
  if (ls_colors != NULL) {
    // GNU style
    if (ft == FT_DEFAULT && ext != NULL) {
      // first try extension match
      if (ls_colors_from_key(sb, ext)) return true;
    }
    if (ft >= FT_DEFAULT && ft < FT_LAST) {
      // then a filetype match
      const char* key = ls_colors_names[ft];
      if (ls_colors_from_key(sb, key)) return true;
    }    
  }
  else if (lscolors != NULL) {
    // BSD style
    char fg = 'x';
    char bg = 'x';
    if (ic_strlen(lscolors) > (2*(ssize_t)ft)+1) {
      fg = lscolors[2*ft];
      bg = lscolors[2*ft + 1];
    }
    sbuf_appendf(sb, "[ansi-color=%d ansi-bgcolor=%d]", ls_colors_from_char(fg), ls_colors_from_char(bg) );
    return true;
  }
  return false;
}

static void ls_colorize(bool no_lscolor, stringbuf_t* sb, file_type_t ft, const char* name, const char* ext, char dirsep) {
  bool close = (no_lscolor ? false : ls_colors_append( sb, ft, ext));
  sbuf_append(sb, "[!pre]" );
  sbuf_append(sb, name);
  if (dirsep != 0) sbuf_append_char(sb, dirsep);
  sbuf_append(sb,"[/pre]" );
  if (close) { sbuf_append(sb, "[/]"); }
}

#if defined(_WIN32)
#include <io.h>
#include <sys/stat.h>

static bool os_is_dir(const char* cpath) {
  struct _stat64 st = { 0 };
  _stat64(cpath, &st);
  return ((st.st_mode & _S_IFDIR) != 0);
}

static file_type_t os_get_filetype(const char* cpath) {
  struct _stat64 st = { 0 };
  _stat64(cpath, &st);
  if (((st.st_mode) & _S_IFDIR) != 0) return FT_DIR;
  if (((st.st_mode) & _S_IFCHR) != 0) return FT_CHAR;
  if (((st.st_mode) & _S_IFIFO) != 0) return FT_PIPE;
  if (((st.st_mode) & _S_IEXEC) != 0) return FT_EXE;
  return FT_DEFAULT;
}


#define dir_cursor intptr_t
#define dir_entry  struct __finddata64_t

static bool os_findfirst(alloc_t* mem, const char* path, dir_cursor* d, dir_entry* entry) {
  stringbuf_t* spath = sbuf_new(mem);
  if (spath == NULL) return false;
  sbuf_append(spath, path);
  sbuf_append(spath, "\\*");
  *d = _findfirsti64(sbuf_string(spath), entry);
  mem_free(mem,spath);
  return (*d != -1);
}

static bool os_findnext(dir_cursor d, dir_entry* entry) {
  return (_findnexti64(d, entry) == 0);  
}

static void os_findclose(dir_cursor d) {
  _findclose(d);
}

static const char* os_direntry_name(dir_entry* entry) {
  return entry->name;  
}

static bool os_path_is_absolute( const char* path ) {
  if (path != NULL && path[0] != 0 && path[1] == ':' && (path[2] == '\\' || path[2] == '/' || path[2] == 0)) {
    char drive = path[0];
    return ((drive >= 'A' && drive <= 'Z') || (drive >= 'a' && drive <= 'z'));
  }
  else return false;
}

ic_private char ic_dirsep(void) {
  return '\\';
}
#else

#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>

static bool os_is_dir(const char* cpath) {
  struct stat st;
  memset(&st, 0, sizeof(st));
  stat(cpath, &st);
  return (S_ISDIR(st.st_mode));
}

static file_type_t os_get_filetype(const char* cpath) {
  struct stat st;
  memset(&st, 0, sizeof(st));
  lstat(cpath, &st);
  switch ((st.st_mode)&S_IFMT) {
    case S_IFSOCK: return FT_SOCK;
    case S_IFLNK: {
      return FT_SYM;
    }
    case S_IFIFO:  return FT_PIPE;
    case S_IFCHR:  return FT_CHAR;
    case S_IFBLK:  return FT_BLOCK;
    case S_IFDIR: {
      if ((st.st_mode & S_ISUID) != 0) return FT_SETUID;
      if ((st.st_mode & S_ISGID) != 0) return FT_SETGID;
      if ((st.st_mode & S_IWGRP) != 0 && (st.st_mode & S_ISVTX) != 0) return FT_DIR_OW_STICKY;
      if ((st.st_mode & S_IWGRP)) return FT_DIR_OW;
      if ((st.st_mode & S_ISVTX)) return FT_DIR_STICKY;
      return FT_DIR;
    }
    case S_IFREG:
    default: {
      if ((st.st_mode & S_IXUSR) != 0) return FT_EXE;
      return FT_DEFAULT;
    }
  }  
}


#define dir_cursor DIR*
#define dir_entry  struct dirent*

static bool os_findnext(dir_cursor d, dir_entry* entry) {
  *entry = readdir(d);
  return (*entry != NULL);
}

static bool os_findfirst(alloc_t* mem, const char* cpath, dir_cursor* d, dir_entry* entry) {
  ic_unused(mem);
  *d = opendir(cpath);
  if (*d == NULL) {
    return false;
  }
  else {
    return os_findnext(*d, entry);
  }
}

static void os_findclose(dir_cursor d) {
  closedir(d);
}

static const char* os_direntry_name(dir_entry* entry) {
  return (*entry)->d_name;  
}

static bool os_path_is_absolute( const char* path ) {
  return (path != NULL && path[0] == '/');
}

ic_private char ic_dirsep(void) {
  return '/';
}
#endif



//-------------------------------------------------------------
// File completion 
//-------------------------------------------------------------

static bool ends_with_n(const char* name, ssize_t name_len, const char* ending, ssize_t len) {
  if (name_len < len) return false;
  if (ending == NULL || len <= 0) return true;
  for (ssize_t i = 1; i <= len; i++) {
    char c1 = name[name_len - i];
    char c2 = ending[len - i];
    #ifdef _WIN32
    if (ic_tolower(c1) != ic_tolower(c2)) return false;
    #else
    if (c1 != c2) return false;
    #endif
  }
  return true;
}

static bool match_extension(const char* name, const char* extensions) {
  if (extensions == NULL || extensions[0] == 0) return true;
  if (name == NULL) return false;
  ssize_t name_len = ic_strlen(name);
  ssize_t len = ic_strlen(extensions);
  ssize_t cur = 0;  
  //debug_msg("match extensions: %s ~ %s", name, extensions);
  for (ssize_t end = 0; end <= len; end++) {
    if (extensions[end] == ';' || extensions[end] == 0) {
      if (ends_with_n(name, name_len, extensions+cur, (end - cur))) {
        return true;
      }
      cur = end+1;
    }
  }
  return false;
}

static bool filename_complete_indir( ic_completion_env_t* cenv, stringbuf_t* dir, 
                                      stringbuf_t* dir_prefix, stringbuf_t* display,
                                       const char* base_prefix, 
                                        char dir_sep, const char* extensions ) 
{
  dir_cursor d = 0;
  dir_entry entry;
  bool cont = true;
  if (os_findfirst(cenv->env->mem, sbuf_string(dir), &d, &entry)) {
    do {
      const char* name = os_direntry_name(&entry);
      if (name != NULL && strcmp(name, ".") != 0 && strcmp(name, "..") != 0 && 
          ic_istarts_with(name, base_prefix))
      {
        // possible match, first check if it is a directory
        file_type_t ft;
        bool isdir;
        const ssize_t plen = sbuf_len(dir_prefix);
        sbuf_append(dir_prefix, name);
        { // check directory and potentially add a dirsep to the dir_prefix
          const ssize_t dlen = sbuf_len(dir);
          sbuf_append_char(dir,ic_dirsep());
          sbuf_append(dir,name);
          ft = os_get_filetype(sbuf_string(dir));
          isdir = os_is_dir(sbuf_string(dir));
          if (isdir && dir_sep != 0) {
            sbuf_append_char(dir_prefix,dir_sep); 
          }
          sbuf_delete_from(dir,dlen);  // restore dir
        }
        if (isdir || match_extension(name, extensions)) {
          // add completion
          sbuf_clear(display);
          ls_colorize(cenv->env->no_lscolors, display, ft, name, NULL, (isdir ? dir_sep : 0));
          cont = ic_add_completion_ex(cenv, sbuf_string(dir_prefix), sbuf_string(display), NULL);
        }
        sbuf_delete_from( dir_prefix, plen ); // restore dir_prefix
      }
    } while (cont && os_findnext(d, &entry));
    os_findclose(d);
  }
  return cont;
}

typedef struct filename_closure_s {
  const char* roots;
  const char* extensions;
  char        dir_sep;
} filename_closure_t;

static void filename_completer( ic_completion_env_t* cenv, const char* prefix ) {
  if (prefix == NULL) return;
  filename_closure_t* fclosure = (filename_closure_t*)cenv->arg;  
  stringbuf_t* root_dir   = sbuf_new(cenv->env->mem);
  stringbuf_t* dir_prefix = sbuf_new(cenv->env->mem);
  stringbuf_t* display    = sbuf_new(cenv->env->mem);  
  if (root_dir!=NULL && dir_prefix != NULL && display != NULL) 
  {
    // split prefix in dir_prefix / base.
    const char* base = strrchr(prefix,'/');
    #ifdef _WIN32
    const char* base2 = strrchr(prefix,'\\');
    if (base == NULL || base2 > base) base = base2;
    #endif
    if (base != NULL) {
      base++; 
      sbuf_append_n(dir_prefix, prefix, base - prefix ); // includes dir separator
    }

    // absolute path
    if (os_path_is_absolute(prefix)) {
      // do not use roots but try to complete directly
      if (base != NULL) {
        sbuf_append_n( root_dir, prefix, (base - prefix));  // include dir separator
      }
      filename_complete_indir( cenv, root_dir, dir_prefix, display,  
                                (base != NULL ? base : prefix), 
                                 fclosure->dir_sep, fclosure->extensions );   
    }
    else {
      // relative path, complete with respect to every root.
      const char* next;
      const char* root = fclosure->roots;
      while ( root != NULL ) {
        // create full root in `root_dir`
        sbuf_clear(root_dir);
        next = strchr(root,';');
        if (next == NULL) {
          sbuf_append( root_dir, root );
          root = NULL;
        }
        else {
          sbuf_append_n( root_dir, root, next - root );
          root = next + 1;
        }      
        sbuf_append_char( root_dir, ic_dirsep());
          
        // add the dir_prefix to the root
        if (base != NULL) {
          sbuf_append_n( root_dir, prefix, (base - prefix) - 1);
        }

        // and complete in this directory    
        filename_complete_indir( cenv, root_dir, dir_prefix, display,
                                  (base != NULL ? base : prefix), 
                                   fclosure->dir_sep, fclosure->extensions);
      }
    }
  }
  sbuf_free(display);
  sbuf_free(root_dir);
  sbuf_free(dir_prefix);
}

ic_public void ic_complete_filename( ic_completion_env_t* cenv, const char* prefix, char dir_sep, const char* roots, const char* extensions ) {
  if (roots == NULL) roots = ".";
  if (extensions == NULL) extensions = "";
  if (dir_sep == 0) dir_sep = ic_dirsep();
  filename_closure_t fclosure;
  fclosure.dir_sep = dir_sep;
  fclosure.roots = roots; 
  fclosure.extensions = extensions;
  cenv->arg = &fclosure;
  ic_complete_qword_ex( cenv, prefix, &filename_completer, &ic_char_is_filename_letter, '\\', "'\"");  
}