4 Copyright (C) 2000 Timo Sirainen
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 #include "lib-config/iconfig.h"
28 #include "completion.h"
30 #define wordreplace_find(word) \
31 iconfig_list_find("replaces", "text", word, "replace")
33 #define completion_find(completion) \
34 iconfig_list_find("completions", "short", completion, "long")
36 static GList *complist; /* list of commands we're currently completing */
37 static char *last_line;
38 static int last_want_space, last_line_pos;
40 #define isseparator_notspace(c) \
43 #define isseparator(c) \
44 (isspace((int) (c)) || isseparator_notspace(c))
46 void chat_completion_init(void);
47 void chat_completion_deinit(void);
49 /* Return whole word at specified position in string */
50 char *get_word_at(const char *str, int pos, char **startpos)
52 const char *start, *end;
54 g_return_val_if_fail(str != NULL, NULL);
55 g_return_val_if_fail(pos >= 0, NULL);
57 /* get previous word if char at `pos' is space */
59 while (start > str && isseparator(start[-1])) start--;
62 while (start > str && !isseparator(start[-1])) start--;
63 while (*end != '\0' && !isseparator(*end)) end++;
64 while (*end != '\0' && isseparator_notspace(*end)) end++;
66 *startpos = (char *) start;
67 return g_strndup(start, (int) (end-start));
70 /* automatic word completion - called when space/enter is pressed */
71 char *auto_word_complete(const char *line, int *pos)
75 char *word, *wordstart, *ret;
78 g_return_val_if_fail(line != NULL, NULL);
79 g_return_val_if_fail(pos != NULL, NULL);
81 word = get_word_at(line, *pos, &wordstart);
82 startpos = (int) (wordstart-line);
84 result = g_string_new(line);
85 g_string_erase(result, startpos, strlen(word));
87 /* check for words in autocompletion list */
88 replace = wordreplace_find(word);
89 if (replace == NULL) {
91 g_string_free(result, TRUE);
93 *pos = startpos+strlen(replace);
95 g_string_insert(result, startpos, replace);
97 g_string_free(result, FALSE);
104 static void free_completions(void)
106 complist = g_list_first(complist);
108 g_list_foreach(complist, (GFunc) g_free, NULL);
109 g_list_free(complist);
112 g_free_and_null(last_line);
115 /* manual word completion - called when TAB is pressed */
116 char *word_complete(WINDOW_REC *window, const char *line, int *pos)
118 static int startpos = 0, wordlen = 0;
121 char *word, *wordstart, *linestart, *ret;
124 g_return_val_if_fail(line != NULL, NULL);
125 g_return_val_if_fail(pos != NULL, NULL);
127 if (complist != NULL && *pos == last_line_pos &&
128 strcmp(line, last_line) == 0) {
129 /* complete from old list */
130 complist = complist->next != NULL ? complist->next :
131 g_list_first(complist);
132 want_space = last_want_space;
134 /* get new completion list */
137 /* get the word we want to complete */
138 word = get_word_at(line, *pos, &wordstart);
139 startpos = (int) (wordstart-line);
140 wordlen = strlen(word);
142 /* get the start of line until the word we're completing */
143 if (isseparator(*line)) {
144 /* empty space at the start of line */
145 if (wordstart == line)
146 wordstart += strlen(wordstart);
148 while (wordstart > line && isseparator(wordstart[-1]))
151 linestart = g_strndup(line, (int) (wordstart-line));
153 /* completions usually add space after the word, that makes
154 things a bit harder. When continuing a completion
155 "/msg nick1 "<tab> we have to cycle to nick2, etc.
156 BUT if we start completion with "/msg "<tab>, we don't
157 want to complete the /msg word, but instead complete empty
158 word with /msg being in linestart. */
159 if (*pos > 0 && line[*pos-1] == ' ') {
163 linestart = *linestart == '\0' ?
165 g_strconcat(linestart, " ", word, NULL);
170 startpos = strlen(linestart)+1;
175 signal_emit("complete word", 5, &complist, window, word, linestart, &want_space);
176 last_want_space = want_space;
182 if (complist == NULL)
186 *pos = startpos+strlen(complist->data);
188 /* replace the word in line - we need to return
190 result = g_string_new(line);
191 g_string_erase(result, startpos, wordlen);
192 g_string_insert(result, startpos, complist->data);
195 if (!isseparator(result->str[*pos]))
196 g_string_insert_c(result, *pos, ' ');
200 wordlen = strlen(complist->data);
201 last_line_pos = *pos;
202 g_free_not_null(last_line);
203 last_line = g_strdup(result->str);
206 g_string_free(result, FALSE);
210 GList *list_add_file(GList *list, const char *name)
215 g_return_val_if_fail(name != NULL, NULL);
217 fname = convert_home(name);
218 if (stat(fname, &statbuf) == 0) {
219 list = g_list_append(list, !S_ISDIR(statbuf.st_mode) ? g_strdup(name) :
220 g_strconcat(name, G_DIR_SEPARATOR_S, NULL));
227 GList *filename_complete(const char *path)
232 char *realpath, *dir, *basename, *name;
235 g_return_val_if_fail(path != NULL, NULL);
239 /* get directory part of the path - expand ~/ */
240 realpath = convert_home(path);
241 dir = g_dirname(realpath);
244 /* open directory for reading */
247 if (dirp == NULL) return NULL;
249 dir = g_dirname(path);
250 if (*dir == G_DIR_SEPARATOR && dir[1] == '\0')
251 *dir = '\0'; /* completing file in root directory */
252 basename = g_basename(path);
253 len = strlen(basename);
255 /* add all files in directory to completion list */
256 while ((dp = readdir(dirp)) != NULL) {
257 if (dp->d_name[0] == '.') {
258 if (dp->d_name[1] == '\0' ||
259 (dp->d_name[1] == '.' && dp->d_name[2] == '\0'))
260 continue; /* skip . and .. */
262 if (basename[0] != '.')
266 if (len == 0 || strncmp(dp->d_name, basename, len) == 0) {
267 name = g_strdup_printf("%s"G_DIR_SEPARATOR_S"%s", dir, dp->d_name);
268 list = list_add_file(list, name);
278 static GList *completion_get_settings(const char *key)
284 g_return_val_if_fail(key != NULL, NULL);
286 sets = settings_get_sorted();
290 for (tmp = sets; tmp != NULL; tmp = tmp->next) {
291 SETTINGS_REC *rec = tmp->data;
293 if (g_strncasecmp(rec->key, key, len) == 0)
294 complist = g_list_insert_sorted(complist, g_strdup(rec->key), (GCompareFunc) g_istr_cmp);
300 static GList *completion_get_bool_settings(const char *key)
306 g_return_val_if_fail(key != NULL, NULL);
308 sets = settings_get_sorted();
312 for (tmp = sets; tmp != NULL; tmp = tmp->next) {
313 SETTINGS_REC *rec = tmp->data;
315 if (rec->type == SETTING_TYPE_BOOLEAN &&
316 g_strncasecmp(rec->key, key, len) == 0)
317 complist = g_list_insert_sorted(complist, g_strdup(rec->key), (GCompareFunc) g_istr_cmp);
323 static GList *completion_get_aliases(const char *alias, char cmdchar)
331 g_return_val_if_fail(alias != NULL, NULL);
333 /* get list of aliases from mainconfig */
334 node = iconfig_node_traverse("aliases", FALSE);
335 tmp = node == NULL ? NULL : node->value;
339 for (; tmp != NULL; tmp = tmp->next) {
340 CONFIG_NODE *node = tmp->data;
342 if (node->type != NODE_TYPE_KEY)
345 if (g_strncasecmp(node->key, alias, len) == 0) {
346 word = g_strdup_printf("%c%s", cmdchar, node->key);
347 /* add matching alias to completion list, aliases will
348 be appended after command completions and kept in
349 uppercase to show it's an alias */
350 if (glist_find_icase_string(complist, word) == NULL)
351 complist = g_list_insert_sorted(complist, word, (GCompareFunc) g_istr_cmp);
359 static GList *completion_get_commands(const char *cmd, char cmdchar)
366 g_return_val_if_fail(cmd != NULL, NULL);
370 for (tmp = commands; tmp != NULL; tmp = tmp->next) {
371 COMMAND_REC *rec = tmp->data;
373 if (strchr(rec->cmd, ' ') != NULL)
376 if (g_strncasecmp(rec->cmd, cmd, len) == 0) {
377 word = cmdchar == '\0' ? g_strdup(rec->cmd) :
378 g_strdup_printf("%c%s", cmdchar, rec->cmd);
379 if (glist_find_icase_string(complist, word) == NULL)
380 complist = g_list_insert_sorted(complist, word, (GCompareFunc) g_istr_cmp);
388 static GList *completion_get_subcommands(const char *cmd)
395 g_return_val_if_fail(cmd != NULL, NULL);
397 /* get the number of chars to skip at the start of command. */
398 spacepos = strrchr(cmd, ' ');
399 skip = spacepos == NULL ? strlen(cmd)+1 :
400 ((int) (spacepos-cmd) + 1);
404 for (tmp = commands; tmp != NULL; tmp = tmp->next) {
405 COMMAND_REC *rec = tmp->data;
407 if ((int)strlen(rec->cmd) < len)
410 if (strchr(rec->cmd+len, ' ') != NULL)
413 if (g_strncasecmp(rec->cmd, cmd, len) == 0)
414 complist = g_list_insert_sorted(complist, g_strdup(rec->cmd+skip), (GCompareFunc) g_istr_cmp);
419 GList *completion_get_options(const char *cmd, const char *option)
426 g_return_val_if_fail(cmd != NULL, NULL);
427 g_return_val_if_fail(option != NULL, NULL);
429 rec = command_find(cmd);
430 if (rec == NULL || rec->options == NULL) return NULL;
433 len = strlen(option);
434 for (tmp = rec->options; *tmp != NULL; tmp++) {
435 const char *optname = *tmp + iscmdtype(**tmp);
437 if (len == 0 || g_strncasecmp(optname, option, len) == 0)
438 list = g_list_append(list, g_strconcat("-", optname, NULL));
444 /* split the line to command and arguments */
445 static char *line_get_command(const char *line, char **args, int aliases)
447 const char *ptr, *cmdargs;
448 char *cmd, *checkcmd;
450 g_return_val_if_fail(line != NULL, NULL);
451 g_return_val_if_fail(args != NULL, NULL);
453 cmd = checkcmd = NULL; *args = "";
454 cmdargs = NULL; ptr = line;
457 ptr = strchr(ptr, ' ');
459 checkcmd = g_strdup(line);
462 checkcmd = g_strndup(line, (int) (ptr-line));
464 while (isspace(*ptr)) ptr++;
468 if (aliases ? !alias_find(checkcmd) :
469 !command_find(checkcmd)) {
470 /* not found, use the previous */
475 /* found, check if it has subcommands */
476 g_free_not_null(cmd);
480 cmd = g_strdup(alias_find(checkcmd));
483 *args = (char *) cmdargs;
484 } while (ptr != NULL);
489 static char *expand_aliases(const char *line)
491 char *cmd, *args, *ret;
493 g_return_val_if_fail(line != NULL, NULL);
495 cmd = line_get_command(line, &args, TRUE);
496 if (cmd == NULL) return g_strdup(line);
497 if (*args == '\0') return cmd;
499 ret = g_strconcat(cmd, " ", args, NULL);
504 static void sig_complete_word(GList **list, WINDOW_REC *window,
505 const char *word, const char *linestart, int *want_space)
507 const char *newword, *cmdchars;
508 char *signal, *cmd, *args, *line;
510 g_return_if_fail(list != NULL);
511 g_return_if_fail(word != NULL);
512 g_return_if_fail(linestart != NULL);
514 /* check against "completion words" list */
515 newword = completion_find(word);
516 if (newword != NULL) {
517 *list = g_list_append(*list, g_strdup(newword));
523 /* command completion? */
524 cmdchars = settings_get_str("cmdchars");
525 if (strchr(cmdchars, *word) && *linestart == '\0') {
526 /* complete /command */
527 *list = completion_get_commands(word+1, *word);
529 /* complete aliases, too */
530 *list = g_list_concat(*list,
531 completion_get_aliases(word+1, *word));
533 if (*list != NULL) signal_stop();
537 /* check only for /command completions from now on */
538 if (*linestart == '\0')
541 cmdchars = strchr(cmdchars, *linestart);
542 if (cmdchars == NULL) return;
544 /* check if there's aliases */
545 line = linestart[1] == *cmdchars ? g_strdup(linestart+2) :
546 expand_aliases(linestart+1);
548 cmd = line_get_command(line, &args, FALSE);
554 /* we're completing -option? */
556 *list = completion_get_options(cmd, word+1);
562 /* complete parameters */
563 signal = g_strconcat("complete command ", cmd, NULL);
564 signal_emit(signal, 5, list, window, word, args, want_space);
566 if (command_have_sub(line)) {
567 /* complete subcommand */
569 cmd = g_strconcat(line, " ", word, NULL);
570 *list = g_list_concat(completion_get_subcommands(cmd), *list);
572 if (*list != NULL) signal_stop();
581 static void sig_complete_set(GList **list, WINDOW_REC *window,
582 const char *word, const char *line, int *want_space)
584 g_return_if_fail(list != NULL);
585 g_return_if_fail(word != NULL);
586 g_return_if_fail(line != NULL);
588 if (*line != '\0') return;
590 *list = completion_get_settings(word);
591 if (*list != NULL) signal_stop();
594 static void sig_complete_toggle(GList **list, WINDOW_REC *window,
595 const char *word, const char *line, int *want_space)
597 g_return_if_fail(list != NULL);
598 g_return_if_fail(word != NULL);
599 g_return_if_fail(line != NULL);
601 if (*line != '\0') return;
603 *list = completion_get_bool_settings(word);
604 if (*list != NULL) signal_stop();
607 /* first argument of command is file name - complete it */
608 static void sig_complete_filename(GList **list, WINDOW_REC *window,
609 const char *word, const char *line, int *want_space)
611 g_return_if_fail(list != NULL);
612 g_return_if_fail(word != NULL);
613 g_return_if_fail(line != NULL);
615 if (*line != '\0') return;
617 *list = filename_complete(word);
624 /* first argument of command is .. command :) (/HELP command) */
625 static void sig_complete_command(GList **list, WINDOW_REC *window,
626 const char *word, const char *line, int *want_space)
630 g_return_if_fail(list != NULL);
631 g_return_if_fail(word != NULL);
632 g_return_if_fail(line != NULL);
635 /* complete base command */
636 *list = completion_get_commands(word, '\0');
637 } else if (command_have_sub(line)) {
638 /* complete subcommand */
639 cmd = g_strconcat(line, " ", word, NULL);
640 *list = completion_get_subcommands(cmd);
644 if (*list != NULL) signal_stop();
647 void completion_init(void)
650 last_line = NULL; last_line_pos = -1;
652 chat_completion_init();
654 signal_add_first("complete word", (SIGNAL_FUNC) sig_complete_word);
655 signal_add("complete command set", (SIGNAL_FUNC) sig_complete_set);
656 signal_add("complete command toggle", (SIGNAL_FUNC) sig_complete_toggle);
657 signal_add("complete command cat", (SIGNAL_FUNC) sig_complete_filename);
658 signal_add("complete command run", (SIGNAL_FUNC) sig_complete_filename);
659 signal_add("complete command save", (SIGNAL_FUNC) sig_complete_filename);
660 signal_add("complete command reload", (SIGNAL_FUNC) sig_complete_filename);
661 signal_add("complete command rawlog open", (SIGNAL_FUNC) sig_complete_filename);
662 signal_add("complete command rawlog save", (SIGNAL_FUNC) sig_complete_filename);
663 signal_add("complete command help", (SIGNAL_FUNC) sig_complete_command);
666 void completion_deinit(void)
670 chat_completion_deinit();
672 signal_remove("complete word", (SIGNAL_FUNC) sig_complete_word);
673 signal_remove("complete command set", (SIGNAL_FUNC) sig_complete_set);
674 signal_remove("complete command toggle", (SIGNAL_FUNC) sig_complete_toggle);
675 signal_remove("complete command cat", (SIGNAL_FUNC) sig_complete_filename);
676 signal_remove("complete command run", (SIGNAL_FUNC) sig_complete_filename);
677 signal_remove("complete command save", (SIGNAL_FUNC) sig_complete_filename);
678 signal_remove("complete command reload", (SIGNAL_FUNC) sig_complete_filename);
679 signal_remove("complete command rawlog open", (SIGNAL_FUNC) sig_complete_filename);
680 signal_remove("complete command rawlog save", (SIGNAL_FUNC) sig_complete_filename);
681 signal_remove("complete command help", (SIGNAL_FUNC) sig_complete_command);