Subject: [PATCH] Add lua-ts-mode

From: john muhl <[email-masked]>
Date: Mon, 11 Sep 2023 10:03:13 -0500

* lisp/progmodes/lua-ts-mode.el:
* test/lisp/progmodes/lua-ts-mode-resources/indent.erts:
* test/lisp/progmodes/lua-ts-mode-tests.el: New file.
* etc/NEWS: Mention new mode.
* lisp/progmodes/eglot.el (eglot-server-programs):
* lisp/progmodes/hideshow.el (hs-special-modes-alist): Support
lua-ts-mode.
* admin/notes/tree-sitter/build-module/batch.sh:
* admin/notes/tree-sitter/build-module/build.sh: Add Lua.
* test/infra/Dockerfile.emba:
* test/infra/test-jobs.yml: Include lua-ts-mode tests.
---
 admin/notes/tree-sitter/build-module/batch.sh |   1 +
 admin/notes/tree-sitter/build-module/build.sh |   3 +
 etc/NEWS                                      |   4 +
 lisp/progmodes/eglot.el                       |   4 +-
 lisp/progmodes/hideshow.el                    |   1 +
 lisp/progmodes/lua-ts-mode.el                 | 431 ++++++++++++++
 test/infra/Dockerfile.emba                    |   1 +
 test/infra/test-jobs.yml                      |   1 +
 .../lua-ts-mode-resources/indent.erts         | 152 +++++
 .../lua-ts-mode-resources/movement.erts       | 553 ++++++++++++++++++
 test/lisp/progmodes/lua-ts-mode-tests.el      |  36 ++
 11 files changed, 1185 insertions(+), 2 deletions(-)
 create mode 100644 lisp/progmodes/lua-ts-mode.el
 create mode 100644 test/lisp/progmodes/lua-ts-mode-resources/indent.erts
 create mode 100644 test/lisp/progmodes/lua-ts-mode-resources/movement.erts
 create mode 100644 test/lisp/progmodes/lua-ts-mode-tests.el

diff --git a/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh
index 3c4e1472c89..9988d1eae4e 100755
--- a/admin/notes/tree-sitter/build-module/batch.sh
+++ b/admin/notes/tree-sitter/build-module/batch.sh
@@ -16,6 +16,7 @@ languages=
     'java'
     'javascript'
     'json'
+    'lua'
     'python'
     'rust'
     'toml'
diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh
index 0832875168b..969187b7f92 100755
--- a/admin/notes/tree-sitter/build-module/build.sh
+++ b/admin/notes/tree-sitter/build-module/build.sh
@@ -42,6 +42,9 @@ grammardir=
     "heex")
         org="phoenixframework"
         ;;
+    "lua")
+        org="MunifTanjim"
+        ;;
     "typescript")
         sourcedir="tree-sitter-typescript/typescript/src"
         grammardir="tree-sitter-typescript/typescript"
diff --git a/etc/NEWS b/etc/NEWS
index 51e89fc96dd..0b7da8b392c 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -782,6 +782,10 @@ It highlight parens via ‘show-paren-mode’ and ‘blink-matching-paren’ in
 a user-friendly way, avoids reporting alleged paren mismatches and makes
 sexp navigation more intuitive.
 
+---
+*** New major mode 'lua-ts-mode'.
+A major mode based on the tree-sitter library for editing Lua files.
+
 ---
 ** The highly accessible Modus themes collection has eight items.
 The 'modus-operandi' and 'modus-vivendi' are the main themes that have
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index f7c7c29c094..e2f663998d3 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -268,8 +268,8 @@ eglot-server-programs
                                 (gdscript-mode . ("localhost" 6008))
                                 ((fortran-mode f90-mode) . ("fortls"))
                                 (futhark-mode . ("futhark" "lsp"))
