In vim, the expandtab option expands tabs to spaces in insert mode as well as when shifting and indenting/outdenting. This is very useful when working on a code-base where the style dictates using spaces instead of tabs for indentation.
NetBSD added an implementation of expandtab to their vi some time ago, but theirs doesn't convert tabs to spaces in insert mode. I've adapted the NetBSD patch and added support for expanding tabs in insert mode, unless escaped via ^V. The option is off by default (of course). Comments? Please, no tabs vs spaces flame wars. - todd Index: usr.bin/vi/common/options.c =================================================================== RCS file: /cvs/src/usr.bin/vi/common/options.c,v retrieving revision 1.27 diff -u -p -u -r1.27 options.c --- usr.bin/vi/common/options.c 21 May 2019 09:24:58 -0000 1.27 +++ usr.bin/vi/common/options.c 2 Apr 2020 20:43:14 -0000 @@ -69,6 +69,8 @@ OPTLIST const optlist[] = { {"escapetime", NULL, OPT_NUM, 0}, /* O_ERRORBELLS 4BSD */ {"errorbells", NULL, OPT_0BOOL, 0}, +/* O_EXPANDTAB NetBSD 5.0 */ + {"expandtab", NULL, OPT_0BOOL, 0}, /* O_EXRC System V (undocumented) */ {"exrc", NULL, OPT_0BOOL, 0}, /* O_EXTENDED 4.4BSD */ @@ -207,6 +209,7 @@ static OABBREV const abbrev[] = { {"co", O_COLUMNS}, /* 4.4BSD */ {"eb", O_ERRORBELLS}, /* 4BSD */ {"ed", O_EDCOMPATIBLE}, /* 4BSD */ + {"et", O_EXPANDTAB}, /* NetBSD 5.0 */ {"ex", O_EXRC}, /* System V (undocumented) */ {"ht", O_HARDTABS}, /* 4BSD */ {"ic", O_IGNORECASE}, /* 4BSD */ Index: usr.bin/vi/docs/USD.doc/vi.man/vi.1 =================================================================== RCS file: /cvs/src/usr.bin/vi/docs/USD.doc/vi.man/vi.1,v retrieving revision 1.77 diff -u -p -u -r1.77 vi.1 --- usr.bin/vi/docs/USD.doc/vi.man/vi.1 4 Oct 2019 20:12:01 -0000 1.77 +++ usr.bin/vi/docs/USD.doc/vi.man/vi.1 2 Apr 2020 22:05:31 -0000 @@ -1606,6 +1606,11 @@ and characters to move forward to the next .Ar shiftwidth column boundary. +If the +.Cm expandtab +option is set, only insert +.Aq space +characters. .Pp .It Aq Cm erase .It Aq Cm control-H @@ -2343,6 +2348,16 @@ key mapping. .Nm ex only. Announce error messages with a bell. +.It Cm expandtab , et Bq off +Expand +.Aq tab +characters to +.Aq space +when inserting, replacing or shifting text, autoindenting, +indenting with +.Aq Ic control-T , +or outdenting with +.Aq Ic control-D . .It Cm exrc , ex Bq off Read the startup files in the local directory. .It Cm extended Bq off Index: usr.bin/vi/docs/USD.doc/vi.ref/set.opt.roff =================================================================== RCS file: /cvs/src/usr.bin/vi/docs/USD.doc/vi.ref/set.opt.roff,v retrieving revision 1.12 diff -u -p -u -r1.12 set.opt.roff --- usr.bin/vi/docs/USD.doc/vi.ref/set.opt.roff 8 Aug 2016 15:09:33 -0000 1.12 +++ usr.bin/vi/docs/USD.doc/vi.ref/set.opt.roff 2 Apr 2020 22:05:27 -0000 @@ -96,7 +96,9 @@ the first nonblank character of the line Lines are indented using tab characters to the extent possible (based on the value of the .OP tabstop -option) and then using space characters as necessary. +option, and if +.OP expandtab +is not set) and then using space characters as necessary. For commands inserting text into the middle of a line, any blank characters to the right of the cursor are discarded, and the first nonblank character to the right of the cursor is aligned as described above. @@ -400,6 +402,17 @@ only. error messages are normally presented in inverse video. If that is not possible for the terminal, setting this option causes error messages to be announced by ringing the terminal bell. +.KY expandtab +.IP "expandtab, et [off]" +Expand +.LI <tab> +characters to +.LI <space> +when inserting, replacing or shifting text, autoindenting, +indenting with +.CO <control-T>, +or outdenting with +.CO <control-D>. .KY exrc .IP "exrc, ex [off]" If this option is turned on in the EXINIT environment variables, Index: usr.bin/vi/ex/ex_shift.c =================================================================== RCS file: /cvs/src/usr.bin/vi/ex/ex_shift.c,v retrieving revision 1.8 diff -u -p -u -r1.8 ex_shift.c --- usr.bin/vi/ex/ex_shift.c 6 Jan 2016 22:28:52 -0000 1.8 +++ usr.bin/vi/ex/ex_shift.c 2 Apr 2020 20:53:16 -0000 @@ -127,10 +127,13 @@ shift(SCR *sp, EXCMD *cmdp, enum which r * Build a new indent string and count the number of * characters it uses. */ - for (tbp = bp, newidx = 0; - newcol >= O_VAL(sp, O_TABSTOP); ++newidx) { - *tbp++ = '\t'; - newcol -= O_VAL(sp, O_TABSTOP); + tbp = bp; + newidx = 0; + if (!O_ISSET(sp, O_EXPANDTAB)) { + for (; newcol >= O_VAL(sp, O_TABSTOP); ++newidx) { + *tbp++ = '\t'; + newcol -= O_VAL(sp, O_TABSTOP); + } } for (; newcol > 0; --newcol, ++newidx) *tbp++ = ' '; Index: usr.bin/vi/ex/ex_txt.c =================================================================== RCS file: /cvs/src/usr.bin/vi/ex/ex_txt.c,v retrieving revision 1.16 diff -u -p -u -r1.16 ex_txt.c --- usr.bin/vi/ex/ex_txt.c 27 May 2016 09:18:12 -0000 1.16 +++ usr.bin/vi/ex/ex_txt.c 2 Apr 2020 20:53:58 -0000 @@ -400,8 +400,12 @@ txt_dent(SCR *sp, TEXT *tp) * * Count up spaces/tabs needed to get to the target. */ - for (cno = 0, tabs = 0; cno + COL_OFF(cno, ts) <= scno; ++tabs) - cno += COL_OFF(cno, ts); + cno = 0; + tabs = 0; + if (!O_ISSET(sp, O_EXPANDTAB)) { + for (; cno + COL_OFF(cno, ts) <= scno; ++tabs) + cno += COL_OFF(cno, ts); + } spaces = scno - cno; /* Make sure there's enough room. */ Index: usr.bin/vi/vi/v_txt.c =================================================================== RCS file: /cvs/src/usr.bin/vi/vi/v_txt.c,v retrieving revision 1.33 diff -u -p -u -r1.33 v_txt.c --- usr.bin/vi/vi/v_txt.c 27 May 2016 09:18:12 -0000 1.33 +++ usr.bin/vi/vi/v_txt.c 2 Apr 2020 22:13:09 -0000 @@ -32,7 +32,7 @@ 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); +static int txt_dent(SCR *, TEXT *, int, int); static int txt_emark(SCR *, TEXT *, size_t); static void txt_err(SCR *, TEXTH *); static int txt_fc(SCR *, TEXT *, int *); @@ -968,7 +968,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, 0); + (void)txt_dent(sp, tp, O_SHIFTWIDTH, 0); break; default: abort(); @@ -1184,7 +1184,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, 1)) + if (txt_dent(sp, tp, O_SHIFTWIDTH, 1)) goto err; goto ebuf_chk; case K_RIGHTBRACE: @@ -1213,6 +1213,13 @@ leftmargin: tp->lb[tp->cno - 1] = ' '; case K_HEXCHAR: hexcnt = 1; goto insq_ch; + case K_TAB: + if (quote != Q_VTHIS && O_ISSET(sp, O_EXPANDTAB)) { + if (txt_dent(sp, tp, O_TABSTOP, 1)) + goto err; + goto ebuf_chk; + } + goto insq_ch; default: /* Insert the character. */ ins_ch: /* * Historically, vi eliminated nul's out of hand. If the @@ -1683,13 +1690,19 @@ txt_ai_resolve(SCR *sp, TEXT *tp, int *c /* * If there are no spaces, or no tabs after spaces and less than * ts spaces, it's already minimal. + * Keep analysing if expandtab is set. */ - if (!spaces || (!tab_after_sp && spaces < ts)) + if ((!spaces || (!tab_after_sp && spaces < ts)) && + !O_ISSET(sp, O_EXPANDTAB)) return; /* Count up spaces/tabs needed to get to the target. */ - for (cno = 0, tabs = 0; cno + COL_OFF(cno, ts) <= scno; ++tabs) - cno += COL_OFF(cno, ts); + cno = 0; + tabs = 0; + if (!O_ISSET(sp, O_EXPANDTAB)) { + for (; cno + COL_OFF(cno, ts) <= scno; ++tabs) + cno += COL_OFF(cno, ts); + } spaces = scno - cno; /* @@ -1846,7 +1859,7 @@ txt_backup(SCR *sp, TEXTH *tiqh, TEXT *t * changes. */ static int -txt_dent(SCR *sp, TEXT *tp, int isindent) +txt_dent(SCR *sp, TEXT *tp, int swopt, int isindent) { CHAR_T ch; u_long sw, ts; @@ -1854,7 +1867,7 @@ txt_dent(SCR *sp, TEXT *tp, int isindent int ai_reset; ts = O_VAL(sp, O_TABSTOP); - sw = O_VAL(sp, O_SHIFTWIDTH); + sw = O_VAL(sp, swopt); /* * Since we don't know what precedes the character(s) being inserted @@ -1921,9 +1934,12 @@ txt_dent(SCR *sp, TEXT *tp, int isindent if (current >= target) spaces = tabs = 0; else { - for (cno = current, - tabs = 0; cno + COL_OFF(cno, ts) <= target; ++tabs) - cno += COL_OFF(cno, ts); + cno = current; + tabs = 0; + if (!O_ISSET(sp, O_EXPANDTAB)) { + for (; cno + COL_OFF(cno, ts) <= target; ++tabs) + cno += COL_OFF(cno, ts); + } spaces = target - cno; }