/* ---------------------------------------------------------------------------- Copyright (c) 2021, Daan Leijen This is free software; you can redistribute it and/or modify it under the terms of the MIT License. A copy of the license can be found in the "LICENSE" file at the root of this distribution. -----------------------------------------------------------------------------*/ #include #include #include "../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; 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 return (*wenv->prev_complete)(env, wenv->prev_env, replacement, display, help, wenv->delete_before_adjust + delete_before, 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; 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; 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 return (*wenv->prev_complete)( env, wenv->prev_env, sbuf_string(wenv->sbuf), display, help, wenv->delete_before_adjust + delete_before, 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.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 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 #include 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 #include #include #include 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, '\\', "'\""); }