Re: Making tab-complete.c easier to maintain
| От | Tom Lane |
|---|---|
| Тема | Re: Making tab-complete.c easier to maintain |
| Дата | |
| Msg-id | 14995.1450568170@sss.pgh.pa.us обсуждение исходный текст |
| Ответ на | Re: Making tab-complete.c easier to maintain (Tom Lane <tgl@sss.pgh.pa.us>) |
| Список | pgsql-hackers |
I wrote:
> 3. The HeadMatches macros are pretty iffy because they can only look back
> nine words. I'm tempted to redesign get_previous_words so it just
> tokenizes the whole line rather than having an arbitrary limitation.
> (For that matter, it's long overdue for it to be able to deal with
> multiline input...)
I poked into this and found that it's really not hard, if you don't mind
still another global variable associated with tab-completion. See
attached patch.
The main objection to this change might be speed, but I experimented with
the longest query in information_schema.sql (CREATE VIEW usage_privileges,
162 lines) and found that pressing tab at the end of that seemed to take
well under a millisecond on my workstation. So I think it's probably
insignificant, especially compared to any of the code paths that send a
query to the server for tab completion.
regards, tom lane
diff --git a/src/bin/psql/input.c b/src/bin/psql/input.c
index 2bc065a..ccd9a3e 100644
*** a/src/bin/psql/input.c
--- b/src/bin/psql/input.c
*************** static void finishInput(void);
*** 53,64 ****
* gets_interactive()
*
* Gets a line of interactive input, using readline if desired.
* The result is a malloc'd string.
*
* Caller *must* have set up sigint_interrupt_jmp before calling.
*/
char *
! gets_interactive(const char *prompt)
{
#ifdef USE_READLINE
if (useReadline)
--- 53,69 ----
* gets_interactive()
*
* Gets a line of interactive input, using readline if desired.
+ *
+ * prompt: the prompt string to be used
+ * query_buf: buffer containing lines already read in the current command
+ * (query_buf is not modified here, but may be consulted for tab completion)
+ *
* The result is a malloc'd string.
*
* Caller *must* have set up sigint_interrupt_jmp before calling.
*/
char *
! gets_interactive(const char *prompt, PQExpBuffer query_buf)
{
#ifdef USE_READLINE
if (useReadline)
*************** gets_interactive(const char *prompt)
*** 76,81 ****
--- 81,89 ----
rl_reset_screen_size();
#endif
+ /* Make current query_buf available to tab completion callback */
+ tab_completion_query_buf = query_buf;
+
/* Enable SIGINT to longjmp to sigint_interrupt_jmp */
sigint_interrupt_enabled = true;
*************** gets_interactive(const char *prompt)
*** 85,90 ****
--- 93,101 ----
/* Disable SIGINT again */
sigint_interrupt_enabled = false;
+ /* Pure neatnik-ism */
+ tab_completion_query_buf = NULL;
+
return result;
}
#endif
diff --git a/src/bin/psql/input.h b/src/bin/psql/input.h
index abd7012..c9d30b9 100644
*** a/src/bin/psql/input.h
--- b/src/bin/psql/input.h
***************
*** 38,44 ****
#include "pqexpbuffer.h"
! char *gets_interactive(const char *prompt);
char *gets_fromFile(FILE *source);
void initializeInput(int flags);
--- 38,44 ----
#include "pqexpbuffer.h"
! char *gets_interactive(const char *prompt, PQExpBuffer query_buf);
char *gets_fromFile(FILE *source);
void initializeInput(int flags);
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c
index b6cef94..cb86457 100644
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
*************** MainLoop(FILE *source)
*** 133,139 ****
/* May need to reset prompt, eg after \r command */
if (query_buf->len == 0)
prompt_status = PROMPT_READY;
! line = gets_interactive(get_prompt(prompt_status));
}
else
{
--- 133,139 ----
/* May need to reset prompt, eg after \r command */
if (query_buf->len == 0)
prompt_status = PROMPT_READY;
! line = gets_interactive(get_prompt(prompt_status), query_buf);
}
else
{
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 731df2a..fec7c38 100644
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
*************** extern char *filename_completion_functio
*** 70,75 ****
--- 70,82 ----
#define WORD_BREAKS "\t\n@$><=;|&{() "
/*
+ * Since readline doesn't let us pass any state through to the tab completion
+ * callback, we have to use this global variable to let get_previous_words()
+ * get at the previous lines of the current command. Ick.
+ */
+ PQExpBuffer tab_completion_query_buf = NULL;
+
+ /*
* This struct is used to define "schema queries", which are custom-built
* to obtain possibly-schema-qualified names of database objects. There is
* enough similarity in the structure that we don't want to repeat it each
*************** static char *pg_strdup_keyword_case(cons
*** 923,929 ****
static char *escape_string(const char *text);
static PGresult *exec_query(const char *query);
! static int get_previous_words(int point, char **previous_words, int nwords);
static char *get_guctype(const char *varname);
--- 930,936 ----
static char *escape_string(const char *text);
static PGresult *exec_query(const char *query);
! static char **get_previous_words(int point, char **buffer, int *nwords);
static char *get_guctype(const char *varname);
*************** psql_completion(const char *text, int st
*** 1027,1039 ****
/* This is the variable we'll return. */
char **matches = NULL;
! /* This array will contain some scannage of the input line. */
! char *previous_words[9];
/* The number of words found on the input line. */
int previous_words_count;
! /* For compactness, we use these macros to reference previous_words[]. */
#define prev_wd (previous_words[0])
#define prev2_wd (previous_words[1])
#define prev3_wd (previous_words[2])
--- 1034,1055 ----
/* This is the variable we'll return. */
char **matches = NULL;
! /* Workspace for parsed words. */
! char *words_buffer;
!
! /* This array will contain pointers to parsed words. */
! char **previous_words;
/* The number of words found on the input line. */
int previous_words_count;
! /*
! * For compactness, we use these macros to reference previous_words[].
! * Caution: do not access a previous_words[] entry without having checked
! * previous_words_count to be sure it's valid. In most cases below, that
! * check is implicit in a TailMatches() or similar macro, but in some
! * places we have to check it explicitly.
! */
#define prev_wd (previous_words[0])
#define prev2_wd (previous_words[1])
#define prev3_wd (previous_words[2])
*************** psql_completion(const char *text, int st
*** 1133,1141 ****
/*
* Macros for matching N words at the start of the line, regardless of
! * what is after them. These are pretty broken because they can only look
! * back lengthof(previous_words) words, but we'll worry about improving
! * their implementation some other day.
*/
#define HeadMatches1(p1) \
(previous_words_count >= 1 && \
--- 1149,1155 ----
/*
* Macros for matching N words at the start of the line, regardless of
! * what is after them.
*/
#define HeadMatches1(p1) \
(previous_words_count >= 1 && \
*************** psql_completion(const char *text, int st
*** 1194,1206 ****
completion_info_charp2 = NULL;
/*
! * Scan the input line before our current position for the last few words.
* According to those we'll make some smart decisions on what the user is
* probably intending to type.
*/
! previous_words_count = get_previous_words(start,
! previous_words,
! lengthof(previous_words));
/* If current word is a backslash command, offer completions for that */
if (text[0] == '\\')
--- 1208,1220 ----
completion_info_charp2 = NULL;
/*
! * Scan the input line to extract the words before our current position.
* According to those we'll make some smart decisions on what the user is
* probably intending to type.
*/
! previous_words = get_previous_words(start,
! &words_buffer,
! &previous_words_count);
/* If current word is a backslash command, offer completions for that */
if (text[0] == '\\')
*************** psql_completion(const char *text, int st
*** 1692,1698 ****
COMPLETE_WITH_LIST(list_TABLEOPTIONS);
}
! else if (TailMatches4("REPLICA", "IDENTITY", "USING", "INDEX"))
{
completion_info_charp = prev5_wd;
COMPLETE_WITH_QUERY(Query_for_index_of_table);
--- 1706,1712 ----
COMPLETE_WITH_LIST(list_TABLEOPTIONS);
}
! else if (TailMatches5(MatchAny, "REPLICA", "IDENTITY", "USING", "INDEX"))
{
completion_info_charp = prev5_wd;
COMPLETE_WITH_QUERY(Query_for_index_of_table);
*************** psql_completion(const char *text, int st
*** 2806,2812 ****
if (!recognized_connection_string(text))
COMPLETE_WITH_QUERY(Query_for_list_of_databases);
}
! else if (strcmp(prev2_wd, "\\connect") == 0 || strcmp(prev2_wd, "\\c") == 0)
{
if (!recognized_connection_string(prev_wd))
COMPLETE_WITH_QUERY(Query_for_list_of_roles);
--- 2820,2828 ----
if (!recognized_connection_string(text))
COMPLETE_WITH_QUERY(Query_for_list_of_databases);
}
! else if (previous_words_count >= 2 &&
! (strcmp(prev2_wd, "\\connect") == 0 ||
! strcmp(prev2_wd, "\\c") == 0))
{
if (!recognized_connection_string(prev_wd))
COMPLETE_WITH_QUERY(Query_for_list_of_roles);
*************** psql_completion(const char *text, int st
*** 2891,2897 ****
COMPLETE_WITH_LIST_CS(my_list);
}
! else if (strcmp(prev2_wd, "\\pset") == 0)
{
if (strcmp(prev_wd, "format") == 0)
{
--- 2907,2914 ----
COMPLETE_WITH_LIST_CS(my_list);
}
! else if (previous_words_count >= 2 &&
! strcmp(prev2_wd, "\\pset") == 0)
{
if (strcmp(prev_wd, "format") == 0)
{
*************** psql_completion(const char *text, int st
*** 2927,2933 ****
{
matches = complete_from_variables(text, "", "", false);
}
! else if (strcmp(prev2_wd, "\\set") == 0)
{
static const char *const boolean_value_list[] =
{"on", "off", NULL};
--- 2944,2951 ----
{
matches = complete_from_variables(text, "", "", false);
}
! else if (previous_words_count >= 2 &&
! strcmp(prev2_wd, "\\set") == 0)
{
static const char *const boolean_value_list[] =
{"on", "off", NULL};
*************** psql_completion(const char *text, int st
*** 3048,3059 ****
}
/* free storage */
! {
! int i;
!
! for (i = 0; i < lengthof(previous_words); i++)
! free(previous_words[i]);
! }
/* Return our Grand List O' Matches */
return matches;
--- 3066,3073 ----
}
/* free storage */
! free(previous_words);
! free(words_buffer);
/* Return our Grand List O' Matches */
return matches;
*************** exec_query(const char *query)
*** 3632,3661 ****
/*
! * Return the nwords word(s) before point. Words are returned right to left,
! * that is, previous_words[0] gets the last word before point.
! * If we run out of words, remaining array elements are set to empty strings.
! * Each array element is filled with a malloc'd string.
! * Returns the number of words that were actually found (up to nwords).
*/
! static int
! get_previous_words(int point, char **previous_words, int nwords)
{
! const char *buf = rl_line_buffer; /* alias */
int words_found = 0;
int i;
! /* first we look for a non-word char before the current point */
for (i = point - 1; i >= 0; i--)
if (strchr(WORD_BREAKS, buf[i]))
break;
point = i;
! while (nwords-- > 0)
{
int start,
end;
! char *s;
/* now find the first non-space which then constitutes the end */
end = -1;
--- 3646,3725 ----
/*
! * Parse all the word(s) before point.
! *
! * Returns a malloc'd array of character pointers that point into the malloc'd
! * data array returned to *buffer; caller must free() both of these when done.
! * *nwords receives the number of words found, ie, the valid length of the
! * return array.
! *
! * Words are returned right to left, that is, previous_words[0] gets the last
! * word before point, previous_words[1] the next-to-last, etc.
*/
! static char **
! get_previous_words(int point, char **buffer, int *nwords)
{
! char **previous_words;
! char *buf;
! int buflen;
int words_found = 0;
int i;
! /*
! * Construct a writable buffer including both preceding and current lines
! * of the query, up to "point" which is where the currently completable
! * word begins. Because our definition of "word" is such that a non-word
! * character must end each word, we can use this buffer to return the word
! * data as-is, by placing a '\0' after each word.
! */
! buflen = point + 1;
! if (tab_completion_query_buf)
! buflen += tab_completion_query_buf->len + 1;
! *buffer = buf = pg_malloc(buflen);
! i = 0;
! if (tab_completion_query_buf)
! {
! memcpy(buf, tab_completion_query_buf->data,
! tab_completion_query_buf->len);
! i += tab_completion_query_buf->len;
! buf[i++] = '\n';
! }
! memcpy(buf + i, rl_line_buffer, point);
! i += point;
! buf[i] = '\0';
!
! /* Readjust point to reference appropriate offset in buf */
! point = i;
!
! /*
! * Allocate array of word start points. There can be at most length/2 + 1
! * words in the buffer.
! */
! previous_words = (char **) pg_malloc((point / 2 + 1) * sizeof(char *));
!
! /*
! * First we look for a non-word char before the current point. (This is
! * probably useless, if readline is on the same page as we are about what
! * is a word, but if so it's cheap.)
! */
for (i = point - 1; i >= 0; i--)
+ {
if (strchr(WORD_BREAKS, buf[i]))
break;
+ }
point = i;
! /*
! * Now parse words, working backwards, until we hit start of line. The
! * backwards scan has some interesting but intentional properties
! * concerning parenthesis handling.
! */
! while (point >= 0)
{
int start,
end;
! bool inquotes = false;
! int parentheses = 0;
/* now find the first non-space which then constitutes the end */
end = -1;
*************** get_previous_words(int point, char **pre
*** 3667,3725 ****
break;
}
}
/*
! * If no end found we return an empty string, because there is no word
! * before the point
*/
! if (end < 0)
! {
! point = end;
! s = pg_strdup("");
! }
! else
{
! /*
! * Otherwise we now look for the start. The start is either the
! * last character before any word-break character going backwards
! * from the end, or it's simply character 0. We also handle open
! * quotes and parentheses.
! */
! bool inquotes = false;
! int parentheses = 0;
!
! for (start = end; start > 0; start--)
{
! if (buf[start] == '"')
! inquotes = !inquotes;
! if (!inquotes)
{
! if (buf[start] == ')')
! parentheses++;
! else if (buf[start] == '(')
! {
! if (--parentheses <= 0)
! break;
! }
! else if (parentheses == 0 &&
! strchr(WORD_BREAKS, buf[start - 1]))
break;
}
}
-
- point = start - 1;
-
- /* make a copy of chars from start to end inclusive */
- s = pg_malloc(end - start + 2);
- strlcpy(s, &buf[start], end - start + 2);
-
- words_found++;
}
! *previous_words++ = s;
}
! return words_found;
}
/*
--- 3731,3776 ----
break;
}
}
+ /* if no end found, we're done */
+ if (end < 0)
+ break;
/*
! * Otherwise we now look for the start. The start is either the last
! * character before any word-break character going backwards from the
! * end, or it's simply character 0. We also handle open quotes and
! * parentheses.
*/
! for (start = end; start > 0; start--)
{
! if (buf[start] == '"')
! inquotes = !inquotes;
! if (!inquotes)
{
! if (buf[start] == ')')
! parentheses++;
! else if (buf[start] == '(')
{
! if (--parentheses <= 0)
break;
}
+ else if (parentheses == 0 &&
+ strchr(WORD_BREAKS, buf[start - 1]))
+ break;
}
}
! /* Return the word located at start to end inclusive */
! previous_words[words_found] = &buf[start];
! buf[end + 1] = '\0';
! words_found++;
!
! /* Continue searching */
! point = start - 1;
}
! *nwords = words_found;
! return previous_words;
}
/*
diff --git a/src/bin/psql/tab-complete.h b/src/bin/psql/tab-complete.h
index 9dcd7e7..0e9a430 100644
*** a/src/bin/psql/tab-complete.h
--- b/src/bin/psql/tab-complete.h
***************
*** 8,15 ****
#ifndef TAB_COMPLETE_H
#define TAB_COMPLETE_H
! #include "postgres_fe.h"
! void initialize_readline(void);
! #endif
--- 8,17 ----
#ifndef TAB_COMPLETE_H
#define TAB_COMPLETE_H
! #include "pqexpbuffer.h"
! extern PQExpBuffer tab_completion_query_buf;
! extern void initialize_readline(void);
!
! #endif /* TAB_COMPLETE_H */
В списке pgsql-hackers по дате отправления: