From c9ae5408c8e5dd64b02200f70efe2d2e14a14ef0 Mon Sep 17 00:00:00 2001
From: Kyotaro Horiguchi <horiguchi.kyotaro@lab.ntt.co.jp>
Date: Mon, 28 Nov 2016 14:22:53 +0900
Subject: [PATCH 03/17] Introduce word shift and removal feature to
 psql-completion

Currently completion of psql is sensitive to noise words such like
temp/temporary, unlogged or concurrent. Addition to that, schema
elemsnts in CREATE SCHEMA syntax or some recursive syntaxes are
processed in somewhat bogus way.  To deal with such cases in simpler
way, this patch introduces two features.

1. Add a feature to ignore leading words to process.  New macros
  HEAD_SHIFT, HEAD_SET to shift or set the position of the first word
  to match using other macros. All *MatchesN macros follow
  this. SHIFT_TO_LAST1 is a macro to shift the head to the position
  where the specified word is found last.

2. Add a feature to remove intermediate words from previous_words
  list.  COLLAPSE(s, n) macro removes n words from the s'th position
  (1-based). Removing "noise" words let the succeeding operations
  simple.

This patch doesn't make any behavioral change.
---
 src/bin/psql/tab-complete-macros.h | 165 ++++++++++++++++++++++++++++---------
 src/bin/psql/tab-complete.c        |  51 +++++++++---
 2 files changed, 169 insertions(+), 47 deletions(-)

diff --git a/src/bin/psql/tab-complete-macros.h b/src/bin/psql/tab-complete-macros.h
index e0cbf49..48c9327 100644
--- a/src/bin/psql/tab-complete-macros.h
+++ b/src/bin/psql/tab-complete-macros.h
@@ -25,41 +25,67 @@
 #define prev8_wd  (previous_words[7])
 #define prev9_wd  (previous_words[8])
 
+/* Return the number of stored words counting head shift */
+#define WORD_COUNT() (previous_words_count - head_shift)
+
 /*
  * Return the index in previous_words for index from the beginning. n is
  * 1-based and the result is 0-based.
  */
-#define HEAD_INDEX(n) \
-	(previous_words_count - (n))
+#define HEAD_INDEX(n) (WORD_COUNT() - (n))
+
+/* Move the position of the beginning word for matching macros.  */
+#define HEAD_SHIFT(n) (head_shift += (n))
+
+/* Set the position of the beginning word for matching macros.  */
+#define HEAD_SET(n) (head_shift = (n))
+
+/*
+ * remove n words from current shifted position. This moves entire the
+ * previous_words regardless of head_shift.
+ */
+#define COLLAPSE(s, n)							\
+	(memmove(previous_words + HEAD_INDEX((s) + (n) - 1), \
+			 previous_words + HEAD_INDEX((s) - 1),		 \
+			 sizeof(char *) *								\
+			 (previous_words_count - HEAD_INDEX((s) - 1))),	\
+	 previous_words_count -= (n))
+
+/*
+ * Find the position where the specified word appears last and shift to there.
+ * The words before the position will be ignored ever after.
+ */
+#define SHIFT_TO_LAST1(p1) \
+	HEAD_SHIFT(find_last_index_of(p1, previous_words, previous_words_count))
 
 /*
  * Macros for matching the last N words before point, and after head_sift,
  * case-insensitively.
  */
 #define TailMatches1(p1) \
