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 */
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to