-                                (lua-mode . ,(eglot-alternatives
-                                              '("lua-language-server" "lua-lsp")))
+                                ((lua-mode lua-ts-mode) . ,(eglot-alternatives
+                                                            '("lua-language-server" "lua-lsp")))
                                 (zig-mode . ("zls"))
                                 ((css-mode css-ts-mode)
                                  . ,(eglot-alternatives '(("vscode-css-language-server" "--stdio")
diff --git a/lisp/progmodes/hideshow.el b/lisp/progmodes/hideshow.el
index b878986d7a4..78e39fad740 100644
--- a/lisp/progmodes/hideshow.el
+++ b/lisp/progmodes/hideshow.el
@@ -264,6 +264,7 @@ hs-special-modes-alist
     (java-ts-mode "{" "}" "/[*/]" nil nil)
     (js-mode "{" "}" "/[*/]" nil)
     (js-ts-mode "{" "}" "/[*/]" nil)
+    (lua-ts-mode "{\\|\\[\\[" "}\\|\\]\\]" "--" nil)
     (mhtml-mode "{\\|<[^/>]*?" "}\\|</[^/>]*[^/]>" "<!--" mhtml-forward nil)
     ;; Add more support here.
     ))
diff --git a/lisp/progmodes/lua-ts-mode.el b/lisp/progmodes/lua-ts-mode.el
new file mode 100644
index 00000000000..071953d3d2f
--- /dev/null
+++ b/lisp/progmodes/lua-ts-mode.el
@@ -0,0 +1,431 @@
+;;; lua-ts-mode.el --- Major mode for editing Lua files -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; Author: John Muhl <[email-masked]>
+;; Created: June 27, 2023
+;; Keywords: lua languages tree-sitter
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+;;
+;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package provides `lua-ts-mode' which is a major mode for Lua
+;; files that uses Tree Sitter to parse the language.
+;;
+;; This package is compatible with and tested against the grammar
+;; for Lua found at https://github.com/MunifTanjim/tree-sitter-lua
+
+;;; Code:
+
+(require 'comint)
+(require 'treesit)
+
+(eval-when-compile
+  (require 'cl-lib)
+  (require 'rx))
+
+(declare-function treesit-node-child-by-field-name "treesit.c")
+(declare-function treesit-node-type "treesit.c")
+(declare-function treesit-parser-create "treesit.c")
+(declare-function treesit-search-subtree "treesit.c")
+
+(defgroup lua-ts nil
+  "Major mode for editing Lua files."
+  :prefix "lua-ts-"
+  :group 'languages)
+
+(defcustom lua-ts-indent-offset 4
+  "Number of spaces for each indentation step in `lua-ts-mode'."
+  :type 'natnum
+  :safe 'natnump
+  :group 'lua-ts
+  :version "30.1")
+
+(defcustom lua-ts-luacheck-program "luacheck"
+  "Location of the Luacheck program."
+  :type '(choice (const nil) string)
+  :group 'lua-ts
+  :version "30.1")
+
+(defcustom lua-ts-inferior-buffer "*Lua*"
+  "Name of the inferior Lua buffer."
+  :type 'string
+  :safe 'stringp
+  :group 'lua-ts
+  :version "30.1")
+
+(defcustom lua-ts-inferior-program "lua"
+  "Program to run in the inferior Lua process."
+  :type '(choice (const nil) string)
+  :group 'lua-ts
+  :version "30.1")
+
+(defcustom lua-ts-inferior-options '("-i")
+  "Command line options for the inferior Lua process."
+  :type '(repeat string)
+  :group 'lua-ts
+  :version "30.1")
+
+(defcustom lua-ts-inferior-startfile nil
+  "File to load into the inferior Lua process at startup."
+  :type '(choice (const nil) (file :must-match t))
+  :group 'lua-ts
+  :version "30.1")
+
+(defcustom lua-ts-inferior-prompt-regexp "^>>?[[:blank:]]"
+  "Regular expression matching the prompt of the inferior Lua process."
+  :type 'regexp
+  :group 'lua-ts
+  :version "30.1")
+
+(defvar lua-ts--builtins
+  '("assert" "bit32" "collectgarbage" "coroutine" "debug" "dofile"
+    "error" "getmetatable" "io" "ipairs" "load" "loadfile"
+    "math" "next" "os" "package" "pairs" "pcall" "print"
+    "rawequal" "rawget" "rawlen" "rawset" "require" "select"
+    "setmetatable" "string" "table" "tonumber" "tostring"
+    "type" "utf8" "warn" "xpcall" "_G" "_VERSION"
+    ;; methods for file handlers
+    "close" "flush" "lines" "read" "seek" "setvbuf" "write")
+  "Lua built-in functions for tree-sitter font-locking.")
+
+(defvar lua-ts--font-lock-settings
+  (treesit-font-lock-rules
+   :language 'lua
+   :feature 'bracket
+   '(["(" ")" "[" "]" "{" "}"] @font-lock-bracket-face)
+
+   :language 'lua
+   :feature 'delimiter
+   '(["," ";"] @font-lock-delimiter-face)
+
+   :language 'lua
+   :feature 'escape
+   '((escape_sequence) @font-lock-escape-face)
+
+   :language 'lua
+   :feature 'constant
+   '((variable_list
+      attribute: (attribute (["<" ">"] (identifier))))
+     @font-lock-constant-face)
+
+   :language 'lua
+   :feature 'operator
+   '(["and" "not" "or" "+" "-" "*" "/" "%" "^"
+      "#" "==" "~=" "<=" ">=" "<" ">" "=" "&"
+      "~" "|" "<<" ">>" "//" ".."]
+     @font-lock-operator-face
+     (vararg_expression) @font-lock-operator-face)
+
+   :language 'lua
+   :feature 'property
+   '((field name: (identifier) @font-lock-property-name-face)
+     (dot_index_expression
+      field: (identifier) @font-lock-property-use-face))
+
+   :language 'lua
+   :feature 'builtin
+   `(((identifier) @font-lock-builtin-face
+      (:match ,(regexp-opt lua-ts--builtins 'symbols)
+              @font-lock-builtin-face)))
+
+   :language 'lua
+   :feature 'function
+   '((function_call name: (identifier) @font-lock-function-call-face)
+     (function_call
+      name: (method_index_expression
+             method: (identifier) @font-lock-function-call-face))
+     (function_call
+      name: (dot_index_expression
+             table: (identifier) @font-lock-function-call-face)))
+
+   :language 'lua
+   :feature 'punctuation
+   '(["." ":"] @font-lock-punctuation-face)
+
+   :language 'lua
+   :feature 'variable
+   '((function_call
+      arguments: (arguments (identifier))
+      @font-lock-variable-use-face)
+     (function_call
+      name: (method_index_expression
+             table: (identifier) @font-lock-variable-use-face))
+     (goto_statement (identifier) @font-lock-variable-use-face))
+
+   :language 'lua
+   :feature 'assignment
+   '((variable_list (identifier) @font-lock-variable-name-face))
+
+   :language 'lua
+   :feature 'number
+   '((number) @font-lock-number-face)
+
+   :language 'lua
+   :feature 'keyword
+   '((break_statement) @font-lock-keyword-face
+     (true) @font-lock-constant-face
+     (false) @font-lock-constant-face
+     (nil) @font-lock-constant-face
+     ["and" "do" "else" "elseif" "end" "for" "function"
+      "goto" "if" "in" "local" "not" "or" "repeat"
+      "return" "then" "until" "while"]
+     @font-lock-keyword-face)
+
+   :language 'lua
+   :feature 'string
+   '((string) @font-lock-string-face)
+
+   :language 'lua
+   :feature 'comment
+   '((comment) @font-lock-comment-face
+     (hash_bang_line) @font-lock-comment-face)
+
+   :language 'lua
+   :feature 'definition
+   '((function_declaration
+      name: (identifier) @font-lock-function-name-face)
+     (parameters
+      name: (identifier) @font-lock-variable-name-face)
+     (label_statement) @font-lock-variable-name-face)
+
+   :language 'lua
+   :feature 'error
+   :override t
+   '((ERROR) @font-lock-warning-face))
+  "Tree-sitter font-lock settings for `lua-ts-mode'.")
+
+(defvar lua-ts--simple-indent-rules
+  `((lua
+     ((parent-is "chunk") column-0 0)
+     ((node-is "comment_end") column-0 0)
+     ((parent-is "block") parent-bol 0)
+     ((node-is "}") parent-bol 0)
+     ((node-is ")") parent-bol 0)
+     ((node-is "else_statement") parent-bol 0)
+     ((node-is "elseif_statement") parent-bol 0)
+     ((node-is "end") parent-bol 0)
+     ((node-is "until") parent-bol 0)
+     ((parent-is "for_statement") parent-bol lua-ts-indent-offset)
+     ((parent-is "function_declaration") parent-bol lua-ts-indent-offset)
+     ((parent-is "function_definition") parent-bol lua-ts-indent-offset)
+     ((parent-is "if_statement") parent-bol lua-ts-indent-offset)
+     ((parent-is "else_statement") parent-bol lua-ts-indent-offset)
+     ((parent-is "repeat_statement") parent-bol lua-ts-indent-offset)
+     ((parent-is "while_statement") parent-bol lua-ts-indent-offset)
+     ((parent-is "table_constructor") parent-bol lua-ts-indent-offset)
+     ((parent-is "arguments") parent-bol lua-ts-indent-offset)
+     ((parent-is "parameters") parent-bol lua-ts-indent-offset)
+     ((parent-is "ERROR") no-indent 0))))
+
+(defvar lua-ts--syntax-table
+  (let ((table (make-syntax-table)))
+    (modify-syntax-entry ?+  "."    table)
+    (modify-syntax-entry ?-  ". 12" table)
+    (modify-syntax-entry ?=  "."    table)
+    (modify-syntax-entry ?%  "."    table)
+    (modify-syntax-entry ?^  "."    table)
+    (modify-syntax-entry ?~  "."    table)
+    (modify-syntax-entry ?<  "."    table)
+    (modify-syntax-entry ?>  "."    table)
+    (modify-syntax-entry ?/  "."    table)
+    (modify-syntax-entry ?*  "."    table)
+    (modify-syntax-entry ?\n ">"    table)
+    (modify-syntax-entry ?\' "\""   table)
+    (modify-syntax-entry ?\" "\""   table)
+    table)
+  "Syntax table for `lua-ts-mode'.")
+
+(defun lua-ts--defun-name-function (node)
+  "Return the defun name of NODE.
+Return nil if there is no name or if NODE is not a defun node."
+  (let ((child (treesit-node-child-by-field-name node "name")))
+    (pcase (treesit-node-type node)
+      ((or "function_declaration" "function_definition")
+       (treesit-node-text child t))
+      ("variable_declaration"
+       (if child
+           (treesit-node-text child t)
+         (treesit-node-text
+          (treesit-node-child-by-field-name
+           (treesit-search-subtree node "assignment_statement" nil nil 1)
+           "name"))))
+      ("field"
+       (and (treesit-search-subtree node "function_definition" nil nil 1)
+            (treesit-node-text child t))))))
+
+(defvar-local lua-ts--flymake-process nil)
+
+(defun lua-ts-flymake-luacheck (report-fn &rest _args)
+  "Luacheck backend for Flymake.
+Calls REPORT-FN directly."
+  (when (process-live-p lua-ts--flymake-process)
+    (kill-process lua-ts--flymake-process))
+  (let ((source (current-buffer)))
+    (save-restriction
+      (widen)
+      (setq lua-ts--flymake-process
+            (make-process
+             :name "lua-ts-flymake-luacheck"
+             :noquery t
+             :connection-type 'pipe
+             :buffer (generate-new-buffer " *lua-ts-flymake-luacheck*")
+             :command `(,lua-ts-luacheck-program
+                        "--codes" "--ranges" "--formatter" "plain" "-")
+             :sentinel
+             (lambda (proc _event)
+               (when (eq 'exit (process-status proc))
+                 (unwind-protect
+                     (if (with-current-buffer source
+                           (eq proc lua-ts--flymake-process))
+                         (with-current-buffer (process-buffer proc)
+                           (goto-char (point-min))
+                           (cl-loop
+                            while (search-forward-regexp
+                                   (rx (seq bol
+                                            (0+ alnum) ":"
+                                            (group (1+ digit)) ":"
+                                            (group (1+ digit)) "-"
+                                            (group (1+ digit)) ": "
+                                            (group (0+ nonl))
+                                            eol))
+                                   nil t)
+                            for line = (string-to-number (match-string 1))
+                            for beg = (string-to-number (match-string 2))
+                            for end = (string-to-number (match-string 3))
+                            for msg = (match-string 4)
+                            for type = (if (string-match "^(W" msg)
+                                           :warning
+                                         :error)
+                            when (and beg end)
+                            collect (flymake-make-diagnostic source
+                                                             (cons line beg)
+                                                             (cons line (1+ end))
+                                                             type
+                                                             msg)
+                            into diags
+                            finally (funcall report-fn diags)))
+                       (flymake-log :warning "Canceling obsolete check %s" proc))
+                   (kill-buffer (process-buffer proc)))))))
+      (process-send-region lua-ts--flymake-process (point-min) (point-max))
+      (process-send-eof lua-ts--flymake-process))))
+
+;;;###autoload
+(defun lua-ts-inferior-lua ()
+  "Run a Lua interpreter in an inferior process."
+  (interactive)
+  (let* ((buffer lua-ts-inferior-buffer)
+         (name (string-replace "*" "" buffer))
+         (program lua-ts-inferior-program)
+         (prompt-regexp lua-ts-inferior-prompt-regexp)
+         (switches lua-ts-inferior-options)
+         (startfile lua-ts-inferior-startfile))
+    (unless (comint-check-proc buffer)
+      (set-buffer (apply (function make-comint) name program startfile switches))
+      (setq-local comint-input-ignoredups t
+                  comint-prompt-read-only t
+                  comint-prompt-regexp prompt-regexp
+                  comint-use-prompt-regexp t))
+    (select-window (display-buffer buffer '((display-buffer-reuse-window
+                                             display-buffer-pop-up-frame)
+                                            (reusable-frames . t))))))
+
+;;;###autoload
+(define-derived-mode lua-ts-mode prog-mode "Lua"
+  "Major mode for editing Lua files, powered by tree-sitter."
+  :syntax-table lua-ts--syntax-table
+
+  (when (treesit-ready-p 'lua)
+    (treesit-parser-create 'lua)
+
+    ;; Comments.
+    (setq-local comment-start "--")
+    (setq-local comment-start-skip "--\\s-*")
+    (setq-local comment-end "")
+
+    ;; Font-lock.
+    (setq-local treesit-font-lock-settings lua-ts--font-lock-settings)
+    (setq-local treesit-font-lock-feature-list
+                '((comment definition)
+                  (keyword property string)
+                  (assignment builtin constant number)
+                  (bracket
+                   delimiter
+                   escape
+                   function
+                   operator
+                   punctuation
+                   variable)))
+
+    ;; Indent.
+    (setq-local treesit-simple-indent-rules lua-ts--simple-indent-rules)
+
+    ;; Navigation.
+    (setq-local treesit-defun-name-function #'lua-ts--defun-name-function)
+    (setq-local treesit-defun-type-regexp
+                (rx (or "function_declaration" "function_definition")))
+    (setq-local treesit-thing-settings
+                `((lua
+                   (sentence ,(rx (or "do_statement"
+                                      "field"
+                                      "for_statement"
+                                      "function_call"
+                                      "if_statement"
+                                      "repeat_statement"
+                                      "return_statement"
+                                      "variable_declaration"
+                                      "while_statement")))
+                   (sexp ,(rx (or "arguments"
+                                  "block"
+                                  "parameters"
+                                  "string"
+                                  "table_constructor")))
+                   (text "comment"))))
+
+    ;; Imenu.
+    (setq-local treesit-simple-imenu-settings
+                `(("Variable" ,(rx bos "variable_declaration" eos) nil nil)
+                  ("Function" ,(rx bos
+                                   (or "function_declaration"
+                                       "function_definition"
+                                       "field")
+                                   eos)
+                   nil nil)))
+
+    ;; Which-function.
+    (setq-local which-func-functions (treesit-defun-at-point))
+
+    ;; Outline.
+    (setq-local outline-regexp
+                (rx (seq (0+ space)
+                         (or (seq "--[[" (0+ space) eol)
+                             (seq symbol-start
+                                  (or "do" "for" "if" "repeat" "while"
+                                      (seq (? (seq "local" (1+ space)))
+                                           "function"))
+                                  symbol-end)))))
+
+    (treesit-major-mode-setup))
+
+  (add-hook 'flymake-diagnostic-functions #'lua-ts-flymake-luacheck nil 'local))
+
+(when (treesit-ready-p 'lua)
+  (add-to-list 'auto-mode-alist '("\\.lua\\'" . lua-ts-mode)))
+
+(provide 'lua-ts-mode)
+
+;;; lua-ts-mode.el ends here
diff --git a/test/infra/Dockerfile.emba b/test/infra/Dockerfile.emba
index 584e4444dc1..e29098ec270 100644
--- a/test/infra/Dockerfile.emba
+++ b/test/infra/Dockerfile.emba
@@ -126,6 +126,7 @@ RUN src/emacs -Q --batch \
       (java "https://github.com/tree-sitter/tree-sitter-java") \
       (javascript "https://github.com/tree-sitter/tree-sitter-javascript") \
       (json "https://github.com/tree-sitter/tree-sitter-json") \
+      (lua "https://github.com/MunifTanjim/tree-sitter-lua") \
       (python "https://github.com/tree-sitter/tree-sitter-python") \
       (ruby "https://github.com/tree-sitter/tree-sitter-ruby") \
       (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src") \
diff --git a/test/infra/test-jobs.yml b/test/infra/test-jobs.yml
index 2f6e0dab4d5..1f5d607eda4 100644
--- a/test/infra/test-jobs.yml
+++ b/test/infra/test-jobs.yml
@@ -580,6 +580,7 @@ test-src-inotify:
       lisp/progmodes/go-ts-mode-tests.log
       lisp/progmodes/heex-ts-mode-tests.log
       lisp/progmodes/java-ts-mode-tests.log
+      lisp/progmodes/lua-ts-mode-tests.log
       lisp/progmodes/ruby-ts-mode-tests.log
       lisp/progmodes/typescript-ts-mode-tests.log
       src/treesit-tests.log
diff --git a/test/lisp/progmodes/lua-ts-mode-resources/indent.erts b/test/lisp/progmodes/lua-ts-mode-resources/indent.erts
new file mode 100644
index 00000000000..040225c8580
--- /dev/null
+++ b/test/lisp/progmodes/lua-ts-mode-resources/indent.erts
@@ -0,0 +1,152 @@
+Code:
+  (lambda ()
+    (setq indent-tabs-mode nil)
+    (setq lua-ts-indent-offset 2)
+    (lua-ts-mode)
+    (indent-region (point-min) (point-max)))
+
+Name: Basic Indent
+
+=-=
+      print(
+0,
+      1
+)
+
+local function f(o)
+   if o.x > o.y then
+ return o.x
+elseif o.y > o.z then
+           return o.y
+      else
+return o.z
+            end
+end
+
+f({
+ x = 1,
+  y = 2,
+   z = 3,
+})
+
+;(function()
+return false
+)()
+=-=
+print(
+  0,
+  1
+)
+
+local function f(o)
+  if o.x > o.y then
+    return o.x
+  elseif o.y > o.z then
+    return o.y
+  else
+    return o.z
+  end
+end
+
+f({
+  x = 1,
+  y = 2,
+  z = 3,
+})
+
+;(function()
+  return false
+)()
+=-=-=
+
+Name: Argument Indent
+
+=-=
+function h(
+string,
+number,
+options)
+print(string, number, options)
+end
+
+local p = h(
+"sring",
+	 1000,
+      {
+cost = 2,
+length = 8,
+	parallelism = 4,
+})
+=-=
+function h(
+  string,
+  number,
+  options)
+  print(string, number, options)
+end
+
+local p = h(
+  "sring",
+  1000,
+  {
+    cost = 2,
+    length = 8,
+    parallelism = 4,
+  })
+=-=-=
+
+Name: Continuation Indent
+
+=-=
+function f()
+  local str = [[
+  multi-line
+	string
+    ]]
+--[[
+multi-line
+comment
+    ]]
+return true
+end
+=-=
+function f()
+  local str = [[
+  multi-line
+	string
+    ]]
+  --[[
+multi-line
+comment
+    ]]
+  return true
+end
+=-=-=
+
+Name: Loop Indent
+
+=-=
+for k, v in pairs({}) do
+	 print(k, v)
+end
+
+while n < 10 do
+n = n + 1
+end
+
+repeat
+z = z * 2
+ until z > 12
+=-=
+for k, v in pairs({}) do
+  print(k, v)
+end
+
+while n < 10 do
+  n = n + 1
+end
+
+repeat
+  z = z * 2
+until z > 12
+=-=-=
diff --git a/test/lisp/progmodes/lua-ts-mode-resources/movement.erts b/test/lisp/progmodes/lua-ts-mode-resources/movement.erts
new file mode 100644
index 00000000000..770aa23b18d
--- /dev/null
+++ b/test/lisp/progmodes/lua-ts-mode-resources/movement.erts
@@ -0,0 +1,553 @@
+Code:
+  (lambda ()
+    (lua-ts-mode)
+    (beginning-of-defun 1))
+
+Point-Char: |
+
+Name: beginning-of-defun moves to start of function declaration
+
+=-=
+local function Test()
+  if true then
+    print(1)
+  else
+    print(0)
+  end|
+end
+=-=
+|local function Test()
+  if true then
+    print(1)
+  else
+    print(0)
+  end
+end
+=-=-=
+
+Name: beginning-of-defun moves to start of function definition
+
+=-=
+local t = {
+  f = function()
+    return true
+  end,
+}|
+=-=
+local t = {
+|  f = function()
+    return true
+  end,
+}
+=-=-=
+
+Code:
+  (lambda ()
+    (lua-ts-mode)
+    (end-of-defun 1))
+
+Point-Char: |
+
+Name: end-of-defun moves to end of function declaration
+
+=-=
+local function Test()
+  if true then
+    pr|int(1)
+  else
+    print(0)
+  end
+end
+
+local t = Test()
+=-=
+local function Test()
+  if true then
+    print(1)
+  else
+    print(0)
+  end
+end
+|
+local t = Test()
+=-=-=
+
+Name: end-of-defun moves to end of function definition
+
+=-=
+local t = {
+  f = function()
+    re|turn true
+  end,
+}
+=-=
+local t = {
+  f = function()
+    return true
+  end|,
+}
+=-=-=
+
+Code:
+  (lambda ()
+    (lua-ts-mode)
+    (forward-sentence 1))
+
+Point-Char: |
+
+Name: forward-sentence moves over if statements
+
+=-=
+function f()
+  |if true then
+    print(1)
+  elseif false then
+    print(0)
+  else
+    print(2)
+  end
+end
+=-=
+function f()
+  if true then
+    print(1)
+  elseif false then
+    print(0)
+  else
+    print(2)
+  end|
+end
+=-=-=
+
+Name: forward-sentence moves over variable declaration
+
+=-=
+|local n = 1
+
+print(n)
+=-=
+local n = 1|
+
+print(n)
+=-=-=
+
+Name: forward-sentence moves over for statements
+
+=-=
+|for k, v in pairs({}) do
+  print(k, v)
+end
+
+print(1)
+=-=
+for k, v in pairs({}) do
+  print(k, v)
+end|
+
+print(1)
+=-=-=
+
+Name: forward-sentence moves over for statements
+
+=-=
+|do
+  local x = 1
+  local y = 2
+
+  print(x, y)
+end
+
+print(1)
+=-=
+do
+  local x = 1
+  local y = 2
+
+  print(x, y)
+end|
+
+print(1)
+=-=-=
+
+Name: forward-sentence moves over while statements
+
+=-=
+local i = 0
+|while i < 9 do
+      print(i)
+      i = i + 1
+end
+
+print(1)
+=-=
+local i = 0
+while i < 9 do
+      print(i)
+      i = i + 1
+end|
+
+print(1)
+=-=-=
+
+Name: forward-sentence moves over repeat statements
+
+=-=
+local i = 0
+|repeat
+  print(i)
+  i = i + 1
+until i > 9
+
+print(1)
+=-=
+local i = 0
+repeat
+  print(i)
+  i = i + 1
+until i > 9|
+
+print(1)
+=-=-=
+
+Name: forward-sentence moves over function calls
+
+=-=
+|print(1)
+=-=
+print(1)|
+=-=-=
+
+Name: forward-sentence moves over return statements
+
+=-=
+function f()
+    |return math.random()
+end
+=-=
+function f()
+    return math.random()|
+end
+=-=-=
+
+Code:
+  (lambda ()
+    (lua-ts-mode)
+    (forward-sentence 2))
+
+Name: forward-sentence moves over table fields
+
+=-=
+local t = {
+  |a = 1,
+  b = 2,
+}
+=-=
+local t = {
+  a = 1,
+  b = 2|,
+}
+=-=-=
+
+Code:
+  (lambda ()
+    (lua-ts-mode)
+    (backward-sentence 1))
+
+Point-Char: |
+
+Name: backward-sentence moves over if statements
+
+=-=
+function f()
+  if true then
+    print(1)
+  elseif false then
+    print(0)
+  else
+    print(2)
+  end|
+end
+=-=
+function f()
+  |if true then
+    print(1)
+  elseif false then
+    print(0)
+  else
+    print(2)
+  end
+end
+=-=-=
+
+Name: backward-sentence moves over variable declaration
+
+=-=
+local n = 1|
+
+print(n)
+=-=
+|local n = 1
+
+print(n)
+=-=-=
+
+Name: backward-sentence moves over for statements
+
+=-=
+for k, v in pairs({}) do
+  print(k, v)
+end|
+
+print(1)
+=-=
+|for k, v in pairs({}) do
+  print(k, v)
+end
+
+print(1)
+=-=-=
+
+Name: backward-sentence moves over for statements
+
+=-=
+do
+  local x = 1
+  local y = 2
+
+  print(x, y)
+end|
+
+print(1)
+=-=
+|do
+  local x = 1
+  local y = 2
+
+  print(x, y)
+end
+
+print(1)
+=-=-=
+
+Name: backward-sentence moves over while statements
+
+=-=
+local i = 0
+while i < 9 do
+      print(i)
+      i = i + 1
+end|
+
+print(1)
+=-=
+local i = 0
+|while i < 9 do
+      print(i)
+      i = i + 1
+end
+
+print(1)
+=-=-=
+
+Name: backward-sentence moves over repeat statements
+
+=-=
+local i = 0
+repeat
+  print(i)
+  i = i + 1
+until i > 9|
+
+print(1)
+=-=
+local i = 0
+|repeat
+  print(i)
+  i = i + 1
+until i > 9
+
+print(1)
+=-=-=
+
+Name: backward-sentence moves over function calls
+
+=-=
+print(1)|
+=-=
+|print(1)
+=-=-=
+
+Name: backward-sentence moves over return statements
+
+=-=
+function f()
+    return math.random()|
+end
+=-=
+function f()
+    |return math.random()
+end
+=-=-=
+
+Code:
+  (lambda ()
+    (lua-ts-mode)
+    (backward-sentence 2))
+
+Point-Char: |
+
+Name: backward-sentence moves over table fields
+
+=-=
+local t = {
+  a = 1,
+  b = 2|,
+}
+=-=
+local t = {
+  |a = 1,
+  b = 2,
+}
+=-=-=
+
+Code:
+  (lambda ()
+    (lua-ts-mode)
+    (forward-sexp 1))
+
+Point-Char: |
+
+Name: forward-sexp moves over blocks
+
+=-=
+local function Test()
+  |local t = {
+    a = 1,
+  }
+
+  if true then
+    print(1)
+  else
+    print(0)
+  end
+end
+=-=
+local function Test()
+  local t = {
+    a = 1,
+  }
+
+  if true then
+    print(1)
+  else
+    print(0)
+  end|
+end
+=-=-=
+
+Name: forward-sexp moves over arguments
+
+=-=
+print|(1, 2, 3)
+=-=
+print(1, 2, 3)|
+=-=-=
+
+Name: forward-sexp moves over parameters
+
+=-=
+function f|(a, b) end
+=-=
+function f(a, b)| end
+=-=-=
+
+Name: forward-sexp moves over strings
+
+=-=
+print("|1, 2, 3")
+=-=
+print("1, 2, 3|")
+=-=-=
+
+Name: forward-sexp moves over tables
+
+=-=
+local t = |{ 1,
+  2,
+  3 }
+=-=
+local t = { 1,
+  2,
+  3 }|
+=-=-=
+
+Code:
+  (lambda ()
+    (lua-ts-mode)
+    (backward-sexp 1))
+
+Point-Char: |
+
+Name: backward-sexp moves over blocks
+
+=-=
+local function Test()
+  local t = {
+    a = 1,
+  }
+
+  if true then
+    print(1)
+  else
+    print(0)
+  end|
+end
+=-=
+local function Test()
+  |local t = {
+    a = 1,
+  }
+
+  if true then
+    print(1)
+  else
+    print(0)
+  end
+end
+=-=-=
+
+Name: backward-sexp moves over arguments
+
+=-=
+print(1, 2, 3)|
+=-=
+print|(1, 2, 3)
+=-=-=
+
+Name: backward-sexp moves over parameters
+
+=-=
+function f(a, b)| end
+=-=
+function f|(a, b) end
+=-=-=
+
+Name: backward-sexp moves over strings
+
+=-=
+print("1, 2, 3|")
+=-=
+print("|1, 2, 3")
+=-=-=
+
+Name: backward-sexp moves over tables
+
+=-=
+local t = { 1,
+  2,
+  3 }|
+=-=
+local t = |{ 1,
+  2,
+  3 }
+=-=-=
diff --git a/test/lisp/progmodes/lua-ts-mode-tests.el b/test/lisp/progmodes/lua-ts-mode-tests.el
new file mode 100644
index 00000000000..d2105b66f6d
--- /dev/null
+++ b/test/lisp/progmodes/lua-ts-mode-tests.el
@@ -0,0 +1,36 @@
+;;; lua-ts-mode-tests.el --- Tests for lua-ts-mode -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'ert-x)
+(require 'treesit)
+
+(ert-deftest lua-ts-mode-test-indentation ()
+  (skip-unless (treesit-ready-p 'lua))
+  (ert-test-erts-file (ert-resource-file "indent.erts")))
+
+(ert-deftest lua-ts-mode-test-movement ()
+  (skip-unless (treesit-ready-p 'lua))
+  (ert-test-erts-file (ert-resource-file "movement.erts")))
+
+(provide 'lua-ts-mode-tests)
+
+;;; lua-ts-mode-tests.el ends here
-- 
2.41.0