OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-link-grammar / link-parser / lg_readline.c

508 lines · c ref:
/***************************************************************************/
/* Copyright (c) 2012 Linas Vepstas                                        */
/* All rights reserved                                                     */
/*                                                                         */
/* Use of the link grammar parsing system is subject to the terms of the   */
/* license set forth in the LICENSE file included with this software.      */
/* This license allows free redistribution and use in source and binary    */
/* forms, with or without modification, subject to certain conditions.     */
/*                                                                         */
/***************************************************************************/

/**
 * Arghhhh. This hacks around multiple stupidities in readline/editline.
 * 1) most versions of editline don't have wide-char support.
 * 2) No versions of editline have UTF8 support.
 * So basically readline() is just plain broken.
 * So hack one up, using the wide-char interfaces.  This is a hack. Argh.
 *
 * Double-arghh.  Current versions of readline hang in an infinite loop
 * on __read_nocancel() in read_char() called from el_wgets() (line 92
 * below) when the input is "He said 《 This is bull shit 》" Notice
 * the unicode angle-brackets.
 */

#include "lg_readline.h"

#ifdef HAVE_EDITLINE
#include <string.h>
#include <histedit.h>
#include <stdlib.h>

#ifdef HAVE_WIDECHAR_EDITLINE
#include <stdbool.h>
#include <fcntl.h>                     // open flags
#if defined O_DIRECTORY && defined O_PATH
#include <unistd.h>                    // fchdir
#include <errno.h>
#endif

#include "command-line.h"
#include "parser-utilities.h"
#include "lg_xdg.h"

extern Switch default_switches[];
static const Switch **sorted_names; /* sorted command names */
static wchar_t * wc_prompt = NULL;
static wchar_t * prompt(EditLine *el)
{
	return wc_prompt;
}

// lg_readline() is called via a chain of functions:
// fget_input_string -> get_line -> get_terminal_line -> lg_readline.
// To avoid changing all of them, this variable is static for now.
// FIXME: Move the call of find_history_filepath() to lg_readline(), and
// implement one of these:
// 1. Add the dictionary, argv[0] and prog to the call of each function.
// 2. Option 1 is cumbersome since the first 3 functions have numerous
//    arguments. So replace them with a new struct "io_params".
static const char *history_file;

static const char history_file_basename[] = "_history";

void find_history_filepath(const char *dictname, const char *argv0,
                           const char *prog)
{
	const char *xdg_prog = xdg_get_program_name(argv0);
	if (NULL == xdg_prog) xdg_prog = prog;

	// Prefix the history file name with a dict indication.
	// To support sharing the history file by several similar dicts,
	// use the dict name up to the first ":" if exists.
	char *dictpref = strdup(dictname);
	dictpref[strcspn(dictpref, ":")] = '\0';

	// Find the location of the history file.
	history_file = xdg_make_path(XDG_BD_STATE, "%s/%s%s", xdg_prog,
	                             dictpref, history_file_basename);
	free(dictpref);

	if (NULL == history_file)
	{
		prt_error("Warning: xdg_get_home(XDG_BD_STATE) failed; "
		          "input history will not be supported.\n");
		history_file = strdup("/dev/null");
	}

	if (get_verbosity() == D_USER_FILES)
		prt_error("Debug: Using history file \"%s\"\n", history_file);
}

/**
 * Try to complete the wide string in \p input, whose length is \p len.
 *
 * @param input Non-NUL-terminated wide string with length \p len.
 * @param len Number of wide characters in \p input.
 * @param is_help \p input is the argument of an !help command.
 * @return 3 types of values:
 *  <code>""</code> There are no completions.
 *  <code>NULL</code> Input not supported, or choices help has been printed.
 *  A NUL-terminated byte string (to be used as a completion).
 */
