patch 8.2.3227: 'virtualedit' can only be set globally

Problem:    'virtualedit' can only be set globally.
Solution:   Make 'virtualedit' global-local. (Gary Johnson, closes #8638)
diff --git a/src/buffer.c b/src/buffer.c
index 25efea1..59397e4 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -2384,6 +2384,7 @@
 #endif
     clear_string_option(&buf->b_p_bkc);
     clear_string_option(&buf->b_p_menc);
+    clear_string_option(&buf->b_p_ve);
 }
 
 /*
diff --git a/src/change.c b/src/change.c
index ee1bd6f..f77b48c 100644
--- a/src/change.c
+++ b/src/change.c
@@ -1257,7 +1257,7 @@
 	// fixpos is TRUE, we don't want to end up positioned at the NUL,
 	// unless "restart_edit" is set or 'virtualedit' contains "onemore".
 	if (col > 0 && fixpos && restart_edit == 0
-					      && (ve_flags & VE_ONEMORE) == 0)
+					      && (get_ve_flags() & VE_ONEMORE) == 0)
 	{
 	    --curwin->w_cursor.col;
 	    curwin->w_cursor.coladd = 0;
diff --git a/src/drawscreen.c b/src/drawscreen.c
index 49615bd..36aad63 100644
--- a/src/drawscreen.c
+++ b/src/drawscreen.c
@@ -2006,21 +2006,21 @@
 	    {
 		colnr_T	    fromc, toc;
 #if defined(FEAT_LINEBREAK)
-		int	    save_ve_flags = ve_flags;
+		int	    save_ve_flags = curbuf->b_ve_flags;
 
 		if (curwin->w_p_lbr)
-		    ve_flags = VE_ALL;
+		    curbuf->b_ve_flags = VE_ALL;
 #endif
 		getvcols(wp, &VIsual, &curwin->w_cursor, &fromc, &toc);
 		++toc;
 #if defined(FEAT_LINEBREAK)
-		ve_flags = save_ve_flags;
+		curbuf->b_ve_flags = save_ve_flags;
 #endif
 		// Highlight to the end of the line, unless 'virtualedit' has
 		// "block".
 		if (curwin->w_curswant == MAXCOL)
 		{
-		    if (ve_flags & VE_BLOCK)
+		    if (get_ve_flags() & VE_BLOCK)
 		    {
 			pos_T	    pos;
 			int	    cursor_above =
diff --git a/src/edit.c b/src/edit.c
index 267c76d..6bdeded 100644
--- a/src/edit.c
+++ b/src/edit.c
@@ -861,7 +861,7 @@
 	    ins_ctrl_o();
 
 	    // don't move the cursor left when 'virtualedit' has "onemore".
-	    if (ve_flags & VE_ONEMORE)
+	    if (get_ve_flags() & VE_ONEMORE)
 	    {
 		ins_at_eol = FALSE;
 		nomove = TRUE;
@@ -2673,7 +2673,7 @@
 
     // move "l" bytes right, but don't end up on the NUL, unless 'virtualedit'
     // contains "onemore".
-    if (ptr[l] == NUL && (ve_flags & VE_ONEMORE) == 0)
+    if (ptr[l] == NUL && (get_ve_flags() & VE_ONEMORE) == 0)
 	return FAIL;
     curwin->w_cursor.col += l;
 
@@ -3656,7 +3656,7 @@
 #endif
 				      )
     {
-	if (curwin->w_cursor.coladd > 0 || ve_flags == VE_ALL)
+	if (curwin->w_cursor.coladd > 0 || get_ve_flags() == VE_ALL)
 	{
 	    oneleft();
 	    if (restart_edit != NUL)
diff --git a/src/misc2.c b/src/misc2.c
index a8e34df..8e99b01 100644
--- a/src/misc2.c
+++ b/src/misc2.c
@@ -22,14 +22,16 @@
     int
 virtual_active(void)
 {
+    unsigned int cur_ve_flags = get_ve_flags();
+
     // While an operator is being executed we return "virtual_op", because
     // VIsual_active has already been reset, thus we can't check for "block"
     // being used.
     if (virtual_op != MAYBE)
 	return virtual_op;
-    return (ve_flags == VE_ALL
-	    || ((ve_flags & VE_BLOCK) && VIsual_active && VIsual_mode == Ctrl_V)
-	    || ((ve_flags & VE_INSERT) && (State & INSERT)));
+    return (cur_ve_flags == VE_ALL
+	    || ((cur_ve_flags & VE_BLOCK) && VIsual_active && VIsual_mode == Ctrl_V)
+	    || ((cur_ve_flags & VE_INSERT) && (State & INSERT)));
 }
 
 /*
@@ -137,7 +139,7 @@
     one_more = (State & INSERT)
 		    || restart_edit != NUL
 		    || (VIsual_active && *p_sel != 'o')
-		    || ((ve_flags & VE_ONEMORE) && wcol < MAXCOL);
+		    || ((get_ve_flags() & VE_ONEMORE) && wcol < MAXCOL);
     line = ml_get_buf(curbuf, pos->lnum, FALSE);
 
     if (wcol >= MAXCOL)
@@ -549,9 +551,10 @@
     void
 check_cursor_col_win(win_T *win)
 {
-    colnr_T len;
-    colnr_T oldcol = win->w_cursor.col;
-    colnr_T oldcoladd = win->w_cursor.col + win->w_cursor.coladd;
+    colnr_T      len;
+    colnr_T      oldcol = win->w_cursor.col;
+    colnr_T      oldcoladd = win->w_cursor.col + win->w_cursor.coladd;
+    unsigned int cur_ve_flags = get_ve_flags();
 
     len = (colnr_T)STRLEN(ml_get_buf(win->w_buffer, win->w_cursor.lnum, FALSE));
     if (len == 0)
@@ -564,7 +567,7 @@
 	// - 'virtualedit' is set
 	if ((State & INSERT) || restart_edit
 		|| (VIsual_active && *p_sel != 'o')
-		|| (ve_flags & VE_ONEMORE)
+		|| (cur_ve_flags & VE_ONEMORE)
 		|| virtual_active())
 	    win->w_cursor.col = len;
 	else
@@ -583,7 +586,7 @@
     // line.
     if (oldcol == MAXCOL)
 	win->w_cursor.coladd = 0;
-    else if (ve_flags == VE_ALL)
+    else if (cur_ve_flags == VE_ALL)
     {
 	if (oldcoladd > win->w_cursor.col)
 	{
diff --git a/src/normal.c b/src/normal.c
index 56f89f5..74c76b2 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -5757,7 +5757,7 @@
 
     // Corner case: the 0 position in a tab may change when going into
     // virtualedit.  Recalculate curwin->w_cursor to avoid bad highlighting.
-    if (c == Ctrl_V && (ve_flags & VE_BLOCK) && gchar_cursor() == TAB)
+    if (c == Ctrl_V && (get_ve_flags() & VE_BLOCK) && gchar_cursor() == TAB)
     {
 	validate_virtcol();
 	coladvance(curwin->w_virtcol);
@@ -6780,7 +6780,7 @@
     // - 'virtualedit' is not "all" and not "onemore".
     if (curwin->w_cursor.col > 0 && gchar_cursor() == NUL
 		&& (!VIsual_active || *p_sel == 'o')
-		&& !virtual_active() && (ve_flags & VE_ONEMORE) == 0)
+		&& !virtual_active() && (get_ve_flags() & VE_ONEMORE) == 0)
     {
 	--curwin->w_cursor.col;
 	// prevent cursor from moving on the trail byte
@@ -7014,7 +7014,7 @@
 set_cursor_for_append_to_line(void)
 {
     curwin->w_set_curswant = TRUE;
-    if (ve_flags == VE_ALL)
+    if (get_ve_flags() == VE_ALL)
     {
 	int save_State = State;
 
diff --git a/src/ops.c b/src/ops.c
index 59d5bff..75619c5 100644
--- a/src/ops.c
+++ b/src/ops.c
@@ -1474,18 +1474,22 @@
 	// doing block_prep().  When only "block" is used, virtual edit is
 	// already disabled, but still need it when calling
 	// coladvance_force().
+	// coladvance_force() uses get_ve_flags() to get the 'virtualedit'
+	// state for the current buffer.  To override that state, we need to
+	// set the buffer-local value of ve_flags rather than the global value.
 	if (curwin->w_cursor.coladd > 0)
 	{
-	    int		old_ve_flags = ve_flags;
+	    int		old_ve_flags = curbuf->b_ve_flags;
 
-	    ve_flags = VE_ALL;
 	    if (u_save_cursor() == FAIL)
 		return;
+
+	    curbuf->b_ve_flags = VE_ALL;
 	    coladvance_force(oap->op_type == OP_APPEND
 					   ? oap->end_vcol + 1 : getviscol());
 	    if (oap->op_type == OP_APPEND)
 		--curwin->w_cursor.col;
-	    ve_flags = old_ve_flags;
+	    curbuf->b_ve_flags = old_ve_flags;
 	}
 	// Get the info about the block before entering the text
 	block_prep(oap, &bd, oap->start.lnum, TRUE);
@@ -1816,15 +1820,17 @@
     void
 adjust_cursor_eol(void)
 {
+    unsigned int cur_ve_flags = get_ve_flags();
+
     if (curwin->w_cursor.col > 0
 	    && gchar_cursor() == NUL
-	    && (ve_flags & VE_ONEMORE) == 0
+	    && (cur_ve_flags & VE_ONEMORE) == 0
 	    && !(restart_edit || (State & INSERT)))
     {
 	// Put the cursor on the last character in the line.
 	dec_cursor();
 
-	if (ve_flags == VE_ALL)
+	if (cur_ve_flags == VE_ALL)
 	{
 	    colnr_T	    scol, ecol;
 
diff --git a/src/option.c b/src/option.c
index 21d113e..182ff34 100644
--- a/src/option.c
+++ b/src/option.c
@@ -5181,6 +5181,10 @@
 	    set_chars_option((win_T *)from, &((win_T *)from)->w_p_lcs);
 	    redraw_later(NOT_VALID);
 	    break;
+	case PV_VE:
+	    clear_string_option(&buf->b_p_ve);
+	    buf->b_ve_flags = 0;
+	    break;
     }
 }
 #endif
@@ -5239,7 +5243,8 @@
 #endif
 	    case PV_BKC:  return (char_u *)&(curbuf->b_p_bkc);
 	    case PV_MENC: return (char_u *)&(curbuf->b_p_menc);
-	    case PV_LCS: return (char_u *)&(curwin->w_p_lcs);
+	    case PV_LCS:  return (char_u *)&(curwin->w_p_lcs);
+	    case PV_VE:	  return (char_u *)&(curbuf->b_p_ve);
 
 	}
 	return NULL; // "cannot happen"
@@ -5507,6 +5512,8 @@
 	case PV_VSTS:	return (char_u *)&(curbuf->b_p_vsts);
 	case PV_VTS:	return (char_u *)&(curbuf->b_p_vts);
 #endif
+	case PV_VE:	return *curbuf->b_p_ve != NUL
+				    ? (char_u *)&(curbuf->b_p_ve) : p->var;
 	default:	iemsg(_("E356: get_varp ERROR"));
     }
     // always return a valid pointer to avoid a crash!
@@ -6084,6 +6091,8 @@
 	    buf->b_p_lw = empty_option;
 #endif
 	    buf->b_p_menc = empty_option;
+	    buf->b_p_ve = empty_option;
+	    buf->b_ve_flags = 0;
 
 	    /*
 	     * Don't copy the options set by ex_help(), use the saved values,
@@ -7026,6 +7035,16 @@
     return buf->b_bkc_flags ? buf->b_bkc_flags : bkc_flags;
 }
 
+/*
+ * Get the local or global value of the 'virtualedit' flags.
+ */
+    unsigned int
+get_ve_flags(void)
+{
+    return (curbuf->b_ve_flags ? curbuf->b_ve_flags : ve_flags)
+	   & ~(VE_NONE | VE_NONEU);
+}
+
 #if defined(FEAT_LINEBREAK) || defined(PROTO)
 /*
  * Get the local or global value of 'showbreak'.
diff --git a/src/option.h b/src/option.h
index 7be729a..5732e82 100644
--- a/src/option.h
+++ b/src/option.h
@@ -1052,6 +1052,8 @@
 #define VE_INSERT	6	// includes "all"
 #define VE_ALL		4
 #define VE_ONEMORE	8
+#define VE_NONE		16
+#define VE_NONEU	32      // Upper-case NONE
 EXTERN long	p_verbose;	// 'verbose'
 #ifdef IN_OPTION_C
 char_u	*p_vfile = (char_u *)""; // used before options are initialized
@@ -1228,6 +1230,7 @@
     , BV_VSTS
     , BV_VTS
 #endif
+    , BV_VE
     , BV_COUNT	    // must be the last one
 };
 
diff --git a/src/optiondefs.h b/src/optiondefs.h
index 782e4ef..ec89558 100644
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -153,6 +153,7 @@
 # define PV_VSTS		OPT_BUF(BV_VSTS)
 # define PV_VTS		OPT_BUF(BV_VTS)
 #endif
+#define PV_VE		OPT_BOTH(OPT_BUF(BV_VE))
 
 // Definition of the PV_ values for window-local options.
 // The WV_ values are defined in option.h.
@@ -2806,7 +2807,7 @@
 			    SCTX_INIT},
     {"virtualedit", "ve",   P_STRING|P_ONECOMMA|P_NODUP|P_VI_DEF
 							    |P_VIM|P_CURSWANT,
-			    (char_u *)&p_ve, PV_NONE,
+			    (char_u *)&p_ve, PV_VE,
 			    {(char_u *)"", (char_u *)""}
 			    SCTX_INIT},
     {"visualbell",  "vb",   P_BOOL|P_VI_DEF,
diff --git a/src/optionstr.c b/src/optionstr.c
index 4c1cacc..c22a441 100644
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -56,7 +56,7 @@
 #if defined(UNIX) || defined(VMS)
 static char *(p_ttym_values[]) = {"xterm", "xterm2", "dec", "netterm", "jsbterm", "pterm", "urxvt", "sgr", NULL};
 #endif
-static char *(p_ve_values[]) = {"block", "insert", "all", "onemore", NULL};
+static char *(p_ve_values[]) = {"block", "insert", "all", "onemore", "none", "NONE", NULL};
 static char *(p_wop_values[]) = {"tagfile", NULL};
 #ifdef FEAT_WAK
 static char *(p_wak_values[]) = {"yes", "menu", "no", NULL};
@@ -298,6 +298,7 @@
     check_string_option(&buf->b_p_vsts);
     check_string_option(&buf->b_p_vts);
 #endif
+    check_string_option(&buf->b_p_ve);
 }
 
 /*
@@ -2075,16 +2076,31 @@
 #endif
 
     // 'virtualedit'
-    else if (varp == &p_ve)
+    else if (gvarp == &p_ve)
     {
-	if (opt_strings_flags(p_ve, p_ve_values, &ve_flags, TRUE) != OK)
-	    errmsg = e_invarg;
-	else if (STRCMP(p_ve, oldval) != 0)
+	char_u		*ve = p_ve;
+	unsigned int	*flags = &ve_flags;
+
+	if (opt_flags & OPT_LOCAL)
 	{
-	    // Recompute cursor position in case the new 've' setting
-	    // changes something.
-	    validate_virtcol();
-	    coladvance(curwin->w_virtcol);
+	    ve = curbuf->b_p_ve;
+	    flags = &curbuf->b_ve_flags;
+	}
+
+	if ((opt_flags & OPT_LOCAL) && *ve == NUL)
+	    // make the local value empty: use the global value
+	    *flags = 0;
+	else
+	{
+	    if (opt_strings_flags(ve, p_ve_values, flags, TRUE) != OK)
+		errmsg = e_invarg;
+	    else if (STRCMP(p_ve, oldval) != 0)
+	    {
+		// Recompute cursor position in case the new 've' setting
+		// changes something.
+		validate_virtcol();
+		coladvance(curwin->w_virtcol);
+	    }
 	}
     }
 
diff --git a/src/proto/option.pro b/src/proto/option.pro
index 25cfb65..13b9c1b 100644
--- a/src/proto/option.pro
+++ b/src/proto/option.pro
@@ -73,6 +73,7 @@
 long get_scrolloff_value(void);
 long get_sidescrolloff_value(void);
 unsigned int get_bkc_value(buf_T *buf);
+unsigned int get_ve_flags(void);
 char_u *get_showbreak_value(win_T *win);
 dict_T *get_winbuf_options(int bufopt);
 int fill_culopt_flags(char_u *val, win_T *wp);
diff --git a/src/register.c b/src/register.c
index 0c714c6..1615874 100644
--- a/src/register.c
+++ b/src/register.c
@@ -1556,6 +1556,7 @@
     long	cnt;
     pos_T	orig_start = curbuf->b_op_start;
     pos_T	orig_end = curbuf->b_op_end;
+    unsigned int cur_ve_flags = get_ve_flags();
 
 #ifdef FEAT_CLIPBOARD
     // Adjust register name for "unnamed" in 'clipboard'.
@@ -1742,7 +1743,7 @@
 
     yanklen = (int)STRLEN(y_array[0]);
 
-    if (ve_flags == VE_ALL && y_type == MCHAR)
+    if (cur_ve_flags == VE_ALL && y_type == MCHAR)
     {
 	if (gchar_cursor() == TAB)
 	{
@@ -1777,7 +1778,7 @@
 
 	if (dir == FORWARD && c != NUL)
 	{
-	    if (ve_flags == VE_ALL)
+	    if (cur_ve_flags == VE_ALL)
 		getvcol(curwin, &curwin->w_cursor, &col, NULL, &endcol2);
 	    else
 		getvcol(curwin, &curwin->w_cursor, NULL, NULL, &col);
@@ -1786,7 +1787,7 @@
 		// move to start of next multi-byte character
 		curwin->w_cursor.col += (*mb_ptr2len)(ml_get_cursor());
 	    else
-	    if (c != TAB || ve_flags != VE_ALL)
+	    if (c != TAB || cur_ve_flags != VE_ALL)
 		++curwin->w_cursor.col;
 	    ++col;
 	}
@@ -1794,7 +1795,7 @@
 	    getvcol(curwin, &curwin->w_cursor, &col, NULL, &endcol2);
 
 	col += curwin->w_cursor.coladd;
-	if (ve_flags == VE_ALL
+	if (cur_ve_flags == VE_ALL
 		&& (curwin->w_cursor.coladd > 0
 		    || endcol2 == curwin->w_cursor.col))
 	{
diff --git a/src/structs.h b/src/structs.h
index bf8102c..09f0703 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -2969,6 +2969,8 @@
 #ifdef FEAT_TERMINAL
     long	b_p_twsl;	// 'termwinscroll'
 #endif
+    char_u	*b_p_ve;	// 'virtualedit' local value
+    unsigned	b_ve_flags;     // flags for 'virtualedit'
 
     /*
      * end of buffer options
diff --git a/src/testdir/test_virtualedit.vim b/src/testdir/test_virtualedit.vim
index 7f6e08c..f3cc260 100644
--- a/src/testdir/test_virtualedit.vim
+++ b/src/testdir/test_virtualedit.vim
@@ -402,4 +402,124 @@
   bw!
 endfunc
 
+" After calling s:TryVirtualeditReplace(), line 1 will contain one of these
+" two strings, depending on whether virtual editing is on or off.
+let s:result_ve_on  = 'a      x'
+let s:result_ve_off = 'x'
+
+" Utility function for Test_global_local()
+func s:TryVirtualeditReplace()
+  call setline(1, 'a')
+  normal gg7l
+  normal rx
+endfunc
+
+" Test for :set and :setlocal
+func Test_global_local()
+  new
+
+  " Verify that 'virtualedit' is initialized to empty, can be set globally to
+  " all and to empty, and can be set locally to all and to empty.
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+  set ve=all
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  set ve=
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+  setlocal ve=all
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  setlocal ve=
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+
+  " Verify that :set affects multiple buffers
+  new
+  set ve=all
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  wincmd p
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  set ve=
+  wincmd p
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+  bwipe!
+
+  " Verify that :setlocal affects only the current buffer
+  setlocal ve=all
+  new
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+  setlocal ve=all
+  wincmd p
+  setlocal ve=
+  wincmd p
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  bwipe!
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+
+  " Verify that the buffer 'virtualedit' state follows the global value only
+  " when empty and that "none" works as expected.
+  "
+  "          'virtualedit' State
+  " +--------+--------------------------+
+  " | Local  |          Global          |
+  " |        |                          |
+  " +--------+--------+--------+--------+
+  " |        | ""     | "all"  | "none" |
+  " +--------+--------+--------+--------+
+  " | ""     |  off   |  on    |  off   |
+  " | "all"  |  on    |  on    |  on    |
+  " | "none" |  off   |  off   |  off   |
+  " +--------+--------+--------+--------+
+  new
+
+  setglobal ve=
+  setlocal ve=
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+  setlocal ve=all
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  setlocal ve=none
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+
+  setglobal ve=all
+  setlocal ve=
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  setlocal ve=all
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  setlocal ve=none
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+  setlocal ve=NONE
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+
+  setglobal ve=none
+  setlocal ve=
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+  setlocal ve=all
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_on, getline(1))
+  setlocal ve=none
+  call s:TryVirtualeditReplace()
+  call assert_equal(s:result_ve_off, getline(1))
+
+  bwipe!
+
+  setlocal virtualedit&
+  set virtualedit&
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index 0d60cc2..cdf5d4e 100644
--- a/src/version.c
+++ b/src/version.c
@@ -756,6 +756,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    3227,
+/**/
     3226,
 /**/
     3225,