Subject: bug#79038: [PATCH] Add support for Vi/Vim modelines.

From: James Youngman <[email-masked]>
Date: Thu, 17 Jul 2025 08:44:27 +0100

I'd like to contribute support for Vi/Vim modelines.  These are
somewhat similar to Emacs' local variables.  The basic idea is that
this makes it easier to collaborate with a software author who uses Vi
modelines to correctly set up their editor, and expects others to use
consistent settings.

This change should be covered by the copyright assignment my employer
(Google) already made for GNU Emacs.

This is my first Emacs patch, so I apologise for any misunderstandings
or omissions.  The patch itself is attached, but here is the ChangeLog
entry:

    Add support for Vi/Vim modelines.

    * lisp/emulation/vimvars.el: New file, implementing support for Vi
      modelines.
    * test/lisp/emulation/vimvars-tests.el: New file; tests for this
      feature.
    * doc/emacs/custom.texi(Vimvars): Describe this feature.
    * doc/misc/viper.texi(Modeline): Mention this feature.
    * etc/NEWS: Mention this change.
From 2c71cefd1452ddeb8631729bdfb4f39ccbc060eb Mon Sep 17 00:00:00 2001
From: James Youngman <[email-masked]>
Date: Thu, 17 Jul 2025 08:27:17 +0100
Subject: [PATCH] Add support for Vi/Vim modelines.

* lisp/emulation/vimvars.el: New file, implementing support for Vi
  modelines.
* test/lisp/emulation/vimvars-tests.el: New file; tests for this
  feature.
* doc/emacs/custom.texi(Vimvars): Describe this feature.
* doc/misc/viper.texi(Modeline): Mention this feature.
* etc/NEWS: Mention this change.
---
 doc/emacs/custom.texi                |  24 ++
 doc/misc/viper.texi                  |  16 +
 etc/NEWS                             |   6 +
 lisp/emulation/vimvars.el            | 222 ++++++++++++++
 test/lisp/emulation/vimvars-tests.el | 428 +++++++++++++++++++++++++++
 5 files changed, 696 insertions(+)
 create mode 100644 lisp/emulation/vimvars.el
 create mode 100644 test/lisp/emulation/vimvars-tests.el

diff --git a/doc/emacs/custom.texi b/doc/emacs/custom.texi
index 9b545b9fd0a..bfff00cc8f1 100644
--- a/doc/emacs/custom.texi
+++ b/doc/emacs/custom.texi
@@ -777,6 +777,7 @@ Variables
 * Directory Variables:: How variable values can be specified by directory.
 * Connection Variables:: Variables which are valid for buffers with a
                            remote default directory.
+* Vimvars::             Support for Vi/Vim modelines.
 @end menu
 
 @node Examining
@@ -1672,6 +1673,29 @@ Connection Variables
 by setting the respective variable's @code{permanent-local} symbol
 property to non-@code{nil}.
 
+@node Vimvars
+@subsection Vimvars: Support for Vi/Vim modelines
+@cindex Vim modelines
+@cindex Vi modelines
+
+The Vi editor (and many of its clones and implementations, including
+Vim) supports a feature like file local variables. Emacs can optionally
+support these, like this:
+
+@findex vimvars-obey-vim-modeline
+@lisp
+(require 'vimvars)
+(add-hook 'find-file-hook 'vimvars-obey-vim-modeline)
+@end lisp
+
+@cindex risky variable
+The Vim modeline setting @code{makeprg} is translated to
+@code{compile-command}, but this is not treated as an unsafe local
+variable.
+
+This has an effect similar to adding @samp{set modeline} to
+@file{~/.vimrc}.
+
 @node Key Bindings
 @section Customizing Key Bindings
 @cindex key bindings
diff --git a/doc/misc/viper.texi b/doc/misc/viper.texi
index 6932d29c296..c8f5375817b 100644
--- a/doc/misc/viper.texi
+++ b/doc/misc/viper.texi
@@ -1610,6 +1610,7 @@ Customization
 * Packages that Change Keymaps:: How to deal with such beasts.
 * Viper Specials::               Special Viper commands.
 * Vi Macros::                    How to do Vi style macros.
+* Modeline::                     Emulation for Vi modelines.
 @end menu
 
 @node Rudimentary Changes
@@ -2993,6 +2994,21 @@ Vi Macros
 currently defined.  To see all macros along with their definitions, type
 @kbd{M-x viper-describe-kbd-macros}.
 
+@node Modeline
+@section Emulation for Vi modelines
+
+You can enable limited support for Vi modelines, with or without Viper,
+with @code{vimvars}:
+
+@findex vimvars-obey-vim-modeline
+@lisp
+(require 'vimvars)
+(add-hook 'find-file-hook 'vimvars-obey-vim-modeline)
+@end lisp
+
+@xref{Vimvars,,Vimvars: Support for Vi/Vim modelines,emacs,The GNU Emacs
+manual}, for more info.
+
 @node Commands
 @chapter Commands
 
diff --git a/etc/NEWS b/etc/NEWS
index a81d1105443..81f262ecb1e 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2419,6 +2419,12 @@ A major mode based on the tree-sitter library for editing "go.work"
 files.  If tree-sitter is properly set-up by the user, it can be
 enabled for files named "go.work".
 
++++
+** Support for Vim/Vi modelines
+Emacs can now support Vim/Vi modelines.
+
+Add 'vimvars-obey-vim-modeline' to 'find-file-hooks' to use this.
+
 
 * Incompatible Lisp Changes in Emacs 31.1
 
diff --git a/lisp/emulation/vimvars.el b/lisp/emulation/vimvars.el
new file mode 100644
index 00000000000..50b56cc6912
--- /dev/null
+++ b/lisp/emulation/vimvars.el
@@ -0,0 +1,222 @@
+;;; vimvars.el --- Emacs support for Vi modelines
+
+;; Copyright (C) 2010,2011,2025 Free Software Foundation, Inc.
+
+;; Author: James Youngman <[email-masked]>
+;; Maintainer: James Youngman <[email-masked]>
+;; Keywords: local-variables, vi, vim, emulations
+
+;; vimvars is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; vimvars is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with vimvars.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Defines a function `vimvars-obey-vim-modeline' which is suitable for
+;; a hook which checks for a Vi-style in the current buffer and sets
+;; various Emacs buffer-local variables accordingly.
+
+;;; Code:
+
+;;; We say "Vim" here for consistency with Emacs documentation (which
+;;; uses "Vi"), instead of "VIM" which is what that editor's
+;;; documentation uses as its name.
+
+(defgroup vimvars nil
+  "Support for Vim mode lines."
+  :version "31.1"
+  :group 'find-file)
+
+
+(defcustom vimvars-enabled t
+  "If nil, Vim mode lines will be ignored."
+  :version "31.1"
+  :type 'boolean
+  :group 'vimvars)
+(make-variable-buffer-local 'vimvars-enabled)
+
+
+(defcustom vimvars-check-lines 5
+  "The number of lines in the top or bottom of a file that we will search for Vim settings (Vim itself checks 5)."
+  :version "31.1"
+  :type 'integer
+  :group 'vimvars)
+
+
+(defcustom vimvars-ignore-mode-line-if-local-variables-exist t
+  "If non-nil, Vim mode lines are ignored in files that have Emacs local variables."
+  :version "31.1"
+  :type 'boolean
+  :group 'vimvars)
+
+;; It appears that real Vim accepts backslash-escaped characters (for
+;; example \\| inside makeprg).
+;;
+;; Also, Vim accepts vi: and vim: at start-of line (but not ex:)
+(defconst vimvars-modeline-re
+  "\\(^\\|[ \t]\\)\\(ex\\|vim?\\):[\t ]?\\(set\\|setlocal\\|se\\)? \\([^:]+\\):"
+  "Regex matching a Vim modeline.")
+
+(defun vimvars-obey-vim-modeline ()
+  "Check the top and bottom of a file for Vim-style settings, and obey them.
+Only the first and last `vimvars-check-lines' lines of the file
+are checked for Vim variables.   You can use this in `find-file-hook'.
+
+One enables modelines in Vim by putting 'set modeline' in the
+.vimrc file."
+  (when (vimvars--should-obey-modeline)
+    (save-excursion
+      (or (vimvars--obey-top-modeline)
+          (vimvars--obey-bottom-modeline)))))
+
+;;; Implementation (you probably don't need to call these functions
+;;; directly).
+
+(defun vimvars--should-obey-modeline ()
+  "Return non-nil if a Vim modeline should be obeyed in this file."
+  ;; Always return nil if vimvars-enabled is nil.
+  ;; Otherwise, if there are Emacs local variables for this file,
+  ;; return nil unless vimvars-ignore-mode-line-if-local-variables-exist
+  ;; is also nil.
+  (when vimvars-enabled
+    (if file-local-variables-alist
+        (not vimvars-ignore-mode-line-if-local-variables-exist)
+      t)))
+
+
+(defun vimvars--accept-tag (leader tag)
+  "Return non-nil if LEADER followed by TAG should be accepted as a modeline."
+  (cond
+   ((equal "vim" tag) t)
+   ((equal "vi" tag) t)
+   ;; Accept "ex:" only when it is not at the beginning of a line.
+   ((equal "ex" tag) (not (equal 0 (length leader))))
+   (t nil)))
+
+
+(defun vimvars--obey-this-vim-modeline (settings-start settings-end)
+  "Obey the Vim modeline which exists between SETTINGS-START and SETTINGS-END."
+  ;; We ignore the local suffix, since for Emacs
+  ;; most settings will be buffer-local anyway.
+  (goto-char settings-start)
+  ;; Look for something like this: vi: set sw=4 ts=4:
+  ;; We should look for it in a comment, but for now
+  ;; we won't worry about the syntax of the major mode.
+  (while (re-search-forward
+          " *\\([^= ]+\\)\\(=\\([^ :]+\\)\\)?" settings-end t)
+    (let ((variable (vimvars--expand-option-name (match-string 1))))
+      (if (match-string 3)
+          (vimvars--assign variable (match-string 3))
+        (vimvars--enable-feature variable))))
+  t)
+
+
+(defun vimvars--obey-top-modeline ()
+  "Check for, and if found, obey a Vim modeline at the top of the file.
+This function moves point."
+  (goto-char (point-min))
+  (if (and
+       (re-search-forward vimvars-modeline-re
+                    (line-end-position vimvars-check-lines) t)
+       (vimvars--accept-tag (match-string 1) (match-string 2)))
+      (vimvars--obey-this-vim-modeline (match-beginning 4) (match-end 4))))
+
+
+(defun vimvars--obey-bottom-modeline ()
+  "Check for, and if found, obey a Vim modeline at the botom of the file.
+This function moves point."
+  (goto-char (point-max))
+  (if (and
+       (re-search-backward vimvars-modeline-re
+                     (line-beginning-position
+                (- 1 vimvars-check-lines)) t)
+       (vimvars--accept-tag (match-string 1) (match-string 2)))
+      (vimvars--obey-this-vim-modeline (match-beginning 4) (match-end 4))))
+
+
+(defun vimvars--set-indent (indent)
+  "Set the amount of indentation caused by tab to INDENT in a mode-aware way."
+  (when (equal major-mode 'c-mode) (setq c-basic-offset indent)))
+
+
+(defun vimvars--expand-option-name (option)
+  "Expand the abbreviated Vim :set variable OPTION to its full name."
+  (let ((expansion
+         (assoc option
+                '(("ro" "readonly")
+                  ("sts" "softtabstop")
+                  ("sw" "shiftwidth")
+                  ("ts" "tabstop")
+                  ("tw" "textwidth")))))
+    (if expansion (cadr expansion) option)))
+
+
+;;; Not supported:
+;;; comments/com (comment leader), because it's not language-specific in Vim.
+(defun vimvars--assign (var val)
+  "Emulate Vim's :set VAR=VAL."
+  (message "Setting Vim option %s to %s in %s" var val (buffer-name))
+  (cond
+   ((equal var "makeprg") (setq compile-command val))
+   ((equal var "shiftwidth") (vimvars--set-indent (string-to-number val)))
+   ((equal var "softtabstop") t) ; Ignore.
+   ((equal var "tabstop") (setq tab-width (string-to-number val)))
+   ((equal var "textwidth") (set-fill-column (string-to-number val)))
+   (t (message "Don't know how to emulate Vim variable %s" var))))
+
+
+;; These features are not supported, but in principle they could be:
+;;
+;; fileencoding, encoding could be useful but likely too hairy
+;; fileformat
+;; tags
+;; textmode (but this is obsolete in Vim, replaced by fileformat)
+;;
+;; Deliberately not supported:
+;;
+;; bomb/nobomd (byte order mark control), because I don't expect it is
+;; comonly enough used to justify the maintenance burden.
+(defun vimvars--enable-feature (var)
+  "Emulate Vim's :set VAR for variables that are just boolean."
+  (message "Enabling Vim option %s in %s" var (buffer-name))
+  (cond
+   ((equal var "expandtab") (setq indent-tabs-mode nil))
+   ((equal var "ignorecase") (setq case-fold-search t))
+   ((equal var "readonly") (toggle-read-only 1))
+   ((equal var "wrap") (setq truncate-lines nil))
+   ((equal var "write") (toggle-read-only -1)) ; See nowrite below.
+
+   ((equal var "noexpandtab") (setq indent-tabs-mode t))
+   ((equal var "noignorecase") (setq case-fold-search nil))
+   ((equal var "noreadonly") (toggle-read-only -1))
+   ((equal var "nowrap") (setq truncate-lines t))
+
+   ;; In Vim, setting 'nowrite' option creates a situation
+   ;; in which editing the file generates no warning (unlike
+   ;; 'set readonly') but by default attempts to save the
+   ;; file (even to a new file name) generate a warning.
+   ((equal var "nowrite") (toggle-read-only 1)) ; Similar, not the same
+
+   (t (message "Don't know how to emulate Vim feature %s" var))))
+
+;; If you want to manually test vimvars, one way to do this is
+;; temporarily to create an Emacs Lisp file (which we will call
+;; setup.el) in a convenient location, like this:
+;;
+;; (load-file "~/vimvars.el")
+;; (add-hook 'find-file-hook 'vimvars-obey-vim-modeline)
+;;
+;; Then, you can test it like this:
+;; emacs -q -nw -l setup.el my-test-file-name
+
+(provide 'vimvars)
+;;; vimvars.el ends here
diff --git a/test/lisp/emulation/vimvars-tests.el b/test/lisp/emulation/vimvars-tests.el
new file mode 100644
index 00000000000..bc0efdb3658
--- /dev/null
+++ b/test/lisp/emulation/vimvars-tests.el
@@ -0,0 +1,428 @@
+;;; vimvars-tests.el --- Tests for vimvars.el
+
+;; Copyright (C) 2011,2025 Free Software Foundation, Inc.
+
+;; Author: James Youngman <[email-masked]>
+;; Maintainer: James Youngman <[email-masked]>
+
+;; vimvars is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; vimvars is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with vimvars.  If not, see <http://www.gnu.org/licenses/>.
+
+(ert-deftest vimvars-test-modeline-accept-vim-vi-bol ()
+  "Check we accept 'vim:' or 'vi:' at beginning of line.
+
+   Current releases of Vim do this for compatibility with Vim version
+   3.0."
+  ;; Verified by comparison with Vim 9.0, Jul 16 2025.
+  (should (vimvars--accept-tag "" "vim"))
+  (should (vimvars--accept-tag "" "vi")))
+
+(ert-deftest vimvars-test-modeline-ignore-ex-bol ()
+  "Check we do not accept 'ex:' at beginning of line."
+  ;; Verified by comparison with Vim 9.0, Jul 16 2025.
+  (should (not (vimvars--accept-tag "" "ex"))))
+
+(ert-deftest vimvars-test-modeline-accept-vim-vi-ex-not-bol ()
+  "Check we accept 'vim:', 'vi' and 'ex:' after the beginning of line"
+  ;; Verified by comparison with Vim 9.0, Jul 16 2025.
+  (should (vimvars--accept-tag " " "vim"))
+  (should (vimvars--accept-tag " " "vi"))
+  (should (vimvars--accept-tag " " "ex"))
+  (should (vimvars--accept-tag "\t" "vim"))
+  (should (vimvars--accept-tag "\t" "vi"))
+  (should (vimvars--accept-tag "\t" "ex")))
+
+(ert-deftest vimvars-test-modeline-ignore-foo-bol ()
+  "Check we do not accept 'foo:' at beginning of line (as a modeline)."
+  ;; Verified by comparison with Vim 9.0, Jul 16 2025.
+  (should (not (vimvars--accept-tag "" "foo")))
+  (should (not (vimvars--accept-tag " " "foo"))))
+
+(ert-deftest vimvars-test-vimvars-enabled-nil ()
+  "Verify that we obey vimvars-enabled when it is nil"
+  (let ((vimvars-enabled nil)
+        (file-local-variables-alist nil)
+        (vimvars-ignore-mode-line-if-local-variables-exist nil))
+    (should (not (vimvars--should-obey-modeline)))))
+
+(ert-deftest vimvars-test-no-local-variables ()
+  "Verify that we obey vi mode lines in the absence of local variables"
+  (let ((vimvars-enabled t)
+        (file-local-variables-alist nil)
+        (vimvars-ignore-mode-line-if-local-variables-exist nil))
+    (should (vimvars--should-obey-modeline))))
+
+(ert-deftest vimvars-test-with-local-variables ()
+  "Verify that we ignore vi mode lines if there are local variables"
+  (let ((vimvars-enabled t)
+        (file-local-variables-alist '((tab-width . 8)))
+        (vimvars-ignore-mode-line-if-local-variables-exist t))
+    (should (not (vimvars--should-obey-modeline)))))
+
+
+(ert-deftest vimvars-test-with-both ()
+  "Verify that we use both vi mode lines and local variables if vimvars-ignore-mode-line-if-local-variables-exist is nil"
+  (let ((vimvars-enabled t)
+        (file-local-variables-alist '((tab-width . 8)))
+        (vimvars-ignore-mode-line-if-local-variables-exist nil))
+    (should (vimvars--should-obey-modeline))))
+
+(ert-deftest vimvars-test-expansions ()
+  "Check expansion of ro, sts, sw, ts, tw"
+  (should (equal (vimvars--expand-option-name "ro") "readonly"))
+  (should (equal (vimvars--expand-option-name "sts") "softtabstop"))
+  (should (equal (vimvars--expand-option-name "sw") "shiftwidth"))
+  (should (equal (vimvars--expand-option-name "ts") "tabstop"))
+  (should (equal (vimvars--expand-option-name "tw") "textwidth")))
+
+(ert-deftest vimvars-test-nonexpansions ()
+  "Check we don't 'expand' something that's not an abbreviation"
+  (should (equal (vimvars--expand-option-name "blehbleh") "blehbleh")))
+
+(defun kill-buffers-visiting (filename)
+  (while
+      (let (existing (get-file-buffer filename))
+           (when existing (kill-buffer existing)))))
+
+(defmacro vimvars-tests--run-checks-for-file-body (suffix content &rest checks)
+  "Write a temporary file whose name has suffix SUFFIX, having content CONTENT.
+Visit it with find-file, and evaluate CHECKS.
+
+Returns the result of the final item in CHECKS."
+  (let ((fname (make-symbol "filename")))
+    `(let ((,fname (make-temp-file "foo" nil ,suffix))
+              (old-vimvars-check-lines vimvars-check-lines)
+              (old-default-fill-column (default-value 'fill-column))
+              (old-tab-width (default-value 'tab-width))
+              (old-find-file-hooks find-file-hooks))
+       (unwind-protect
+              (progn
+                      ;; Set up a standard testing environment (choosing
+                      ;; unusual values so that it's obvious when there is a
+                      ;; problem).
+                      (setq-default fill-column 40
+                                       tab-width 14)
+                      (setq vimvars-check-lines 5)
+                      ;; Guidance for ERT (in "Tests and Their Environment")
+                      ;; is to avoid using find-file in tests, so we should
+                      ;; find a way to perform these tests without using it.
+                      (add-hook 'find-file-hook 'vimvars-obey-vim-modeline)
+                (with-temp-file ,fname (insert ,content))
+                (kill-buffers-visiting ,fname)
+                (find-file ,fname)
+                ,@checks)
+            (progn
+              ;; Kill the buffer and delete the temporary file.
+              (kill-buffer)
+              (delete-file ,fname)
+                 ;; Restore defaults etc.
+                 (setq-default fill-column old-default-fill-column
+                                     tab-width old-tab-width)
+                 (setq vimvars-check-lines old-vimvars-check-lines
+                          find-file-hooks old-find-file-hooks))))))
+
+(defmacro vimvars-tests--run-checks-for-text-file (content &rest checks)
+  "Calls vimvars-tests--run-checks-for-file-body with SUFFIX set to .txt."
+  `(vimvars-tests--run-checks-for-file-body ".txt" ,content ,@checks))
+
+(ert-deftest vimvars-test-sw-2 ()
+  "Verify 'set sw=2' works in C source files."
+  (vimvars-tests--run-checks-for-file-body
+   ".c"
+   "/* This is a C source file.\n vim: set sw=2 :\n*/\n"
+   (should (equal c-basic-offset 2))))
+
+;; Do a different check with a distinct c-basic-offset so that
+;; we can tell for sure we're actually changing it.
+(ert-deftest vimvars-test-sw-4 ()
+  "Verify 'set sw=4' works in C source files."
+  (vimvars-tests--run-checks-for-file-body
+   ".c"
+   "/* This is a C source file.\n vim: set sw=4 :\n*/\n"
+   (should (equal c-basic-offset 4))))
+
+(ert-deftest vimvars-test-disable ()
+  "Check that setting vimvars-enabled to nil turns vimvars-obey-vim-modeline off."
+  (let ((vimvars-enabled nil))
+    (vimvars-tests--run-checks-for-text-file
+     "This is a text file.\n# vim: set ts=18 :\n"
+     (should (equal tab-width 14)))))
+
+(ert-deftest vimvars-test-basic-success-case ()
+    "Check that even one item actually works."
+    (vimvars-tests--run-checks-for-text-file
+     "# vi: set ts=18 :\n"
+     (should (equal tab-width 18))))
+
+(ert-deftest vimvars-test-require-trailing-colon ()
+  "Check that we ignore modelines with no trailing colon."
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set ts=18 \n"
+   (should (equal tab-width 14)))) ; i.e. unchanged from default.
+
+(ert-deftest test-reject-ex-at-start ()
+  "Check ex: at the start of the line is rejected."
+  (vimvars-tests--run-checks-for-text-file
+   "ex: set ts=18 :\n"
+   (should (equal tab-width 14))))
+
+(ert-deftest vimvars-test-modeline-too-far-from-top ()
+  "Check we ignore mode lines more than `vimvars-check-lines' into the file."
+  (vimvars-tests--run-checks-for-text-file
+   "Mode lines at line 6 should be ignored\n\n\n\n\n# vim: set ts=6 :\n\n\n\n\n\n\n"
+   (should (equal tab-width 14)))
+
+  (vimvars-tests--run-checks-for-text-file
+   "Mode lines at line 5 should be accepted.\n\n\n\n# vim: set ts=10 :\n"
+   (should (equal tab-width 10))))
+
+(ert-deftest vimvars-test-modeline-too-far-from-bottom ()
+  "Check we only accept mode lines within `vimvars-check-lines' of EOF."
+  (vimvars-tests--run-checks-for-text-file
+   "Mode lines 5 lines from EOF should be accepted.\n\n\n\n\n\n
+# vim: set ts=10 :\n\n\n\n\n"
+   (should (equal tab-width 10)))
+
+  (vimvars-tests--run-checks-for-text-file
+   "Mode lines 6 lines from EOF should be ignored.\n\n\n\n\n\n
+# vim: set ts=10 :\n\n\n\n\n\n\n"
+   (should (equal tab-width 14))))
+
+(ert-deftest vimvars-test-makeprg ()
+  "Check makeprg=blah works."
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set makeprg=gmake :\n"
+   (should (equal compile-command "gmake"))))
+
+
+(defun vimvars-test-check-recognise-modeline (description modeline)
+  (let ((actual-modeline (concat modeline "\n")))
+    (vimvars-tests--run-checks-for-text-file
+     actual-modeline
+     (should (equal tab-width 18)))))
+
+
+(ert-deftest vimvars-test-modeline-accept-vim-set ()
+    (vimvars-test-check-recognise-modeline
+     "test-accept-vim-set"
+     "# vim: set ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-vi-set ()
+  (vimvars-test-check-recognise-modeline
+   "test-accept-vi-set"
+   "# vi: set ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-ex-set ()
+  (vimvars-test-check-recognise-modeline
+   "test-accept-ex-set"
+   "# ex: set ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-vim-se ()
+  (vimvars-test-check-recognise-modeline
+   "test-accept-vim-se"
+   "# vim: se ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-vi-se ()
+  (vimvars-test-check-recognise-modeline
+   "test-accept-vi-se"
+   "# vi: se ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-ex-se ()
+  (vimvars-test-check-recognise-modeline
+   "test-accept-ex-se"
+   "# ex: se ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-vim-setlocal ()
+  (vimvars-test-check-recognise-modeline
+   "test-accept-vim-setlocal"
+   "# vim: setlocal ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-vi-setlocal ()
+  (vimvars-test-check-recognise-modeline
+   "test-accept-vi-setlocal"
+   "# vi: setlocal ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-ex-setlocal ()
+  (vimvars-test-check-recognise-modeline
+   "test-accept-ex-setlocal"
+   "# ex: setlocal ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-no-space-before-final-colon ()
+  "Check modelines work even without a space before the final colon."
+  (vimvars-test-check-recognise-modeline
+   "test-accept-no-space-before-final-colon"
+   "# vi: set ts=18:\n"))
+
+(ert-deftest vimvars-test-modeline-accept-extra-spaces ()
+  "Check extra spaces before vi: are accepted"
+  (vimvars-test-check-recognise-modeline
+   "test-accept-extra-spaces"
+   "#   vi: set ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-tab ()
+  "Check tabs before vi: are accepted."
+  (vimvars-test-check-recognise-modeline
+   "test-accept-tab"
+   "#\tvi: set ts=18 :\n"))
+
+(ert-deftest vimvars-test-modeline-accept-vi-at-start ()
+  "Check vi: at the start of the line is accepted"
+  (vimvars-test-check-recognise-modeline
+   "test-accept-vi-at-start"
+   "vi: set ts=18 :\n"))
+
+(defmacro vimvars-tests--with-temp-default (varname temp-value &rest body)
+  "Execute BODY with the default value of VAR set to VALUE.
+
+The value of the final expression in BODY is returned."
+  (let ((oldval-var (make-symbol "previous-default")))
+    `(let ((,oldval-var ,varname))
+       (setq-default ,varname ,temp-value)
+       (unwind-protect
+              (progn ,@body)
+            (setq-default ,varname ,oldval-var)))))
+
+(ert-deftest vimvars-test-obey-set-noignorecase ()
+  (vimvars-tests--with-temp-default
+   case-fold-search t
+   (vimvars-tests--run-checks-for-text-file
+    "This is a text file.\n# vim: set noignorecase :\n"
+    (should (not case-fold-search)))))
+
+(ert-deftest vimvars-test-obey-set-ignorecase ()
+  (vimvars-tests--with-temp-default
+   case-fold-search nil
+   (vimvars-tests--run-checks-for-text-file
+    "This is a text file.\n# vim: set ignorecase :\n"
+    (should case-fold-search))))
+
+(ert-deftest vimvars-test-obey-set-wrap ()
+  (vimvars-tests--with-temp-default
+   truncate-lines t
+   (vimvars-tests--run-checks-for-text-file
+    "This is a text file.\n# vim: set wrap :\n"
+    (should (not truncate-lines)))))
+
+(ert-deftest vimvars-test-obey-set-nowrap ()
+  (vimvars-tests--with-temp-default
+   truncate-lines nil
+   (vimvars-tests--run-checks-for-text-file
+    "This is a text file.\n# vim: set nowrap :\n"
+    (should truncate-lines))))
+
+(ert-deftest vimvars-test-obey-set-textwidth-87 ()
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set tw=87 :\n"
+   (should (equal tab-width 14))
+   (should (equal fill-column 87))))
+
+(ert-deftest vimvars-test-ts-18 ()
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set ts=18 :\n"
+   (should (equal tab-width 18))
+   (should (equal fill-column 40))))
+
+(ert-deftest vimvars-test-set-readonly ()
+  ;; Verified by comparison with Vim 9.0, Jul 16 2025.
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set readonly :\n"
+   (should buffer-read-only)))
+
+(ert-deftest vimvars-test-set-noreadonly ()
+  "This test doesn't really do anything useful, since
+  not read-only is the default anyway."
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set noreadonly :\n"
+   (should (not buffer-read-only))))
+
+(ert-deftest vimvars-test-set-write ()
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set write :\n"
+   (should (not buffer-read-only))))
+
+(ert-deftest vimvars-test-set-nowrite ()
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set nowrite :\n"
+   (should buffer-read-only)))
+
+(ert-deftest vimvars-test-ts-19 ()
+  (vimvars-tests--run-checks-for-text-file
+   "This is a text file.\n# vim: set ts=19 :\n"
+   (should (equal tab-width 19))
+   (should (equal fill-column 40))))
+
+(ert-deftest vimvars-test-expandtab ()
+  (vimvars-tests--with-temp-default
+   indent-tabs-mode t
+   (vimvars-tests--run-checks-for-text-file
+    "# vim: set expandtab :\n"
+    (should (not indent-tabs-mode)))))
+
+(ert-deftest vimvars-test-noexpandtab ()
+  (vimvars-tests--with-temp-default
+   indent-tabs-mode t
+   (vimvars-tests--run-checks-for-text-file
+    "# vim: set noexpandtab :\n"
+    (should indent-tabs-mode))))
+
+(ert-deftest vimvars-test-ignore-mode-line-when-local-variables ()
+  "Check that we ignore vim modelines when `vimvars-ignore-mode-line-if-local-variables-exist'."
+  (let
+      ((old-vimvars-ignore-mode-line-if-local-variables-exist
+           vimvars-ignore-mode-line-if-local-variables-exist)
+       (filebody       "# vim: set ts=18 :
+Some random text in the middle of the file.
+
+Local Variables:
+tab-width: 11
+fill-column: 59
+End:
+"))
+    (unwind-protect
+           (progn
+             (setq vimvars-ignore-mode-line-if-local-variables-exist t)
+             (vimvars-tests--run-checks-for-text-file
+              filebody
+              ;; Because the mode line was ignored, the tab width and
+              ;; fill column should have the settings from the Emacs
+              ;; local variables.
+              (should (equal tab-width 11))
+              (should (equal fill-column 59))))
+      (progn
+           (setq vimvars-ignore-mode-line-if-local-variables-exist old-vimvars-ignore-mode-line-if-local-variables-exist)) )))
+
+(ert-deftest vimvars-test-no-ignore-mode-line-when-local-variables ()
+  "Check that we ignore vim modelines when `vimvars-ignore-mode-line-if-local-variables-exist' is nil."
+  (let
+      ((old-vimvars-ignore-mode-line-if-local-variables-exist
+           vimvars-ignore-mode-line-if-local-variables-exist)
+       (filebody       "# vim: set ts=18 :
+Some random text in the middle of the file.
+
+Local Variables:
+tab-width: 11
+fill-column: 59
+End:
+"))
+    (unwind-protect
+           (progn
+             (setq vimvars-ignore-mode-line-if-local-variables-exist nil)
+             (vimvars-tests--run-checks-for-text-file
+              filebody
+              ;; Because the mode line was ignored, the tab width and
+              ;; fill column should have the settings from the Emacs
+              ;; local variables.
+              (should (equal tab-width 18))
+              (should (equal fill-column 59))))
+      (progn
+           (setq vimvars-ignore-mode-line-if-local-variables-exist old-vimvars-ignore-mode-line-if-local-variables-exist)) )))
-- 
2.39.5