static char *complete_command(const wchar_t *input, size_t len, bool is_help)
{
	const Switch **start = NULL;
	const Switch **end;
	const Switch **match;
	const char *prev = NULL;
	size_t addlen = 0;
	bool is_assignment = false; /* marking for the help facility */

	if ((1 < len) && L'=' == input[len-1] && !is_help)
	{
		/* This is a variable assignment without a value.
		 * Arrange for displaying an help line (only - no completion). */
		is_assignment = true;
		len--; /* disregard the ending '=' to enable command search. */
	}

	/* Our commands are ASCII strings.
	 * So for simplicity, convert the input to an ASCII string. */
	char *astr = malloc(len+1);
	for (size_t i = 0; i < len; i++)
	{
		if (input[i] <= 0 || input[i] > 127)
		{
			free(astr);
			return NULL; /* unsupported input */
		}
		astr[i] = (char)input[i];
	}
	astr[len] = '\0';

	/* Find the possible completions. */
	for (match = sorted_names; NULL != *match; match++)
	{
		if (UNDOC[0] == (*match)->description[0]) continue;
		if (0 == strncmp(astr, (*match)->string, len))
		{
			if (NULL == start)
			{
				start = match;
				addlen = strlen((*match)->string) - len;
			}
			else
			{
				/* There is more than one match. Find the maximal common
				 * additional substring that we can append. */
				for (; addlen  > 0; addlen--)
				{
					if (0 == strncmp((*match)->string+len, prev+len, addlen))
					    break;
				}
			}
			prev = (*match)->string;
		}
		else if (NULL != start) break;
	}
	free(astr);
	end = match;

	if (NULL == start) return strdup(""); /* nothing found - no completions */
	int cnum = end - start;

	/* Show a possible completion list in a form of 1-line command help,
	 * for these cases:
	 * 1. Multiple completions with no common substring to add.
	 * 2. An assignment command.
	 */
	if (((cnum > 1) && (0 == addlen)) || is_assignment)
	{
		bool all_commands = true;

		printf("\n");
		if (is_assignment)
		{
			for (size_t i = 0; &start[i] < end; i++)
				if ((Cmd != start[i]->param_type)) all_commands = false;
		}
		do
		{
			if (UNDOC[0] == (*start)->description[0]) continue;
			if (is_assignment && all_commands)
			{
				printf("\"%s\" is not a user variable\n", (*start)->string);
			}
			else
			{
				if (!is_assignment || (Cmd != (*start)->param_type))
					display_1line_help(*start, /*is_completion*/true);
			}
		}
		while (++start < end);

		return NULL;
	}

	/* Here addlen>0, so we have 1 or more possible completions. */
	char *addstr = malloc(addlen + 2); /* + '=' + '\0' */
	strncpy(addstr, (*start)->string + len, addlen);

	if ((1 == cnum) && !is_help)
	{
		/* A single completion.  Indicate a success:
		 * - For a non-Boolean user variable appending '='.
		 * - For a command or a Boolean append ' '. */
		addstr[addlen++] =
			((Cmd != (*start)->param_type) && (Bool != (*start)->param_type))
			? '=' : ' ';
	}
	else
	{
		/* Multiple completions. The maximal common substring will be returned. */
	}

	addstr[addlen] = '\0';
	return addstr;
}

static int by_byteorder(const void *a, const void *b)
{
	const Switch * const *sa = a;
	const Switch * const *sb = b;

	return strcmp((*sa)->string, (*sb)->string);
}

static void build_command_list(const Switch ds[])
{
	size_t cl_num = 0;
	for (size_t i = 0; NULL != ds[i].string; i++)
	{
		if (UNDOC[0] == ds[i].description[0]) continue;
		cl_num++;
	}
	sorted_names = malloc((cl_num+1) * sizeof(*sorted_names));

	int j = 0;
	for (size_t i = 0; NULL != ds[i].string; i++)
	{
		if (UNDOC[0] == ds[i].description[0]) continue;
		sorted_names[j++] = &ds[i];
	}
	sorted_names[cl_num] = NULL;
	qsort(sorted_names, cl_num, sizeof(*sorted_names), by_byteorder);
}

/**
 * Complete variables / commands and also file name after "!file ".
 * Filenames with blanks are not supported well.
 * This is partially a problem of editline, so there is no point to
 * fix that.
 *
 * There is a special treatment of the !help command since it can be
 * abbreviated up to one character and its argument position also
 * needs a command completion (which is slightly different - no '=' is
 * appended to variables).
 */