-	(previous_words_count >= 1 && \
+	(WORD_COUNT() >= 1 && \
 	 word_matches(p1, prev_wd))
 
 #define TailMatches2(p2, p1) \
-	(previous_words_count >= 2 && \
+	(WORD_COUNT() >= 2 && \
 	 word_matches(p1, prev_wd) && \
 	 word_matches(p2, prev2_wd))
 
 #define TailMatches3(p3, p2, p1) \
-	(previous_words_count >= 3 && \
+	(WORD_COUNT() >= 3 && \
 	 word_matches(p1, prev_wd) && \
 	 word_matches(p2, prev2_wd) && \
 	 word_matches(p3, prev3_wd))
 
 #define TailMatches4(p4, p3, p2, p1) \
-	(previous_words_count >= 4 && \
+	(WORD_COUNT() >= 4 && \
 	 word_matches(p1, prev_wd) && \
 	 word_matches(p2, prev2_wd) && \
 	 word_matches(p3, prev3_wd) && \
 	 word_matches(p4, prev4_wd))
 
 #define TailMatches5(p5, p4, p3, p2, p1) \
-	(previous_words_count >= 5 && \
+	(WORD_COUNT() >= 5 && \
 	 word_matches(p1, prev_wd) && \
 	 word_matches(p2, prev2_wd) && \
 	 word_matches(p3, prev3_wd) && \
@@ -67,7 +93,7 @@
 	 word_matches(p5, prev5_wd))
 
 #define TailMatches6(p6, p5, p4, p3, p2, p1) \
-	(previous_words_count >= 6 && \
+	(WORD_COUNT() >= 6 && \
 	 word_matches(p1, prev_wd) && \
 	 word_matches(p2, prev2_wd) && \
 	 word_matches(p3, prev3_wd) && \
@@ -76,7 +102,7 @@
 	 word_matches(p6, prev6_wd))
 
 #define TailMatches7(p7, p6, p5, p4, p3, p2, p1) \
-	(previous_words_count >= 7 && \
+	(WORD_COUNT() >= 7 && \
 	 word_matches(p1, prev_wd) && \
 	 word_matches(p2, prev2_wd) && \
 	 word_matches(p3, prev3_wd) && \
@@ -86,7 +112,7 @@
 	 word_matches(p7, prev7_wd))
 
 #define TailMatches8(p8, p7, p6, p5, p4, p3, p2, p1) \
-	(previous_words_count >= 8 && \
+	(WORD_COUNT() >= 8 && \
 	 word_matches(p1, prev_wd) && \
 	 word_matches(p2, prev2_wd) && \
 	 word_matches(p3, prev3_wd) && \
@@ -97,7 +123,7 @@
 	 word_matches(p8, prev8_wd))
 
 #define TailMatches9(p9, p8, p7, p6, p5, p4, p3, p2, p1) \
-	(previous_words_count >= 9 && \
+	(WORD_COUNT() >= 9 && \
 	 word_matches(p1, prev_wd) && \
 	 word_matches(p2, prev2_wd) && \
 	 word_matches(p3, prev3_wd) && \
@@ -113,10 +139,10 @@
 	 * head_shift, case-sensitively.
 	 */
 #define TailMatchesCS1(p1) \
-	(previous_words_count >= 1 && \
+	(WORD_COUNT() >= 1 && \
 	 word_matches_cs(p1, prev_wd))
 #define TailMatchesCS2(p2, p1) \
-	(previous_words_count >= 2 && \
+	(WORD_COUNT() >= 2 && \
 	 word_matches_cs(p1, prev_wd) && \
 	 word_matches_cs(p2, prev2_wd))
 
@@ -125,31 +151,31 @@
 	 * case-insensitively.
 	 */
 #define Matches1(p1) \
-	(previous_words_count == 1 && \
+	(WORD_COUNT() == 1 && \
 	 TailMatches1(p1))
 #define Matches2(p1, p2) \
-	(previous_words_count == 2 && \
+	(WORD_COUNT() == 2 && \
 	 TailMatches2(p1, p2))
 #define Matches3(p1, p2, p3) \
-	(previous_words_count == 3 && \
+	(WORD_COUNT() == 3 && \
 	 TailMatches3(p1, p2, p3))
 #define Matches4(p1, p2, p3, p4) \
-	(previous_words_count == 4 && \
+	(WORD_COUNT() == 4 && \
 	 TailMatches4(p1, p2, p3, p4))
 #define Matches5(p1, p2, p3, p4, p5) \
-	(previous_words_count == 5 && \
+	(WORD_COUNT() == 5 && \
 	 TailMatches5(p1, p2, p3, p4, p5))
 #define Matches6(p1, p2, p3, p4, p5, p6) \
-	(previous_words_count == 6 && \
+	(WORD_COUNT() == 6 && \
 	 TailMatches6(p1, p2, p3, p4, p5, p6))
 #define Matches7(p1, p2, p3, p4, p5, p6, p7) \
-	(previous_words_count == 7 && \
+	(WORD_COUNT() == 7 && \
 	 TailMatches7(p1, p2, p3, p4, p5, p6, p7))
 #define Matches8(p1, p2, p3, p4, p5, p6, p7, p8) \
-	(previous_words_count == 8 && \
+	(WORD_COUNT() == 8 && \
 	 TailMatches8(p1, p2, p3, p4, p5, p6, p7, p8))
 #define Matches9(p1, p2, p3, p4, p5, p6, p7, p8, p9) \
-	(previous_words_count == 9 && \
+	(WORD_COUNT() == 9 && \
 	 TailMatches9(p1, p2, p3, p4, p5, p6, p7, p8, p9))
 
 /*
@@ -195,16 +221,39 @@
 	 word_matches(p5, previous_words[HEAD_INDEX((s) + 4)]) &&			\
 	 word_matches(p6, previous_words[HEAD_INDEX((s) + 5)]))
 
-#define MidMatches7(s,p1, p2, p3, p4, p5, p6, p7)	\
+#define MidMatches7(s,p1, p2, p3, p4, p5, p6, p7)			\
 	(HEAD_INDEX((s) + 6) >= 0 &&							\
-	 word_matches(p1, previous_words[HEAD_INDEX(s)]) &&	\
-	 word_matches(p2, previous_words[HEAD_INDEX((s) + 1)]) &&	\
-	 word_matches(p3, previous_words[HEAD_INDEX((s) + 2)]) &&		\
+	 word_matches(p1, previous_words[HEAD_INDEX(s)]) &&			\
+	 word_matches(p2, previous_words[HEAD_INDEX((s) + 1)]) &&		\
+	 word_matches(p3, previous_words[HEAD_INDEX((s) + 2)]) &&			\
 	 word_matches(p4, previous_words[HEAD_INDEX((s) + 3)]) &&			\
 	 word_matches(p5, previous_words[HEAD_INDEX((s) + 4)]) &&			\
 	 word_matches(p6, previous_words[HEAD_INDEX((s) + 5)]) &&			\
 	 word_matches(p7, previous_words[HEAD_INDEX((s) + 6)]))
 
+#define MidMatches8(s,p1, p2, p3, p4, p5, p6, p7, p8)		\
+	(HEAD_INDEX((s) + 7) >= 0 &&							\
+	 word_matches(p1, previous_words[HEAD_INDEX(s)]) &&				\
+	 word_matches(p2, previous_words[HEAD_INDEX((s) + 1)]) &&		\
+	 word_matches(p3, previous_words[HEAD_INDEX((s) + 2)]) &&		\
+	 word_matches(p4, previous_words[HEAD_INDEX((s) + 3)]) &&		\
+	 word_matches(p5, previous_words[HEAD_INDEX((s) + 4)]) &&		\
+	 word_matches(p6, previous_words[HEAD_INDEX((s) + 5)]) &&		\
+	 word_matches(p7, previous_words[HEAD_INDEX((s) + 6)]) &&		\
+	 word_matches(p8, previous_words[HEAD_INDEX((s) + 7)]))
+
+#define MidMatches9(s,p1, p2, p3, p4, p5, p6, p7, p8, p9)		\
+	(HEAD_INDEX((s) + 8) >= 0 &&							\
+	 word_matches(p1, previous_words[HEAD_INDEX(s)]) &&				\
+	 word_matches(p2, previous_words[HEAD_INDEX((s) + 1)]) &&		\
+	 word_matches(p3, previous_words[HEAD_INDEX((s) + 2)]) &&		\
+	 word_matches(p4, previous_words[HEAD_INDEX((s) + 3)]) &&		\
+	 word_matches(p5, previous_words[HEAD_INDEX((s) + 4)]) &&		\
+	 word_matches(p6, previous_words[HEAD_INDEX((s) + 5)]) &&		\
+	 word_matches(p7, previous_words[HEAD_INDEX((s) + 6)]) &&		\
+	 word_matches(p8, previous_words[HEAD_INDEX((s) + 7)]) &&		\
+	 word_matches(p9, previous_words[HEAD_INDEX((s) + 8)]))
+
 #define HeadMatches1(p1) \
 	MidMatches1(1, p1)
 #define HeadMatches2(p1, p2) \
@@ -219,6 +268,41 @@
 	MidMatches6(1, p1, p2, p3, p4, p5, p6)
 #define HeadMatches7(p1, p2, p3, p4, p5, p6, p7) \
 	MidMatches7(1, p1, p2, p3, p4, p5, p6, p7)
+#define HeadMatches8(p1, p2, p3, p4, p5, p6, p7, p8)	\
+	MidMatches8(1, p1, p2, p3, p4, p5, p6, p7, p8)
+#define HeadMatches9(p1, p2, p3, p4, p5, p6, p7, p8, p9)	\
+	MidMatches9(1, p1, p2, p3, p4, p5, p6, p7, p8, p9)
+
+#define HeadMatchAndRemove1(s, l, p1)			\
+	((WORD_COUNT() >= s + l - 1 && HeadMatches1(p1))? \
+	 COLLAPSE(s, l), true : false)
+#define HeadMatchAndRemove2(s, l, p1, p2)		\
+	((WORD_COUNT() >= s + l - 1 && HeadMatches2(p1, p2)) ?	\
+	 COLLAPSE(s, l), true : false)
+#define HeadMatchAndRemove3(s, l, p1, p2, p3)							\
+	((WORD_COUNT() >= s + l - 1 && HeadMatches3(p1, p2, p3)) ?		\
+	 COLLAPSE(s, l), true : false)
+#define HeadMatchAndRemove4(s, l, p1, p2, p3, p4)	\
+	((WORD_COUNT() >= s + l - 1 && HeadMatches4(p1, p2, p3, p4)) ? \
+	 COLLAPSE(s, l), true : false)
+#define HeadMatchAndRemove5(s, l, p1, p2, p3, p4, p5)					\
+	((WORD_COUNT() >= s + l - 1 && HeadMatches5(p1, p2, p3, p4, p5))? \
+	 COLLAPSE(s, l), true : false)
+#define HeadMatchAndRemove6(s, l, p1, p2, p3, p4, p5, p6)				\
+	((WORD_COUNT() >= s + l - 1 && HeadMatches6(p1, p2, p3, p4, p5, p6))? \
+	 COLLAPSE(s, l), true : false)
+#define HeadMatchAndRemove7(s, l, p1, p2, p3, p4, p5, p6, p7)			\
+	((WORD_COUNT() >= s + l - 1 &&									\
+			HeadMatches7(p1, p2, p3, p4, p5, p6, p7)) ?					\
+	 COLLAPSE(s, l), true : false)
+#define HeadMatchAndRemove8(s, l, p1, p2, p3, p4, p5, p6, p7, p8)		\
+	((WORD_COUNT() >= s + l - 1 &&									\
+			HeadMatches8(p1, p2, p3, p4, p5, p6, p7, p8)) ?				\
+	 COLLAPSE(s, l), true : false)
+#define HeadMatchAndRemove9(s, l, p1, p2, p3, p4, p5, p6, p7, p8, p9)	\
+	((WORD_COUNT() >= s + l - 1 &&									\
+			HeadMatches9(p1, p2, p3, p4, p5, p6, p7, p8, p9)) ?			\
+	 COLLAPSE(s, l), true : false)
 
 /*
  * A few macros to ease typing. You can use these to complete the given
@@ -240,12 +324,6 @@
 
 #define COMPLETION_CHARP (completion_charp->data)
 
-#define COMPLETE_WITH_QUERY(query)				\
-do { \
-	SET_COMP_CHARP(query);	\
-	return completion_matches(text, complete_from_query);	\
-} while (0)
-
 /*
  * COMPLETE_WITH_QUERY with additional keywords. Keywords are complete
  * case-sensitively
@@ -257,11 +335,7 @@ do { \
 	return completion_matches(text, complete_from_query);	\
 } while (0)
 
-#define COMPLETE_WITH_SCHEMA_QUERY(query) \
-do { \
-	completion_squery = &(query); \
-	return completion_matches(text, complete_from_schema_query); \
-} while (0)
+#define COMPLETE_WITH_QUERY(query) COMPLETE_WITH_QUERY_KW((query), "")
 
 /*
  * COMPLETE_WITH_SCHEMA_QUERY with additional keywords. Keywords are complete
@@ -274,6 +348,8 @@ do { \
 	return completion_matches(text, complete_from_schema_query); \
 } while (0)
 
+#define COMPLETE_WITH_SCHEMA_QUERY(query) COMPLETE_WITH_SCHEMA_QUERY_KW((query), "")
+
 #define COMPLETE_WITH_LIST_CS(list) \
 do { \
 	completion_charpp = list; \
@@ -476,4 +552,19 @@ do { \
 	additional_kw_query(text, 16, s1, s2, s3, s4, s5, s6, s7,		\
 						s8, s9, s10, s11, s12, s13, s14, s15, s16)
 
+#define COMPLETE_THING_KW(p, addon)					\
+do { \
+	const pgsql_thing_t *ent = find_thing_entry(previous_words[-(p) - 1]);	\
+	if (ent) \
+	{ \
+		if (ent->query) \
+			COMPLETE_WITH_QUERY_KW(ent->query, (addon));	\
+		else if (ent->squery) \
+			COMPLETE_WITH_SCHEMA_QUERY_KW(*ent->squery, (addon));	\
+	} \
+	return NULL; \
+} while (0)
+
+#define COMPLETE_THING(p) COMPLETE_THING_KW(p, "")
+
 #endif   /* TAB_COMPLETE_MACROS_H */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8842dae..c14619a 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -793,6 +793,7 @@ static void append_variable_names(char ***varnames, int *nvars,
 static char **complete_from_variables(const char *text,
 					const char *prefix, const char *suffix, bool need_value);
 static char *complete_from_files(const char *text, int state);
+static int find_last_index_of(char *w, char **previous_words, int len);
 
 static char *pg_strdup_keyword_case(const char *s, const char *ref);
 static char *concatenate_strings(const char *s1, const char *s2);
@@ -806,6 +807,7 @@ static char *get_guctype(const char *varname);
 
 static char **psql_completion_internal(const char *text, char **previous_words,
 										   int previous_words_count);
+static const pgsql_thing_t *find_thing_entry(char *word);
 #ifdef NOT_USED
 static char *quote_file_name(char *text, int match_type, char *quote_pointer);
 static char *dequote_file_name(char *text, char quote_char);
@@ -1014,6 +1016,9 @@ static char **
 psql_completion_internal(const char *text, char **previous_words,
 						 int previous_words_count)
 {
+	/* The number of prefixing words to be ignored */
+	int			head_shift = 0;
+
 	/* Known command-starting keywords. */
 	static const char *const sql_commands[] = {
 		"ABORT", "ALTER", "ANALYZE", "BEGIN", "CHECKPOINT", "CLOSE", "CLUSTER",
@@ -2980,18 +2985,14 @@ psql_completion_internal(const char *text, char **previous_words,
 	 */
 	else
 	{
-		int			i;
+		const pgsql_thing_t *ent = find_thing_entry(prev_wd);
 
-		for (i = 0; words_after_create[i].name; i++)
+		if (ent)
 		{
-			if (pg_strcasecmp(prev_wd, words_after_create[i].name) == 0)
-			{
-				if (words_after_create[i].query)
-					COMPLETE_WITH_QUERY(words_after_create[i].query);
-				if (words_after_create[i].squery)
-					COMPLETE_WITH_SCHEMA_QUERY(*words_after_create[i].squery);
-				break;
-			}
+			if (ent->query)
+				COMPLETE_WITH_QUERY(ent->query);
+			else if (ent->squery)
+				COMPLETE_WITH_SCHEMA_QUERY(*ent->squery);
 		}
 	}
 
@@ -3502,6 +3503,18 @@ complete_from_files(const char *text, int state)
 
 /* HELPER FUNCTIONS */
 
+/*
+ * Return the index (reverse to the index of previous_words) of the tailmost
+ * (topmost in the array) appearance of w.
+ */
+static int
+find_last_index_of(char *w, char **previous_words, int len)
+{
+	int i;
+
+	for (i = 0 ; i < len && !word_matches(w, previous_words[i]) ; i++);
+	return i < len ? (len - i - 1) : 0;
+}
 
 /*
  * Make a pg_strdup copy of s and convert the case according to
@@ -3801,6 +3814,24 @@ get_guctype(const char *varname)
 	return guctype;
 }
 
+/*
+ * Finds the entry in words_after_create[] that matches the word.
+ * NULL if not found.
+ */
+static const pgsql_thing_t *
+find_thing_entry(char *word)
+{
+	int			i;
+
+	for (i = 0; words_after_create[i].name; i++)
+	{
+		if (pg_strcasecmp(word, words_after_create[i].name) == 0)
+			return words_after_create + i;
+	}
+
+	return NULL;
+}
+
 #ifdef NOT_USED
 
 /*
-- 
2.9.2

