patch 9.1.0917: various vartabstop and shiftround bugs when shifting lines

Problem:  various vartabstop and shiftround bugs when shifting lines
Solution: Fix the bugs, add new tests for shifting lines in various ways
          (Gary Johnson)

fixes: #14891
closes: #16193

Signed-off-by: Gary Johnson <garyjohn@spocom.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/ops.c b/src/ops.c
index eb8f64c..a75efab 100644
--- a/src/ops.c
+++ b/src/ops.c
@@ -218,25 +218,57 @@
     }
 }
 
+#ifdef FEAT_VARTABS
 /*
- * Shift the current line one shiftwidth left (if left != 0) or right
- * leaves cursor on first blank in the line.
+ * Return the tabstop width at the index of the variable tabstop array.  If an
+ * index greater than the length of the array is given, the last tabstop width
+ * in the array is returned.
  */
-    void
-shift_line(
-    int	left,
-    int	round,
-    int	amount,
-    int call_changed_bytes)	// call changed_bytes()
+    static int
+get_vts(int *vts_array, int index)
 {
-    vimlong_T	count;
-    int		i, j;
-    int		sw_val = trim_to_int(get_sw_value_indent(curbuf, left));
+    int	ts;
 
-    if (sw_val == 0)
-	sw_val = 1;		// shouldn't happen, just in case
+    if (index < 1)
+	ts = 0;
+    else if (index <= vts_array[0])
+	ts = vts_array[index];
+    else
+	ts = vts_array[vts_array[0]];
 
-    count = get_indent();	// get current indent
+    return ts;
+}
+
+/*
+ * Return the sum of all the tabstops through the index-th.
+ */
+    static int
+get_vts_sum(int *vts_array, int index)
+{
+    int	sum = 0;
+    int	i;
+
+    // Perform the summation for indeces within the actual array.
+    for (i = 1; i <= index && i <= vts_array[0]; i++)
+	sum += vts_array[i];
+
+    // Add topstops whose indeces exceed the actual array.
+    if (i <= index)
+	sum += vts_array[vts_array[0]] * (index - vts_array[0]);
+
+    return sum;
+}
+#endif
+
+    static vimlong_T
+get_new_sw_indent(
+    int		left,		// TRUE if shift is to the left
+    int		round,		// TRUE if new indent is to be to a tabstop
+    vimlong_T	amount,		// Number of shifts
+    vimlong_T	sw_val)
+{
+    vimlong_T	count = get_indent();
+    vimlong_T	i, j;
 
     if (round)			// round off indent
     {
@@ -252,20 +284,124 @@
 	}
 	else
 	    i += amount;
-	count = (vimlong_T)i * (vimlong_T)sw_val;
+	count = i * sw_val;
     }
-    else		// original vi indent
+    else			// original vi indent
     {
 	if (left)
 	{
-	    count -= (vimlong_T)sw_val * (vimlong_T)amount;
+	    count -= sw_val * amount;
 	    if (count < 0)
 		count = 0;
 	}
 	else
-	    count += (vimlong_T)sw_val * (vimlong_T)amount;
+	    count += sw_val * amount;
     }
 