static unsigned char lg_complete(EditLine *el, int ch)
{
	const LineInfoW *li = el_wline(el);
	const wchar_t *word_start; /* the word to be completed */
	const wchar_t *word_end;
	size_t word_len;
	const wchar_t *ctemp;
	unsigned char rc;
	bool is_file_command = false;
	bool is_help_command = false;

	/* A command must start with '!' on column 0. */
	if (L'!' != li->buffer[0])
		return CC_ERROR;

	/* Allow for whitespace after the initial "!" */
	for (ctemp = li->buffer+1; ctemp < li->lastchar; ctemp++)
		if (!iswspace(*ctemp)) break;

	word_start = ctemp;

	/* Find the word (if any) end. */
	for (ctemp = word_start; ctemp < li->lastchar; ctemp++)
		if (iswspace(*ctemp)) break;

	word_end = ctemp;
	word_len = word_end - word_start;

	if (0 < word_len)
	{
		/* Don't try to complete in a middle of word. */
		if ((li->cursor >= word_start) && ((li->cursor < word_end)))
			return CC_ERROR;

		/* Now check if it is !help or !file (or their abbreviation), as
		 * these commands need an argument completion. Whitespace is
		 * needed after these commands as else this is a command completion. */
		if (iswspace(*word_end))
		{
			if (0 == wcsncmp(word_start, L"help", word_len))
				is_help_command = true;
			else if (0 == wcsncmp(word_start, L"file", word_len))
				is_file_command = true;

			if (is_file_command || is_help_command)
			{
				/* Check if the command has an argument. */
				for (ctemp = word_end + 1; ctemp < li->lastchar; ctemp++)
					if (!iswspace(*ctemp)) break;
				word_start = ctemp;
				for (ctemp = word_start; ctemp < li->lastchar; ctemp++)
					if (iswspace(*ctemp)) break;
				word_end = ctemp;
				word_len = word_end - word_start;
			}
		}
	}

	/* Cannot complete if the cursor is not directly after the word. */
	if ((0 < word_len) && li->cursor != word_end) return CC_ERROR;

	/* Cannot complete if there is non-whitespace after the cursor. */
	for (ctemp = word_end; ctemp < li->lastchar; ctemp++)
		if (!iswspace(*ctemp)) return CC_ERROR;

	if (is_file_command)
	{
		/* The code before _el_fn_complete() supports printing the
		 * completion possibilities without a directory name prefix, as
		 * normally displayed by most programs. The code after
		 * _el_fn_complete() restores its temporary changes. */
#if defined O_DIRECTORY && defined O_PATH
		wchar_t *dn_end = wcsrchr(word_start, L'/');
		int cwdfd = -1;

		if (dn_end != NULL)
		{
			cwdfd = open(".", O_DIRECTORY|O_PATH);
			if (cwdfd == -1)
			{
				printf("Error: Open current directory: %s\n", strerror(errno));
			}
			else
			{
				wchar_t *wdirname = wcsdup(word_start);
				wdirname[dn_end - word_start] = L'\0';

				/* Original buffer is const, but never mind. Restored below. */
				*dn_end = L' '; /* Not saved - supposing it was L'/' */

				size_t byte_len = wcstombs(NULL, wdirname, 0);
				if (byte_len == (size_t)-1)
				{
					printf("Error: Unable to process non-ASCII in directory name.\n");
					free(wdirname);
					return CC_ERROR;
				}
				char *dirname = malloc(byte_len + 1);
				wcstombs(dirname, wdirname, byte_len + 1);
				free(wdirname);
				char *eh_dirname = expand_homedir(dirname);
				free(dirname);
				int chdir_status = chdir(eh_dirname);
				free(eh_dirname);
				if (chdir_status == -1)
				{
					*dn_end = L'/';
				}
			}
		}
#endif

		rc = _el_fn_complete(el, ch);

#if defined O_DIRECTORY && defined O_PATH
		if (cwdfd != -1)
		{
			if (fchdir(cwdfd) < 0)
			{
				/* Unexpected error: may occur only if the directory
				 * referenced by cwdfd becomes unreadable after cwdfd
				 * has been established. */
				printf("\nfchdir(): Cannot change directory back: %s\n",
				       strerror(errno));
			}
			close(cwdfd);
			*dn_end = L'/';
		}
#endif

		/* CC_NORM is returned if there is no possible completion. */
		return (rc == CC_NORM) ? CC_ERROR : rc;
	}

	char *completion = complete_command(word_start, word_len, is_help_command);
	if (NULL == completion)
		rc = CC_REDISPLAY; /* completions got printed - redraw the input line */
	else if ('\0' == completion[0])
		rc =  CC_NORM;     /* nothing to complete */
	else if (el_insertstr(el, completion) == -1)
		rc = CC_ERROR;     /* no more space in the line buffer */
	else
		rc = CC_REFRESH;   /* there is a completion */

	free(completion);
	return (rc == CC_NORM) ? CC_ERROR : rc;
}

