Index | Thread | Search

From:
jerry <mail@jerryfletcher.org>
Subject:
vi(1) bug autoindent with expandtab
To:
tech@openbsd.org
Date:
Mon, 15 Sep 2025 23:31:21 +0200

Download raw body.

Thread
  • jerry:

    vi(1) bug autoindent with expandtab

Hello,

while using vi(1) I found a bug that I managed to fix.
The bug happens also in nvi(1) from ports.

Right now autoindent behaves differently when expandtab is also set.

Steps to replicate the bug:
* open vi with just autoindent
* go into an empty line, go into insert mode and type <tab>
* esc, move around and go back to the line
* notice that tab stays there

Repeating the same process but with also expandtab set, will leave the
line empty, without the 2/4/8 spaces (the expanded tab).

With expandtab, <tab> behaves in the same way as <control-T>.  Repeating
the process above with <control-T> with or without expandtab set will
result in the line being cleared.

The bug happens because <tab> with expandtab set calls the same function
(txt_dent) that is called for <control-T>, probably because the
expandtab feature was added afterwards, and this difference was not
noticed.  I have fixed the function so that when called for <tab> it
does not reset the autoindent characters.

Bye,
jerry

diff --git vi/v_txt.c vi/v_txt.c
index 5f245a0d457..202b4852ed2 100644
--- vi/v_txt.c
+++ vi/v_txt.c
@@ -29,10 +29,12 @@
 
 #define MINIMUM(a, b)	(((a) < (b)) ? (a) : (b))
 
+enum dent_type {D_INDENT, D_OUTDENT, D_TAB};
+
 static int	 txt_abbrev(SCR *, TEXT *, CHAR_T *, int, int *, int *);
 static void	 txt_ai_resolve(SCR *, TEXT *, int *);
 static TEXT	*txt_backup(SCR *, TEXTH *, TEXT *, u_int32_t *);
-static int	 txt_dent(SCR *, TEXT *, int, int);
+static int	 txt_dent(SCR *, TEXT *, int, enum dent_type);
 static int	 txt_emark(SCR *, TEXT *, size_t);
 static void	 txt_err(SCR *, TEXTH *);
 static int	 txt_fc(SCR *, TEXT *, int *);
@@ -968,7 +970,7 @@ leftmargin:		tp->lb[tp->cno - 1] = ' ';
 			if (tp->ai == 0 || tp->cno > tp->ai + tp->offset)
 				goto ins_ch;
 
-			(void)txt_dent(sp, tp, O_SHIFTWIDTH, 0);
+			(void)txt_dent(sp, tp, O_SHIFTWIDTH, D_OUTDENT);
 			break;
 		default:
 			abort();
@@ -1184,7 +1186,7 @@ leftmargin:		tp->lb[tp->cno - 1] = ' ';
 	case K_CNTRLT:			/* Add autoindent characters. */
 		if (!LF_ISSET(TXT_CNTRLT))
 			goto ins_ch;
-		if (txt_dent(sp, tp, O_SHIFTWIDTH, 1))
+		if (txt_dent(sp, tp, O_SHIFTWIDTH, D_INDENT))
 			goto err;
 		goto ebuf_chk;
 	case K_RIGHTBRACE:
@@ -1216,7 +1218,7 @@ leftmargin:		tp->lb[tp->cno - 1] = ' ';
 	case K_TAB:
 		if (sp->showmode != SM_COMMAND && quote != Q_VTHIS &&
 		    O_ISSET(sp, O_EXPANDTAB)) {
-			if (txt_dent(sp, tp, O_TABSTOP, 1))
+			if (txt_dent(sp, tp, O_TABSTOP, D_TAB))
 				goto err;
 			goto ebuf_chk;
 		}
@@ -1854,13 +1856,13 @@ txt_backup(SCR *sp, TEXTH *tiqh, TEXT *tp, u_int32_t *flagsp)
  * there are screens with different character representations.
  *
  * txt_dent --
- *	Handle ^T indents, ^D outdents.
+ *	Handle ^T indents, ^D outdents and <tab> when expandtab is set.
  *
  * If anything changes here, check the ex version to see if it needs similar
  * changes.
  */
 static int
-txt_dent(SCR *sp, TEXT *tp, int swopt, int isindent)
+txt_dent(SCR *sp, TEXT *tp, int swopt, enum dent_type dent_type)
 {
 	CHAR_T ch;
 	u_long sw, ts;
@@ -1891,7 +1893,7 @@ txt_dent(SCR *sp, TEXT *tp, int swopt, int isindent)
 		    COL_OFF(current, ts) : KEY_LEN(sp, tp->lb[cno]);
 
 	target = current;
-	if (isindent)
+	if (dent_type == D_INDENT || dent_type == D_TAB)
 		target += COL_OFF(target, sw);
 	else {
 		--target;
@@ -1904,9 +1906,12 @@ txt_dent(SCR *sp, TEXT *tp, int swopt, int isindent)
 	 * and the indent flag because there's no single test.  (^T can only
 	 * be detected by the cursor position, and while we know that the test
 	 * is always true for ^D, the cursor can be in more than one place, as
-	 * "0^D" and "^D" are different.)
+	 * "0^D" and "^D" are different). Since <tab> without expandtab is a
+	 * normal character and does not reset the ai count, we do not reset it
+	 * also when expandtab is set.
 	 */
-	ai_reset = !isindent || tp->cno == tp->ai + tp->offset;
+	ai_reset = dent_type == D_OUTDENT ||
+	    (dent_type == D_INDENT && tp->cno == tp->ai + tp->offset);
 
 	/*
 	 * Back up over any previous <blank> characters, changing them into
@@ -1937,10 +1942,9 @@ txt_dent(SCR *sp, TEXT *tp, int swopt, int isindent)
 	else {
 		cno = current;
 		tabs = 0;
-		if (!O_ISSET(sp, O_EXPANDTAB)) {
+		if (!O_ISSET(sp, O_EXPANDTAB))
 			for (; cno + COL_OFF(cno, ts) <= target; ++tabs)
 				cno += COL_OFF(cno, ts);
-			}
 		spaces = target - cno;
 	}