+    return count;
+}
+
+#ifdef FEAT_VARTABS
+    static vimlong_T
+get_new_vts_indent(
+    int	left,			// TRUE if shift is to the left
+    int	round,			// TRUE if new indent is to be to a tabstop
+    int	amount,			// Number of shifts
+    int	*vts_array)
+{
+    vimlong_T	indent = get_indent();
+    int		vtsi = 0;
+    int		vts_indent = 0;
+    int		ts = 0;		// Silence uninitialized variable warning.
+    int		offset;		// Extra indent spaces to the right of the
+				// tabstop
+
+    // Find the tabstop at or to the left of the current indent.
+    while (vts_indent <= indent)
+    {
+	vtsi++;
+	ts = get_vts(vts_array, vtsi);
+	vts_indent += ts;
+    }
+    vts_indent -= ts;
+    vtsi--;
+
+    offset = indent - vts_indent;
+
+    if (round)
+    {
+	if (left)
+	{
+	    if (offset == 0)
+		indent = get_vts_sum(vts_array, vtsi - amount);
+	    else
+		indent = get_vts_sum(vts_array, vtsi - (amount - 1));
+	}
+	else
+	    indent = get_vts_sum(vts_array, vtsi + amount);
+    }
+    else
+    {
+	if (left)
+	{
+	    if (amount > vtsi)
+		indent = 0;
+	    else
+		indent = get_vts_sum(vts_array, vtsi - amount) + offset;
+	}
+	else
+	    indent = get_vts_sum(vts_array, vtsi + amount) + offset;
+    }
+
+    return indent;
+}
+#endif
+
+/*
+ * Shift the current line 'amount' shiftwidth(s) left (if 'left' is TRUE) or
+ * right.
+ *
+ * The rules for choosing a shiftwidth are:  If 'shiftwidth' is non-zero, use
+ * 'shiftwidth'; else if 'vartabstop' is not empty, use 'vartabstop'; else use
+ * 'tabstop'.  The Vim documentation says nothing about 'softtabstop' or
+ * 'varsofttabstop' affecting the shiftwidth, and neither affects the
+ * shiftwidth in current versions of Vim, so they are not considered here.
+ */
+    void
+shift_line(
+    int	left,			// TRUE if shift is to the left
+    int	round,			// TRUE if new indent is to be to a tabstop
+    int	amount,			// Number of shifts
+    int	call_changed_bytes)	// call changed_bytes()
+{
+    vimlong_T	count;
+    long	sw_val = curbuf->b_p_sw;
+    long	ts_val = curbuf->b_p_ts;
+#ifdef FEAT_VARTABS
+    int		*vts_array = curbuf->b_p_vts_array;
+#endif
+
+    if (sw_val != 0)
+	// 'shiftwidth' is not zero; use it as the shift size.
+	count = get_new_sw_indent(left, round, amount, sw_val);
+    else
+#ifdef FEAT_VARTABS
+	if ((vts_array == NULL) || (vts_array[0] == 0))
+#endif
+    {
+	// 'shiftwidth is zero and 'vartabstop' is empty; use 'tabstop' as the
+	// shift size.
+	count = get_new_sw_indent(left, round, amount, ts_val);
+    }
+#ifdef FEAT_VARTABS
+    else
+    {
+	// 'shiftwidth is zero and 'vartabstop' is defined; use 'vartabstop'
+	// to determine the new indent.
+	count = get_new_vts_indent(left, round, amount, vts_array);
+    }
+#endif
+
     // Set new indent
     if (State & VREPLACE_FLAG)
 	change_indent(INDENT_SET, trim_to_int(count), FALSE, NUL, call_changed_bytes);
diff --git a/src/testdir/test_shift.vim b/src/testdir/test_shift.vim
index 370082f..f31c5a1 100644
--- a/src/testdir/test_shift.vim
+++ b/src/testdir/test_shift.vim
@@ -112,4 +112,805 @@
   call assert_fails('2,1<', 'E493:')
 endfunc
 
+" Test inserting a backspace at the start of a line.
+"
+" This is to verify the proper behavior of tabstop_start() as called from
+" ins_bs().
+"
+func Test_shift_ins_bs()
+  set backspace=indent,start
+  set softtabstop=11
+
+  call setline(1, repeat(" ", 33) . "word")
+  exe "norm! I\<BS>"
+  call assert_equal(repeat(" ", 22) . "word", getline(1))
+  call setline(1, repeat(" ", 23) . "word")
+  exe "norm! I\<BS>"
+  call assert_equal(repeat(" ", 22) . "word", getline(1))
+  exe "norm! I\<BS>"
+  call assert_equal(repeat(" ", 11) . "word", getline(1))
+
+  set backspace& softtabstop&
+  bw!
+endfunc
+
+" Test inserting a backspace at the start of a line, with 'varsofttabstop'.
+"
+func Test_shift_ins_bs_vartabs()
+  CheckFeature vartabs
+  set backspace=indent,start
+  set varsofttabstop=13,11,7
+
+  call setline(1, repeat(" ", 44) . "word")
+  exe "norm! I\<BS>"
+  call assert_equal(repeat(" ", 38) . "word", getline(1))
+  call setline(1, repeat(" ", 39) . "word")
+  exe "norm! I\<BS>"
+  call assert_equal(repeat(" ", 38) . "word", getline(1))
+  exe "norm! I\<BS>"
+  call assert_equal(repeat(" ", 31) . "word", getline(1))
+  exe "norm! I\<BS>"
+  call assert_equal(repeat(" ", 24) . "word", getline(1))
+  exe "norm! I\<BS>"
+  call assert_equal(repeat(" ", 13) . "word", getline(1))
+  exe "norm! I\<BS>"
+  call assert_equal(                  "word", getline(1))
+  exe "norm! I\<BS>"
+  call assert_equal(                  "word", getline(1))
+
+  set backspace& varsofttabstop&
+  bw!
+endfunc
+
+" Test the >> and << normal-mode commands.
+"
+func Test_shift_norm()
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftwidth=5
+  set tabstop=7
+
+  call setline(1, "  word")
+
+  " Shift by 'shiftwidth' right and left.
+
+  norm! >>
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  12) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  17) . "word", getline(1))
+
+  norm! <<
+  call assert_equal(repeat(" ",  12) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  " Shift by 'tabstop' right and left.
+
+  set shiftwidth=0
+  call setline(1, "  word")
+
+  norm! >>
+  call assert_equal(repeat(" ",  9) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  16) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  23) . "word", getline(1))
+
+  norm! <<
+  call assert_equal(repeat(" ",  16) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  9) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftwidth& tabstop&
+  bw!
+endfunc
+
+" Test the >> and << normal-mode commands, with 'vartabstop'.
+"
+func Test_shift_norm_vartabs()
+  CheckFeature vartabs
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftwidth=0
+  set vartabstop=19,17,11
+
+  " Shift by 'vartabstop' right and left.
+
+  call setline(1, "  word")
+
+  norm! >>
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  38) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  49) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  60) . "word", getline(1))
+
+  norm! <<
+  call assert_equal(repeat(" ",  49) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  38) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftwidth& vartabstop&
+  bw!
+endfunc
+
+" Test the >> and << normal-mode commands with 'shiftround'.
+"
+func Test_shift_norm_round()
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftround
+  set shiftwidth=5
+  set tabstop=7
+
+  call setline(1, "word")
+
+  " Shift by 'shiftwidth' right and left.
+
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  5) . "word", getline(1))
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  10) . "word", getline(1))
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  15) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  20) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  25) . "word", getline(1))
+
+  norm! <<
+  call assert_equal(repeat(" ",  20) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  15) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  10) . "word", getline(1))
+  exe "norm! I  "
+  norm! <<
+  call assert_equal(repeat(" ",  10) . "word", getline(1))
+
+  call setline(1, repeat(" ", 7) . "word")
+  norm! <<
+  call assert_equal(repeat(" ",  5) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  call setline(1, repeat(" ", 2) . "word")
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  " Shift by 'tabstop' right and left.
+
+  set shiftwidth=0
+  call setline(1, "word")
+
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  14) . "word", getline(1))
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  28) . "word", getline(1))
+  norm! >>
+  call assert_equal(repeat(" ",  35) . "word", getline(1))
+
+  norm! <<
+  call assert_equal(repeat(" ",  28) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  14) . "word", getline(1))
+  exe "norm! I  "
+  norm! <<
+  call assert_equal(repeat(" ",  14) . "word", getline(1))
+
+  call setline(1, repeat(" ", 9) . "word")
+  norm! <<
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  call setline(1, repeat(" ", 2) . "word")
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftround& shiftwidth& tabstop&
+  bw!
+endfunc
+
+" Test the >> and << normal-mode commands with 'shiftround' and 'vartabstop'.
+"
+func Test_shift_norm_round_vartabs()
+  CheckFeature vartabs
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftround
+  set shiftwidth=0
+  set vartabstop=19,17,11
+
+  " Shift by 'vartabstop' right and left.
+
+  call setline(1, "word")
+
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  19) . "word", getline(1))
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  36) . "word", getline(1))
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  47) . "word", getline(1))
+  exe "norm! I  "
+  norm! >>
+  call assert_equal(repeat(" ",  58) . "word", getline(1))
+
+  exe "norm! I  "
+  norm! <<
+  call assert_equal(repeat(" ",  58) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  47) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  36) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  19) . "word", getline(1))
+  exe "norm! I  "
+  norm! <<
+  call assert_equal(repeat(" ",  19) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  exe "norm! I  "
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftround& shiftwidth& vartabstop&
+  bw!
+endfunc
+
+" Test the V> and V< visual-mode commands.
+"
+" See ":help v_<" and ":help v_>".  See also the last paragraph of "3. Simple
+" changes", ":help simple-change", immediately above "4. Complex changes",
+" ":help complex-change".
+"
+func Test_shift_vis()
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftwidth=5
+  set tabstop=7
+
+  call setline(1, "  word")
+
+  " Shift by 'shiftwidth' right and left.
+
+  norm! V>
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  norm! V2>
+  call assert_equal(repeat(" ",  17) . "word", getline(1))
+  norm! V3>
+  call assert_equal(repeat(" ",  32) . "word", getline(1))
+
+  norm! V<
+  call assert_equal(repeat(" ",  27) . "word", getline(1))
+  norm! V2<
+  call assert_equal(repeat(" ",  17) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  norm! V<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  " Shift by 'tabstop' right and left.
+
+  set shiftwidth=0
+  call setline(1, "  word")
+
+  norm! V>
+  call assert_equal(repeat(" ",  9) . "word", getline(1))
+  norm! V2>
+  call assert_equal(repeat(" ",  23) . "word", getline(1))
+  norm! V3>
+  call assert_equal(repeat(" ",  44) . "word", getline(1))
+
+  norm! V<
+  call assert_equal(repeat(" ",  37) . "word", getline(1))
+  norm! V2<
+  call assert_equal(repeat(" ",  23) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  norm! V<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftwidth& tabstop&
+  bw!
+endfunc
+
+" Test the V> and V< visual-mode commands, with 'vartabstop'.
+"
+" See ":help v_<" and ":help v_>".  See also the last paragraph of "3. Simple
+" changes", ":help simple-change", immediately above "4. Complex changes",
+" ":help complex-change".
+"
+func Test_shift_vis_vartabs()
+  CheckFeature vartabs
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftwidth=0
+  set vartabstop=19,17,11
+
+  " Shift by 'vartabstop' right and left.
+
+  call setline(1, "  word")
+
+  norm! V>
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  norm! V2>
+  call assert_equal(repeat(" ",  49) . "word", getline(1))
+  norm! V3>
+  call assert_equal(repeat(" ",  82) . "word", getline(1))
+
+  norm! V<
+  call assert_equal(repeat(" ",  71) . "word", getline(1))
+  norm! V2<
+  call assert_equal(repeat(" ",  49) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  norm! V<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftwidth& vartabstop&
+  bw!
+endfunc
+
+" Test the V> and V< visual-mode commands with 'shiftround'.
+"
+func Test_shift_vis_round()
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftround
+  set shiftwidth=5
+  set tabstop=7
+
+  call setline(1, "word")
+
+  " Shift by 'shiftwidth' right and left.
+
+  exe "norm! I  "
+  norm! V>
+  call assert_equal(repeat(" ",  5) . "word", getline(1))
+  exe "norm! I  "
+  norm! V2>
+  call assert_equal(repeat(" ",  15) . "word", getline(1))
+  exe "norm! I  "
+  norm! V3>
+  call assert_equal(repeat(" ",  30) . "word", getline(1))
+
+  exe "norm! I  "
+  norm! V2<
+  call assert_equal(repeat(" ",  25) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  10) . "word", getline(1))
+  norm! V<
+  call assert_equal(repeat(" ",  5) . "word", getline(1))
+  norm! V<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  " Shift by 'tabstop' right and left.
+
+  set shiftwidth=0
+  call setline(1, "word")
+
+  exe "norm! I  "
+  norm! V>
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  exe "norm! I  "
+  norm! V2>
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  exe "norm! I  "
+  norm! V3>
+  call assert_equal(repeat(" ",  42) . "word", getline(1))
+
+  exe "norm! I  "
+  norm! V<
+  call assert_equal(repeat(" ",  42) . "word", getline(1))
+  norm! V<
+  call assert_equal(repeat(" ",  35) . "word", getline(1))
+  norm! V2<
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! V<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  norm! V3<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  call setline(1, "  word")
+  norm! V<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+
+  set expandtab& shiftround& shiftwidth& tabstop&
+  bw!
+endfunc
+
+" Test the V> and V< visual-mode commands with 'shiftround' and 'vartabstop'.
+"
+func Test_shift_vis_round_vartabs()
+  CheckFeature vartabs
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftround
+  set shiftwidth=0
+  set vartabstop=19,17,11
+
+  " Shift by 'vartabstop' right and left.
+
+  call setline(1, "word")
+
+  exe "norm! I  "
+  norm! V>
+  call assert_equal(repeat(" ",  19) . "word", getline(1))
+  exe "norm! I  "
+  norm! V3>
+  call assert_equal(repeat(" ",  58) . "word", getline(1))
+
+  exe "norm! I  "
+  norm! V2<
+  call assert_equal(repeat(" ",  47) . "word", getline(1))
+  exe "norm! I  "
+  norm! V3<
+  call assert_equal(repeat(" ",  19) . "word", getline(1))
+  exe "norm! I  "
+  norm! V3<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  exe "norm! I  "
+  norm! V<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftround& shiftwidth& vartabstop&
+  bw!
+endfunc
+
+" Test the :> and :< ex-mode commands.
+"
+func Test_shift_ex()
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftwidth=5
+  set tabstop=7
+
+  call setline(1, "  word")
+
+  " Shift by 'shiftwidth' right and left.
+
+  >
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  >>
+  call assert_equal(repeat(" ",  17) . "word", getline(1))
+  >>>
+  call assert_equal(repeat(" ",  32) . "word", getline(1))
+
+  <<<<
+  call assert_equal(repeat(" ",  12) . "word", getline(1))
+  <
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  <
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  <
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  " Shift by 'tabstop' right and left.
+
+  set shiftwidth=0
+  call setline(1, "  word")
+
+  >
+  call assert_equal(repeat(" ",  9) . "word", getline(1))
+  >>
+  call assert_equal(repeat(" ",  23) . "word", getline(1))
+  >>>
+  call assert_equal(repeat(" ",  44) . "word", getline(1))
+
+  <<<<
+  call assert_equal(repeat(" ",  16) . "word", getline(1))
+  <<
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  <
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftwidth& tabstop&
+  bw!
+endfunc
+
+" Test the :> and :< ex-mode commands, with vartabstop.
+"
+func Test_shift_ex_vartabs()
+  CheckFeature vartabs
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftwidth=0
+  set vartabstop=19,17,11
+
+  " Shift by 'vartabstop' right and left.
+
+  call setline(1, "  word")
+
+  >
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  >>
+  call assert_equal(repeat(" ",  49) . "word", getline(1))
+  >>>
+  call assert_equal(repeat(" ",  82) . "word", getline(1))
+
+  <<<<
+  call assert_equal(repeat(" ",  38) . "word", getline(1))
+  <<
+  call assert_equal(repeat(" ",  2) . "word", getline(1))
+  <
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftwidth& vartabstop&
+  bw!
+endfunc
+
+" Test the :> and :< ex-mode commands with 'shiftround'.
+"
+func Test_shift_ex_round()
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftround
+  set shiftwidth=5
+  set tabstop=7
+
+  call setline(1, "word")
+
+  " Shift by 'shiftwidth' right and left.
+
+  exe "norm! I  "
+  >
+  call assert_equal(repeat(" ",  5) . "word", getline(1))
+  exe "norm! I  "
+  >>
+  call assert_equal(repeat(" ",  15) . "word", getline(1))
+  exe "norm! I  "
+  >>>
+  call assert_equal(repeat(" ",  30) . "word", getline(1))
+
+  exe "norm! I  "
+  <<<<
+  call assert_equal(repeat(" ",  15) . "word", getline(1))
+  exe "norm! I  "
+  <<
+  call assert_equal(repeat(" ",  10) . "word", getline(1))
+  <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  >>
+  <<<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  " Shift by 'tabstop' right and left.
+
+  set shiftwidth=0
+  call setline(1, "word")
+
+  exe "norm! I  "
+  >
+  call assert_equal(repeat(" ",  7) . "word", getline(1))
+  exe "norm! I  "
+  >>
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  exe "norm! I  "
+  >>>
+  call assert_equal(repeat(" ",  42) . "word", getline(1))
+
+  exe "norm! I  "
+  <<<<
+  call assert_equal(repeat(" ",  21) . "word", getline(1))
+  exe "norm! I  "
+  <<
+  call assert_equal(repeat(" ",  14) . "word", getline(1))
+  exe "norm! I  "
+  <<<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  >>
+  <<<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftround& shiftwidth& tabstop&
+  bw!
+endfunc
+
+" Test the :> and :< ex-mode commands with 'shiftround' and 'vartabstop'.
+"
+func Test_shift_ex_round_vartabs()
+  CheckFeature vartabs
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftround
+  set shiftwidth=0
+  set vartabstop=19,17,11
+
+  " Shift by 'vartabstop' right and left.
+
+  call setline(1, "word")
+
+  exe "norm! I  "
+  >
+  call assert_equal(repeat(" ",  19) . "word", getline(1))
+  exe "norm! I  "
+  >>
+  call assert_equal(repeat(" ",  47) . "word", getline(1))
+  >>>
+  call assert_equal(repeat(" ",  80) . "word", getline(1))
+
+  <<<<
+  call assert_equal(repeat(" ",  36) . "word", getline(1))
+  exe "norm! I  "
+  <<
+  call assert_equal(repeat(" ",  19) . "word", getline(1))
+  exe "norm! I  "
+  <<
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+  <
+  call assert_equal(repeat(" ",  0) . "word", getline(1))
+
+  set expandtab& shiftround& shiftwidth& vartabstop&
+  bw!
+endfunc
+
+" Test shifting lines with <C-T> and <C-D>.
+"
+func Test_shift_ins()
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftwidth=5
+  set tabstop=7
+
+  " Shift by 'shiftwidth' right and left.
+
+  call setline(1, repeat(" ", 7) . "word")
+  exe "norm! 9|i\<C-T>"
+  call assert_equal(repeat(" ", 10) . "word", getline(1))
+  exe "norm! A\<C-T>"
+  call assert_equal(repeat(" ", 15) . "word", getline(1))
+  exe "norm! I  \<C-T>"
+  call assert_equal(repeat(" ", 20) . "word", getline(1))
+
+  exe "norm! I  \<C-D>"
+  call assert_equal(repeat(" ", 20) . "word", getline(1))
+  exe "norm! I  "
+  exe "norm! 24|i\<C-D>"
+  call assert_equal(repeat(" ", 20) . "word", getline(1))
+  exe "norm! A\<C-D>"
+  call assert_equal(repeat(" ", 15) . "word", getline(1))
+  exe "norm! I  "
+  exe "norm! A\<C-D>\<C-D>"
+  call assert_equal(repeat(" ", 10) . "word", getline(1))
+  exe "norm! I\<C-D>\<C-D>\<C-D>"
+  call assert_equal(repeat(" ", 0) . "word", getline(1))
+  exe "norm! I\<C-D>"
+  call assert_equal(repeat(" ", 0) . "word", getline(1))
+
+  " Shift by 'tabstop' right and left.
+
+  set shiftwidth=0
+  call setline(1, "word")
+
+  call setline(1, repeat(" ", 9) . "word")
+  exe "norm! 11|i\<C-T>"
+  call assert_equal(repeat(" ", 14) . "word", getline(1))
+  exe "norm! A\<C-T>"
+  call assert_equal(repeat(" ", 21) . "word", getline(1))
+  exe "norm! I  \<C-T>"
+  call assert_equal(repeat(" ", 28) . "word", getline(1))
+
+  exe "norm! I  \<C-D>"
+  call assert_equal(repeat(" ", 28) . "word", getline(1))
+  exe "norm! I  "
+  exe "norm! 32|i\<C-D>"
+  call assert_equal(repeat(" ", 28) . "word", getline(1))
+  exe "norm! A\<C-D>"
+  call assert_equal(repeat(" ", 21) . "word", getline(1))
+  exe "norm! I  "
+  exe "norm! A\<C-D>\<C-D>"
+  call assert_equal(repeat(" ", 14) . "word", getline(1))
+  exe "norm! I\<C-D>\<C-D>\<C-D>"
+  call assert_equal(repeat(" ", 0) . "word", getline(1))
+  exe "norm! I\<C-D>"
+  call assert_equal(repeat(" ", 0) . "word", getline(1))
+
+  set expandtab& shiftwidth& tabstop&
+  bw!
+endfunc
+
+" Test shifting lines with <C-T> and <C-D>, with 'vartabstop'.
+"
+func Test_shift_ins_vartabs()
+  CheckFeature vartabs
+  set expandtab                 " Don't want to worry about tabs vs. spaces in
+                                " results.
+
+  set shiftwidth=0
+  set vartabstop=19,17,11
+
+  " Shift by 'vartabstop' right and left.
+
+  call setline(1, "word")
+
+  call setline(1, repeat(" ", 9) . "word")
+  exe "norm! 11|i\<C-T>"
+  call assert_equal(repeat(" ", 19) . "word", getline(1))
+  exe "norm! A\<C-T>"
+  call assert_equal(repeat(" ", 36) . "word", getline(1))
+  exe "norm! I  \<C-T>"
+  call assert_equal(repeat(" ", 47) . "word", getline(1))
+
+  exe "norm! I  \<C-D>"
+  call assert_equal(repeat(" ", 47) . "word", getline(1))
+  exe "norm! I  "
+  exe "norm! 51|i\<C-D>"
+  call assert_equal(repeat(" ", 47) . "word", getline(1))
+  exe "norm! A\<C-D>"
+  call assert_equal(repeat(" ", 36) . "word", getline(1))
+  exe "norm! I  "
+  exe "norm! A\<C-D>\<C-D>"
+  call assert_equal(repeat(" ", 19) . "word", getline(1))
+  exe "norm! I\<C-D>\<C-D>\<C-D>"
+  call assert_equal(repeat(" ", 0) . "word", getline(1))
+  exe "norm! I\<C-D>"
+  call assert_equal(repeat(" ", 0) . "word", getline(1))
+
+  set expandtab& shiftwidth& vartabstop&
+  bw!
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index 5933af3..96b3803 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    917,
+/**/
     916,
 /**/
     915,