char *lg_readline(const char *mb_prompt)
{
	static bool is_init = false;
	static HistoryW *hist = NULL;
	static HistEventW ev;
	static EditLine *el = NULL;
	static char *mb_line = NULL;

	if (!is_init)
	{
		is_init = true;

		size_t sz = mbstowcs(NULL, mb_prompt, 0) + 4;
		wc_prompt = malloc (sz*sizeof(wchar_t));
		mbstowcs(wc_prompt, mb_prompt, sz);

		hist = history_winit(); /* Init built-in history */
		el = el_init("link-parser", stdin, stdout, stderr);
		history_w(hist, &ev, H_SETSIZE, 100);
		history_w(hist, &ev, H_SETUNIQUE, 1);
		el_wset(el, EL_HIST, history_w, hist);
		history_w(hist, &ev, H_LOAD, history_file);

		el_set(el, EL_SIGNAL, 1); /* Restore tty setting on returning to shell */

		/* By default, it comes up in vi mode, with the editor not in
		 * insert mode; and even when in insert mode, it drops back to
		 * command mode at the drop of a hat. Totally confusing/lame. */
		el_set(el, EL_EDITOR, "emacs");
		el_wset(el, EL_PROMPT, prompt); /* Set the prompt function */

		el_set(el, EL_ADDFN, "lg_complete", "command completion", lg_complete);
		el_set(el, EL_BIND, "^I", "lg_complete", NULL);

		build_command_list(default_switches);

		el_source(el, NULL); /* Source the user's defaults file. */
	}

	int numc = 1; /* Uninitialized at libedit. */
	const wchar_t *wc_line = el_wgets(el, &numc);

	/* Received end-of-file */
	if (numc <= 0)
	{
		el_end(el);
		history_wend(hist);
		free((void *)history_file);
		free(wc_prompt);
		wc_prompt = NULL;
		hist = NULL;
		el = NULL;
		is_init = false;
		return NULL;
	}

	if (1 < numc)
	{
		history_w(hist, &ev, H_ENTER, wc_line);
		history_w(hist, &ev, H_SAVE, history_file);
	}
	/* fwprintf(stderr, L"==> got %d %ls", numc, wc_line); */

	size_t byte_len = wcstombs(NULL, wc_line, 0) + 4;
	free(mb_line); // free previous.
	if (byte_len == (size_t)-1)
	{
		prt_error("Error: Unable to process UTF8 in input string.\n");
		mb_line = strdup(""); /* Just ignore the input line. */
		return mb_line;
	}
	mb_line = malloc(byte_len);
	wcstombs(mb_line, wc_line, byte_len);

	/* In order to be compatible with regular libedit, we have to
	 * strip away the trailing newline, if any. */
	char *nl = strchr(mb_line, '\n');
	if (nl) *nl = 0x0;

	return mb_line;
}

#else /* HAVE_WIDECHAR_EDITLINE */

#include <editline/readline.h>

char *lg_readline(const char *prompt)
{
	static char *pline = NULL;

	free(pline);
	pline = readline(prompt);

	/* Save non-blank lines */
	if (pline && *pline)
		add_history(pline);

	return pline;
}
#endif /* HAVE_WIDECHAR_EDITLINE */
#endif /* HAVE_EDITLINE */