diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..3cf1000
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,25 @@
+# Documentation: .
+
+# Stop searching for .editorconfig files after reading this one.
+root = true
+
+
+# Match all file names, unless more specific match later.
+[*]
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+
+[{*.mk,Makefile,rules}]
+indent_style = tab
+
+
+[{*.md,*.yaml,.yml}]
+indent_size = 2
+
+
+# Local variables:
+# coding: utf-8
+# End:
+# vim: fileencoding=utf-8 filetype=dosini :
diff --git a/gdscript-customization.el b/gdscript-customization.el
index 76431ef..5457ba3 100644
--- a/gdscript-customization.el
+++ b/gdscript-customization.el
@@ -1,156 +1,156 @@
-;;; gdscript-customization.el --- Customizable variables for the GDScript language support -*- lexical-binding: t -*-
-
-;; Copyright (C) 2020-2026 GDQuest and contributors
-
-;; Author: Nathan Lovato
-;; Maintainer: Jen-Chieh Shen
-
-;; This file is not part of GNU Emacs
-
-;; This file 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, or (at your option)
-;; any later version.
-
-;; This program 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.
-
-;; For a full copy of the GNU General Public License
-;; see .
-
-;;; Commentary:
-;;
-;; Contains all the defcustom for gdscript-mode
-;;
-
-;;; Code:
-
-(defgroup gdscript nil
- "GDScript language support for Emacs."
- :group 'languages
- :version "26"
- :link '(emacs-commentary-link "gdscript"))
-
-(defcustom gdscript-use-type-hints t
- "If t, inserted snippets contain type hints."
- :type 'boolean
- :group 'gdscript)
-
-;; gdscript-indent
-(defcustom gdscript-use-tab-indents t "Use tabs (t) or spaces (nil)."
- :type 'boolean
- :group 'gdscript)
-
-(defcustom gdscript-tab-width 4 "Indentation width."
- :type 'integer
- :group 'gdscript)
-
-(defcustom gdscript-indent-offset 4 "Default indentation offset for Gdscript."
- :type 'integer
- :safe 'integerp
- :group 'gdscript)
-
-(defcustom gdscript-indent-trigger-commands '(indent-for-tab-command yas-expand yas/expand)
- "Commands that might trigger a `gdscript-indent-line' call."
- :type '(repeat symbol)
- :group'gdscript)
-
-;; gdscript-fill-paragraph.el
-(defcustom gdscript-fill-comment-function 'gdscript-fill-paragraph-fill-comment
- "Function to fill comments.
-This is the function used by `gdscript-fill-paragraph' to
-fill comments."
- :type 'symbol
- :group 'gdscript)
-
-(defcustom gdscript-fill-string-function 'gdscript-fill-paragraph-fill-string
- "Function to fill strings.
-This is the function used by `gdscript-fill-paragraph' to
-fill strings."
- :type 'symbol
- :group 'gdscript)
-
-(defcustom gdscript-fill-paren-function 'gdscript-fill-paragraph-fill-paren
- "Function to fill parens.
-This is the function used by `gdscript-fill-paragraph' to
-fill parens."
- :type 'symbol
- :group 'gdscript)
-
-(defcustom gdscript-indent-def-block-scale 2
- "Multiplier applied to indentation inside multi-line def blocks."
- :version "26.1"
- :type 'integer
- :safe 'natnump
- :group 'gdscript)
-
-(defcustom gdscript-indent-line-continuation-scale 1
- "Multiplier applied to indentation of line continuation in
-general (inside parentheses and after backslash)."
- :type 'integer
- :options '(1 2)
- :group 'gdscript)
-
-(defcustom gdscript-godot-executable "godot"
- "The godot executable which is either a full path such as `~/bin/godot2.2'
-or the name of an executable on the system PATH (usually `godot')"
- :type 'string
- :group 'gdscript)
-
-(defcustom gdscript-gdformat-executable "gdformat"
- "The path to the gdformat executable.
-By default, it assumes that the executable is in the system's
-PATH."
- :type 'string
- :group 'gdscript)
-
-(defcustom gdscript-gdformat-line-length 100
- "How many characters per line to allow when formatting gdscript by gdformat."
- :type 'integer
- :group 'gdscript)
-
-(defcustom gdscript-gdformat-save-and-format nil
- "If t, save all modified buffers and format them with gdformat.
-It happens anytime Godot executable is run. Formatting runs on background,
-so it is not slowing down Godot execution."
- :type 'boolean
- :group 'gdscript)
-
-(defcustom gdscript-docs-force-online-lookup nil
- "If true, calling commands like gdscript-docs-browse-api
-browses the online API reference, even if a local copy is
-available."
- :type 'boolean
- :group 'gdscript)
-
-(defcustom gdscript-docs-use-eww t
- "If set to false use the emacs configurable `browse-url'
-function rather than `eww' directly. `browse-url' can be
-configured to open the desktop default GUI browser, for example,
-via the variable `browse-url-browser-function'"
- :type 'boolean
- :group 'gdscript)
-
-(defcustom gdscript-docs-local-path ""
- "Optional path to a local build of the Godot documentation.
-If not set to an empty string, the commands `gdscript-docs-browse-api'
-and `gdscript-docs-browse-symbol-at-point' allow you to browse the local files.
-Must be the root directory of the website, that is to say, a
-directory path containing the file `index.html'."
- :type 'string
- :group 'gdscript)
-
-(defcustom gdscript-docs-online-search-api-url "https://docs.godotengine.org/en/stable/search.html?q=%s&check_keywords=yes&area=default"
- "Online Godot API search url"
- :type 'string
- :group 'gdscript)
-
-(defcustom gdscript-debug-port 6010
- "Debugger server port."
- :type 'integer
- :group 'gdscript)
-
-(provide 'gdscript-customization)
-;;; gdscript-customization.el ends here
+;;; gdscript-customization.el --- Customizable variables for the GDScript language support -*- lexical-binding: t -*-
+
+;; Copyright (C) 2020-2026 GDQuest and contributors
+
+;; Author: Nathan Lovato
+;; Maintainer: Jen-Chieh Shen
+
+;; This file is not part of GNU Emacs
+
+;; This file 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, or (at your option)
+;; any later version.
+
+;; This program 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.
+
+;; For a full copy of the GNU General Public License
+;; see .
+
+;;; Commentary:
+;;
+;; Contains all the defcustom for gdscript-mode
+;;
+
+;;; Code:
+
+(defgroup gdscript nil
+ "GDScript language support for Emacs."
+ :group 'languages
+ :version "26"
+ :link '(emacs-commentary-link "gdscript"))
+
+(defcustom gdscript-use-type-hints t
+ "If t, inserted snippets contain type hints."
+ :type 'boolean
+ :group 'gdscript)
+
+;; gdscript-indent
+(defcustom gdscript-use-tab-indents t "Use tabs (t) or spaces (nil)."
+ :type 'boolean
+ :group 'gdscript)
+
+(defcustom gdscript-tab-width 4 "Indentation width."
+ :type 'integer
+ :group 'gdscript)
+
+(defcustom gdscript-indent-offset 4 "Default indentation offset for Gdscript."
+ :type 'integer
+ :safe 'integerp
+ :group 'gdscript)
+
+(defcustom gdscript-indent-trigger-commands '(indent-for-tab-command yas-expand yas/expand)
+ "Commands that might trigger a `gdscript-indent-line' call."
+ :type '(repeat symbol)
+ :group'gdscript)
+
+;; gdscript-fill-paragraph.el
+(defcustom gdscript-fill-comment-function 'gdscript-fill-paragraph-fill-comment
+ "Function to fill comments.
+This is the function used by `gdscript-fill-paragraph' to
+fill comments."
+ :type 'symbol
+ :group 'gdscript)
+
+(defcustom gdscript-fill-string-function 'gdscript-fill-paragraph-fill-string
+ "Function to fill strings.
+This is the function used by `gdscript-fill-paragraph' to
+fill strings."
+ :type 'symbol
+ :group 'gdscript)
+
+(defcustom gdscript-fill-paren-function 'gdscript-fill-paragraph-fill-paren
+ "Function to fill parens.
+This is the function used by `gdscript-fill-paragraph' to
+fill parens."
+ :type 'symbol
+ :group 'gdscript)
+
+(defcustom gdscript-indent-def-block-scale 2
+ "Multiplier applied to indentation inside multi-line def blocks."
+ :version "26.1"
+ :type 'integer
+ :safe 'natnump
+ :group 'gdscript)
+
+(defcustom gdscript-indent-line-continuation-scale 1
+ "Multiplier applied to indentation of line continuation in
+general (inside parentheses and after backslash)."
+ :type 'integer
+ :options '(1 2)
+ :group 'gdscript)
+
+(defcustom gdscript-godot-executable "godot"
+ "The godot executable which is either a full path such as `~/bin/godot2.2'
+or the name of an executable on the system PATH (usually `godot')"
+ :type 'string
+ :group 'gdscript)
+
+(defcustom gdscript-gdformat-executable "gdformat"
+ "The path to the gdformat executable.
+By default, it assumes that the executable is in the system's
+PATH."
+ :type 'string
+ :group 'gdscript)
+
+(defcustom gdscript-gdformat-line-length 100
+ "How many characters per line to allow when formatting gdscript by gdformat."
+ :type 'integer
+ :group 'gdscript)
+
+(defcustom gdscript-gdformat-save-and-format nil
+ "If t, save all modified buffers and format them with gdformat.
+It happens anytime Godot executable is run. Formatting runs on background,
+so it is not slowing down Godot execution."
+ :type 'boolean
+ :group 'gdscript)
+
+(defcustom gdscript-docs-force-online-lookup nil
+ "If true, calling commands like gdscript-docs-browse-api
+browses the online API reference, even if a local copy is
+available."
+ :type 'boolean
+ :group 'gdscript)
+
+(defcustom gdscript-docs-use-eww t
+ "If set to false use the emacs configurable `browse-url'
+function rather than `eww' directly. `browse-url' can be
+configured to open the desktop default GUI browser, for example,
+via the variable `browse-url-browser-function'"
+ :type 'boolean
+ :group 'gdscript)
+
+(defcustom gdscript-docs-local-path ""
+ "Optional path to a local build of the Godot documentation.
+If not set to an empty string, the commands `gdscript-docs-browse-api'
+and `gdscript-docs-browse-symbol-at-point' allow you to browse the local files.
+Must be the root directory of the website, that is to say, a
+directory path containing the file `index.html'."
+ :type 'string
+ :group 'gdscript)
+
+(defcustom gdscript-docs-online-search-api-url "https://docs.godotengine.org/en/stable/search.html?q=%s&check_keywords=yes&area=default"
+ "Online Godot API search url"
+ :type 'string
+ :group 'gdscript)
+
+(defcustom gdscript-debug-port 6010
+ "Debugger server port."
+ :type 'integer
+ :group 'gdscript)
+
+(provide 'gdscript-customization)
+;;; gdscript-customization.el ends here
diff --git a/gdscript-imenu.el b/gdscript-imenu.el
index 860b1d2..609a618 100644
--- a/gdscript-imenu.el
+++ b/gdscript-imenu.el
@@ -1,183 +1,183 @@
-;;; gdscript-imenu.el --- Imenu support for GDScript -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2020-2026 GDQuest and contributors
-
-;; Author: Nathan Lovato
-;; Fabián E. Gallina
-;; Maintainer: Jen-Chieh Shen
-
-;; This program 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.
-
-;; This program 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 this program. If not, see .
-
-;;; Commentary:
-
-;; Populates imenu with a list of headings: functions and subclasses in the
-;; current GDScript buffer.
-
-;;; Code:
-
-(require 'gdscript-indent-and-nav)
-
-(defvar gdscript-imenu-format-item-label-function
- 'gdscript-imenu-format-item-label
- "Imenu function used to format an item label.
-It must be a function with two arguments: TYPE and NAME.")
-
-(defvar gdscript-imenu-format-parent-item-label-function
- 'gdscript-imenu-format-parent-item-label
- "Imenu function used to format a parent item label.
-It must be a function with two arguments: TYPE and NAME.")
-
-(defvar gdscript-imenu-format-parent-item-jump-label-function
- 'gdscript-imenu-format-parent-item-jump-label
- "Imenu function used to format a parent jump item label.
-It must be a function with two arguments: TYPE and NAME.")
-
-(defun gdscript-imenu-format-item-label (type name)
- "Return Imenu label for single node using TYPE and NAME."
- (format "%s (%s)" name type))
-
-(defun gdscript-imenu-format-parent-item-label (type name)
- "Return Imenu label for parent node using TYPE and NAME."
- (format "%s..." (gdscript-imenu-format-item-label type name)))
-
-(defun gdscript-imenu-format-parent-item-jump-label (type _name)
- "Return Imenu label for parent node jump using TYPE and NAME."
- (if (string= type "class")
- "*class definition*"
- "*function definition*"))
-
-(defun gdscript-imenu--get-defun-type-name ()
- "Return defun type and name at current position."
- (when (looking-at gdscript-nav-beginning-of-defun-regexp)
- (let ((split (split-string (match-string-no-properties 0))))
- (if (= (length split) 2)
- split
- (list (concat (car split) " " (cadr split))
- (car (last split)))))))
-
-(defun gdscript-imenu--put-parent (type name pos tree)
- "Add the parent with TYPE, NAME and POS to TREE."
- (let ((label
- (funcall gdscript-imenu-format-item-label-function type name))
- (jump-label
- (funcall gdscript-imenu-format-parent-item-jump-label-function type name)))
- (if (not tree)
- (cons label pos)
- (cons label (cons (cons jump-label pos) tree)))))
-
-(defun gdscript-imenu--build-tree (&optional min-indent prev-indent tree)
- "Recursively build the tree of nested definitions of a node.
-Arguments MIN-INDENT, PREV-INDENT and TREE are internal and should
-not be passed explicitly unless you know what you are doing."
- (setq min-indent (or min-indent 0)
- prev-indent (or prev-indent gdscript-indent-offset))
- (let* ((pos (gdscript-nav-backward-defun))
- (defun-type-name (and pos (gdscript-imenu--get-defun-type-name)))
- (type (car defun-type-name))
- (name (cadr defun-type-name))
- (label (when name
- (funcall gdscript-imenu-format-item-label-function type name)))
- (indent (current-indentation))
- (children-indent-limit (+ gdscript-indent-offset min-indent)))
- (cond ((not pos)
- ;; Nothing found, probably near to bobp.
- nil)
- ((<= indent min-indent)
- ;; The current indentation points that this is a parent
- ;; node, add it to the tree and stop recursing.
- (gdscript-imenu--put-parent type name pos tree))
- (t
- (gdscript-imenu--build-tree
- min-indent
- indent
- (if (<= indent children-indent-limit)
- ;; This lies within the children indent offset range,
- ;; so it's a normal child of its parent (i.e., not
- ;; a child of a child).
- (cons (cons label pos) tree)
- ;; Oh no, a child of a child?! Fear not, we
- ;; know how to roll. We recursively parse these by
- ;; swapping prev-indent and min-indent plus adding this
- ;; newly found item to a fresh subtree. This works, I
- ;; promise.
- (cons
- (gdscript-imenu--build-tree
- prev-indent indent (list (cons label pos)))
- tree)))))))
-
-(defun gdscript-imenu-create-index ()
- "Return tree Imenu alist for the current Gdscript buffer.
-Change `gdscript-imenu-format-item-label-function',
-`gdscript-imenu-format-parent-item-label-function',
-`gdscript-imenu-format-parent-item-jump-label-function' to
-customize how labels are formatted."
- (goto-char (point-max))
- (let ((index)
- (tree))
- (while (setq tree (gdscript-imenu--build-tree))
- (setq index (cons tree index)))
- index))
-
-(defun gdscript-imenu-create-flat-index (&optional alist prefix)
- "Return flat outline of the current Gdscript buffer for Imenu.
-Optional argument ALIST is the tree to be flattened; when nil
-`gdscript-imenu-build-index' is used with
-`gdscript-imenu-format-parent-item-jump-label-function'
-`gdscript-imenu-format-parent-item-label-function'
-`gdscript-imenu-format-item-label-function' set to
- (lambda (type name) name)
-Optional argument PREFIX is used in recursive calls and should
-not be passed explicitly.
-
-Converts this:
-
- ((\"Foo\" . 103)
- (\"Bar\" . 138)
- (\"decorator\"
- (\"decorator\" . 173)
- (\"wrap\"
- (\"wrap\" . 353)
- (\"wrapped_f\" . 393))))
-
-To this:
-
- ((\"Foo\" . 103)
- (\"Bar\" . 138)
- (\"decorator\" . 173)
- (\"decorator.wrap\" . 353)
- (\"decorator.wrapped_f\" . 393))"
- ;; Inspired by imenu--flatten-index-alist removed in revno 21853.
- (apply
- 'nconc
- (mapcar
- (lambda (item)
- (let ((name (if prefix
- (concat prefix "." (car item))
- (car item)))
- (pos (cdr item)))
- (cond ((or (numberp pos) (markerp pos))
- (list (cons name pos)))
- ((listp pos)
- (cons
- (cons name (cdar pos))
- (gdscript-imenu-create-flat-index (cddr item) name))))))
- (or alist
- (let* ((fn (lambda (_type name) name))
- (gdscript-imenu-format-item-label-function fn)
- (gdscript-imenu-format-parent-item-label-function fn)
- (gdscript-imenu-format-parent-item-jump-label-function fn))
- (gdscript-imenu-create-index))))))
-
-(provide 'gdscript-imenu)
-;;; gdscript-imenu.el ends here
+;;; gdscript-imenu.el --- Imenu support for GDScript -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2026 GDQuest and contributors
+
+;; Author: Nathan Lovato
+;; Fabián E. Gallina
+;; Maintainer: Jen-Chieh Shen
+
+;; This program 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.
+
+;; This program 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 this program. If not, see .
+
+;;; Commentary:
+
+;; Populates imenu with a list of headings: functions and subclasses in the
+;; current GDScript buffer.
+
+;;; Code:
+
+(require 'gdscript-indent-and-nav)
+
+(defvar gdscript-imenu-format-item-label-function
+ 'gdscript-imenu-format-item-label
+ "Imenu function used to format an item label.
+It must be a function with two arguments: TYPE and NAME.")
+
+(defvar gdscript-imenu-format-parent-item-label-function
+ 'gdscript-imenu-format-parent-item-label
+ "Imenu function used to format a parent item label.
+It must be a function with two arguments: TYPE and NAME.")
+
+(defvar gdscript-imenu-format-parent-item-jump-label-function
+ 'gdscript-imenu-format-parent-item-jump-label
+ "Imenu function used to format a parent jump item label.
+It must be a function with two arguments: TYPE and NAME.")
+
+(defun gdscript-imenu-format-item-label (type name)
+ "Return Imenu label for single node using TYPE and NAME."
+ (format "%s (%s)" name type))
+
+(defun gdscript-imenu-format-parent-item-label (type name)
+ "Return Imenu label for parent node using TYPE and NAME."
+ (format "%s..." (gdscript-imenu-format-item-label type name)))
+
+(defun gdscript-imenu-format-parent-item-jump-label (type _name)
+ "Return Imenu label for parent node jump using TYPE and NAME."
+ (if (string= type "class")
+ "*class definition*"
+ "*function definition*"))
+
+(defun gdscript-imenu--get-defun-type-name ()
+ "Return defun type and name at current position."
+ (when (looking-at gdscript-nav-beginning-of-defun-regexp)
+ (let ((split (split-string (match-string-no-properties 0))))
+ (if (= (length split) 2)
+ split
+ (list (concat (car split) " " (cadr split))
+ (car (last split)))))))
+
+(defun gdscript-imenu--put-parent (type name pos tree)
+ "Add the parent with TYPE, NAME and POS to TREE."
+ (let ((label
+ (funcall gdscript-imenu-format-item-label-function type name))
+ (jump-label
+ (funcall gdscript-imenu-format-parent-item-jump-label-function type name)))
+ (if (not tree)
+ (cons label pos)
+ (cons label (cons (cons jump-label pos) tree)))))
+
+(defun gdscript-imenu--build-tree (&optional min-indent prev-indent tree)
+ "Recursively build the tree of nested definitions of a node.
+Arguments MIN-INDENT, PREV-INDENT and TREE are internal and should
+not be passed explicitly unless you know what you are doing."
+ (setq min-indent (or min-indent 0)
+ prev-indent (or prev-indent gdscript-indent-offset))
+ (let* ((pos (gdscript-nav-backward-defun))
+ (defun-type-name (and pos (gdscript-imenu--get-defun-type-name)))
+ (type (car defun-type-name))
+ (name (cadr defun-type-name))
+ (label (when name
+ (funcall gdscript-imenu-format-item-label-function type name)))
+ (indent (current-indentation))
+ (children-indent-limit (+ gdscript-indent-offset min-indent)))
+ (cond ((not pos)
+ ;; Nothing found, probably near to bobp.
+ nil)
+ ((<= indent min-indent)
+ ;; The current indentation points that this is a parent
+ ;; node, add it to the tree and stop recursing.
+ (gdscript-imenu--put-parent type name pos tree))
+ (t
+ (gdscript-imenu--build-tree
+ min-indent
+ indent
+ (if (<= indent children-indent-limit)
+ ;; This lies within the children indent offset range,
+ ;; so it's a normal child of its parent (i.e., not
+ ;; a child of a child).
+ (cons (cons label pos) tree)
+ ;; Oh no, a child of a child?! Fear not, we
+ ;; know how to roll. We recursively parse these by
+ ;; swapping prev-indent and min-indent plus adding this
+ ;; newly found item to a fresh subtree. This works, I
+ ;; promise.
+ (cons
+ (gdscript-imenu--build-tree
+ prev-indent indent (list (cons label pos)))
+ tree)))))))
+
+(defun gdscript-imenu-create-index ()
+ "Return tree Imenu alist for the current Gdscript buffer.
+Change `gdscript-imenu-format-item-label-function',
+`gdscript-imenu-format-parent-item-label-function',
+`gdscript-imenu-format-parent-item-jump-label-function' to
+customize how labels are formatted."
+ (goto-char (point-max))
+ (let ((index)
+ (tree))
+ (while (setq tree (gdscript-imenu--build-tree))
+ (setq index (cons tree index)))
+ index))
+
+(defun gdscript-imenu-create-flat-index (&optional alist prefix)
+ "Return flat outline of the current Gdscript buffer for Imenu.
+Optional argument ALIST is the tree to be flattened; when nil
+`gdscript-imenu-build-index' is used with
+`gdscript-imenu-format-parent-item-jump-label-function'
+`gdscript-imenu-format-parent-item-label-function'
+`gdscript-imenu-format-item-label-function' set to
+ (lambda (type name) name)
+Optional argument PREFIX is used in recursive calls and should
+not be passed explicitly.
+
+Converts this:
+
+ ((\"Foo\" . 103)
+ (\"Bar\" . 138)
+ (\"decorator\"
+ (\"decorator\" . 173)
+ (\"wrap\"
+ (\"wrap\" . 353)
+ (\"wrapped_f\" . 393))))
+
+To this:
+
+ ((\"Foo\" . 103)
+ (\"Bar\" . 138)
+ (\"decorator\" . 173)
+ (\"decorator.wrap\" . 353)
+ (\"decorator.wrapped_f\" . 393))"
+ ;; Inspired by imenu--flatten-index-alist removed in revno 21853.
+ (apply
+ 'nconc
+ (mapcar
+ (lambda (item)
+ (let ((name (if prefix
+ (concat prefix "." (car item))
+ (car item)))
+ (pos (cdr item)))
+ (cond ((or (numberp pos) (markerp pos))
+ (list (cons name pos)))
+ ((listp pos)
+ (cons
+ (cons name (cdar pos))
+ (gdscript-imenu-create-flat-index (cddr item) name))))))
+ (or alist
+ (let* ((fn (lambda (_type name) name))
+ (gdscript-imenu-format-item-label-function fn)
+ (gdscript-imenu-format-parent-item-label-function fn)
+ (gdscript-imenu-format-parent-item-jump-label-function fn))
+ (gdscript-imenu-create-index))))))
+
+(provide 'gdscript-imenu)
+;;; gdscript-imenu.el ends here
diff --git a/gdscript-indent-and-nav.el b/gdscript-indent-and-nav.el
index 19c6111..65cb78a 100644
--- a/gdscript-indent-and-nav.el
+++ b/gdscript-indent-and-nav.el
@@ -1,1422 +1,1422 @@
-;;; gdscript-indent-and-nav.el --- Syntax highlighting for GDScript -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2020-2026 GDQuest and contributors
-
-;; Author: Nathan Lovato
-;; Fabián E. Gallina
-;; Maintainer: Jen-Chieh Shen
-
-;; This program 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.
-
-;; This program 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 this program. If not, see .
-
-;;; Commentary:
-;;
-;; Functions copies from Emacs 27's built-in python.el to handle indents,
-;; querying info about the language's symbols or the context at point, and for
-;; navigation.
-;; There are dependencies between the *indent*, *info*, and *nav* functions that prevent us from splitting them into separate .el files at the moment.
-;;
-
-;;; Code:
-
-(require 'cl-lib)
-(require 'seq)
-
-(require 'gdscript-customization)
-(require 'gdscript-utils)
-(require 'gdscript-rx)
-(require 'gdscript-syntax)
-
-(defvar gdscript-nav-beginning-of-defun-regexp
- (gdscript-rx line-start (* space) defun (+ space) (group symbol-name))
- "Regexp matching class or function definition.
-The name of the defun should be grouped so it can be retrieved
-via `match-string'.")
-
-(defun gdscript-indent-context ()
- "Get information about the current indentation context.
-Context is returned in a cons with the form (STATUS . START).
-
-STATUS can be one of the following:
-
-keyword
--------
-
-:after-comment
- - Point is after a comment line.
- - START is the position of the \"#\" character.
-:inside-string
- - Point is inside string.
- - START is the position of the first quote that starts it.
-:no-indent
- - No possible indentation case matches.
- - START is always zero.
-
-:inside-paren
- - Fallback case when point is inside paren.
- - START is the first non space char position *after* the open paren.
-:inside-paren-at-closing-nested-paren
- - Point is on a line that contains a nested paren closer.
- - START is the position of the open paren it closes.
-:inside-paren-at-closing-paren
- - Point is on a line that contains a paren closer.
- - START is the position of the open paren.
-:inside-paren-newline-start
- - Point is inside a paren with items starting in their own line.
- - START is the position of the open paren.
-:inside-paren-newline-start-from-block
- - Point is inside a paren with items starting in their own line
- from a block start.
- - START is the position of the open paren.
-
-:after-backslash
- - Fallback case when point is after backslash.
- - START is the char after the position of the backslash.
-:after-backslash-assignment-continuation
- - Point is after a backslashed assignment.
- - START is the char after the position of the backslash.
-:after-backslash-block-continuation
- - Point is after a backslashed block continuation.
- - START is the char after the position of the backslash.
-:after-backslash-dotted-continuation
- - Point is after a backslashed dotted continuation. Previous
- line must contain a dot to align with.
- - START is the char after the position of the backslash.
-:after-backslash-first-line
- - First line following a backslashed continuation.
- - START is the char after the position of the backslash.
-
-:after-block-end
- - Point is after a line containing a block ender.
- - START is the position where the ender starts.
-:after-block-start
- - Point is after a line starting a block.
- - START is the position where the block starts.
-:after-line
- - Point is after a simple line.
- - START is the position where the previous line starts.
-:at-dedenter-block-start
- - Point is on a line starting a dedenter block.
- - START is the position where the dedenter block starts."
- (let ((ppss (save-excursion
- (beginning-of-line)
- (syntax-ppss))))
- (cond
- ;; Beginning of buffer.
- ((= (line-number-at-pos) 1)
- (cons :no-indent 0))
- ;; Inside a string.
- ((let ((start (gdscript-syntax-context 'string ppss)))
- (when start
- (cons :inside-string start))))
- ;; Inside a paren.
- ((let* ((start (gdscript-syntax-context 'paren ppss))
- (starts-in-newline
- (when start
- (save-excursion
- (goto-char start)
- (forward-char)
- (not
- (= (line-number-at-pos)
- (progn
- (gdscript--util-forward-comment)
- (line-number-at-pos))))))))
- (when start
- (cond
- ;; Current line only holds the closing paren.
- ((save-excursion
- (skip-syntax-forward " ")
- (when (and (gdscript-syntax-closing-paren-p)
- (progn
- (forward-char 1)
- (not (gdscript-syntax-context 'paren))))
- (cons :inside-paren-at-closing-paren start))))
- ;; Current line only holds a closing paren for nested.
- ((save-excursion
- (back-to-indentation)
- (gdscript-syntax-closing-paren-p))
- (cons :inside-paren-at-closing-nested-paren start))
- ;; This line starts from an opening block in its own line.
- ((save-excursion
- (goto-char start)
- (when (and
- starts-in-newline
- (save-excursion
- (back-to-indentation)
- (looking-at (gdscript-rx block-start))))
- (cons
- :inside-paren-newline-start-from-block start))))
- (starts-in-newline
- (cons :inside-paren-newline-start start))
- ;; General case.
- (t (cons :inside-paren
- (save-excursion
- (goto-char (1+ start))
- (skip-syntax-forward "(" 1)
- (skip-syntax-forward " ")
- (point))))))))
- ;; After backslash.
- ((let ((start (unless (gdscript-syntax-comment-or-string-p (syntax-ppss))
- (gdscript-info-line-ends-backslash-p
- (1- (line-number-at-pos))))))
- (when start
- (cond
- ;; Continuation of dotted expression.
- ((save-excursion
- (back-to-indentation)
- (when (eq (char-after) ?\.)
- ;; Move point back until it's not inside a paren.
- (while (prog2
- (forward-line -1)
- (and (not (bobp))
- (gdscript-syntax-context 'paren))))
- (goto-char (line-end-position))
- (while (and (search-backward
- "." (line-beginning-position) t)
- (gdscript-syntax-context-type)))
- ;; Ensure previous statement has dot to align with.
- (when (and (eq (char-after) ?\.)
- (not (gdscript-syntax-context-type)))
- (cons :after-backslash-dotted-continuation (point))))))
- ;; Continuation of block definition.
- ((let ((block-continuation-start
- (gdscript-info-block-continuation-line-p)))
- (when block-continuation-start
- (save-excursion
- (goto-char block-continuation-start)
- (re-search-forward
- (gdscript-rx block-start (* space))
- (line-end-position) t)
- (cons :after-backslash-block-continuation (point))))))
- ;; Continuation of assignment.
- ((let ((assignment-continuation-start
- (gdscript-info-assignment-continuation-line-p)))
- (when assignment-continuation-start
- (save-excursion
- (goto-char assignment-continuation-start)
- (cons :after-backslash-assignment-continuation (point))))))
- ;; First line after backslash continuation start.
- ((save-excursion
- (goto-char start)
- (when (or (= (line-number-at-pos) 1)
- (not (gdscript-info-beginning-of-backslash
- (1- (line-number-at-pos)))))
- (cons :after-backslash-first-line start))))
- ;; General case.
- (t (cons :after-backslash start))))))
- ;; After beginning of block.
- ((let ((start (save-excursion
- (back-to-indentation)
- (gdscript--util-forward-comment -1)
- (when (equal (char-before) ?:)
- (gdscript-nav-beginning-of-block)))))
- (when start
- (cons :after-block-start start))))
- ;; At dedenter statement.
- ((let ((start (gdscript-info-dedenter-statement-p)))
- (when start
- (cons :at-dedenter-block-start start))))
- ;; After normal line, comment or ender (default case).
- ((save-excursion
- (back-to-indentation)
- (skip-chars-backward " \t\n")
- (if (bobp)
- (cons :no-indent 0)
- (gdscript-nav-beginning-of-statement)
- (cons
- (cond ((gdscript-info-current-line-comment-p)
- :after-comment)
- ((save-excursion
- (goto-char (line-end-position))
- (gdscript--util-forward-comment -1)
- (gdscript-nav-beginning-of-statement)
- (looking-at (gdscript-rx block-ender)))
- :after-block-end)
- (t :after-line))
- (point))))))))
-
-(defun gdscript-indent--calculate-indentation ()
- "Internal implementation of `gdscript-indent-calculate-indentation'.
-May return an integer for the maximum possible indentation at
-current context or a list of integers. The latter case is only
-happening for :at-dedenter-block-start context since the
-possibilities can be narrowed to specific indentation points."
- (save-excursion
- (pcase (gdscript-indent-context)
- (`(:no-indent . ,_) (prog-first-column)) ; usually 0
- (`(,(or :after-line
- :after-comment
- :inside-string
- :after-backslash) . ,start)
- ;; Copy previous indentation.
- (goto-char start)
- (current-indentation))
- (`(,(or :inside-paren-at-closing-paren
- :inside-paren-at-closing-nested-paren) . ,start)
- (goto-char (+ 1 start))
- (if (looking-at "[ \t]*\\(?:#\\|$\\)")
- ;; Copy previous indentation.
- (current-indentation)
- ;; Align with opening paren.
- (current-column)))
- (`(,(or :after-block-start
- :after-backslash-first-line
- :inside-paren-newline-start) . ,start)
- ;; Add one indentation level.
- (goto-char start)
- (+ (current-indentation) gdscript-indent-offset))
- (`(,(or :inside-paren
- :after-backslash-block-continuation
- :after-backslash-dotted-continuation
- :after-backslash-assignment-continuation) . ,start)
- ;; Use (possibly extra) indentation given by the configuration
- (goto-char start)
- (+ (current-indentation)
- (* gdscript-indent-offset
- gdscript-indent-line-continuation-scale)))
- (`(:after-block-end . ,start)
- ;; Subtract one indentation level.
- (goto-char start)
- (- (current-indentation) gdscript-indent-offset))
- (`(:at-dedenter-block-start . ,_)
- ;; List all possible indentation levels from opening blocks.
- (let ((opening-block-start-points
- (gdscript-info-dedenter-opening-block-positions)))
- (if (not opening-block-start-points)
- (prog-first-column) ; if not found default to first column
- (mapcar (lambda (pos)
- (save-excursion
- (goto-char pos)
- (current-indentation)))
- opening-block-start-points))))
- (`(,(or :inside-paren-newline-start-from-block) . ,start)
- (goto-char start)
- (+ (current-indentation)
- (* gdscript-indent-offset gdscript-indent-def-block-scale))))))
-
-(defun gdscript-indent--calculate-levels (indentation)
- "Calculate levels list given INDENTATION.
-Argument INDENTATION can either be an integer or a list of
-integers. Levels are returned in ascending order, and in the
-case INDENTATION is a list, this order is enforced."
- (if (listp indentation)
- (sort (copy-sequence indentation) #'<)
- (nconc (number-sequence (prog-first-column) (1- indentation)
- gdscript-indent-offset)
- (list indentation))))
-
-(defun gdscript-indent--previous-level (levels indentation)
- "Return previous level from LEVELS relative to INDENTATION."
- (let* ((levels (sort (copy-sequence levels) #'>))
- (default (car levels)))
- (catch 'return
- (dolist (level levels)
- (when (funcall #'< level indentation)
- (throw 'return level)))
- default)))
-
-(defun gdscript-indent-calculate-indentation (&optional previous)
- "Calculate indentation.
-Get indentation of PREVIOUS level when argument is non-nil.
-Return the max level of the cycle when indentation reaches the
-minimum."
- (let* ((indentation (gdscript-indent--calculate-indentation))
- (levels (gdscript-indent--calculate-levels indentation)))
- (if previous
- (gdscript-indent--previous-level levels (current-indentation))
- (if levels
- (apply #'max levels)
- (prog-first-column)))))
-
-(defun gdscript-indent-line (&optional previous)
- "Internal implementation of `gdscript-indent-line-function'.
-Use the PREVIOUS level when argument is non-nil, otherwise indent
-to the maximum available level. When indentation is the minimum
-possible and PREVIOUS is non-nil, cycle back to the maximum
-level."
- (let ((follow-indentation-p
- (<= (current-column) (current-indentation))))
- (save-excursion
- (indent-line-to
- (gdscript-indent-calculate-indentation previous))
- (gdscript-info-dedenter-opening-block-message))
- (when follow-indentation-p
- (back-to-indentation))))
-
-(defun gdscript-indent-calculate-levels ()
- "Return possible indentation levels."
- (gdscript-indent--calculate-levels
- (gdscript-indent--calculate-indentation)))
-
-(defun gdscript-indent-line-function ()
- "`indent-line-function' for Gdscript mode.
-When the variable `last-command' is equal to one of the symbols
-inside `gdscript-indent-trigger-commands' it cycles possible
-indentation levels from right to left."
- (gdscript-indent-line
- (and (memq this-command gdscript-indent-trigger-commands)
- (eq last-command this-command))))
-
-(defun gdscript-indent-dedent-line ()
- "De-indent current line."
- (interactive "*")
- (when (and (not (bolp))
- (not (gdscript-syntax-comment-or-string-p))
- (= (current-indentation) (current-column)))
- (gdscript-indent-line t)
- t))
-
-(defun gdscript-indent-dedent-line-backspace (arg)
- "De-indent current line.
-Argument ARG is passed to `backward-delete-char-untabify' when
-point is not in between the indentation."
- (interactive "*p")
- (unless (gdscript-indent-dedent-line)
- (backward-delete-char-untabify arg)))
-
-(put 'gdscript-indent-dedent-line-backspace 'delete-selection 'supersede)
-
-(defun gdscript-indent-region (start end)
- "Indent a Gdscript region automagically.
-
-Called from a program, START and END specify the region to indent."
- (let ((deactivate-mark nil))
- (save-excursion
- (goto-char end)
- (setq end (point-marker))
- (goto-char start)
- (or (bolp) (forward-line 1))
- (while (< (point) end)
- (or (and (bolp) (eolp))
- (when (and
- ;; Skip if previous line is empty or a comment.
- (save-excursion
- (let ((line-is-comment-p
- (gdscript-info-current-line-comment-p)))
- (forward-line -1)
- (not
- (or (and (gdscript-info-current-line-comment-p)
- ;; Unless this line is a comment too.
- (not line-is-comment-p))
- (gdscript-info-current-line-empty-p)))))
- ;; Skip if current line is a block start, a
- ;; dedenter or block ender.
- (save-excursion
- (back-to-indentation)
- (not (looking-at
- (gdscript-rx
- (or block-start dedenter block-ender))))))
- (gdscript-indent-line)))
- (forward-line 1))
- (move-marker end nil))))
-
-(defun gdscript-indent-shift-left (start end &optional count)
- "Shift lines contained in region START END by COUNT columns to the left.
-COUNT defaults to `gdscript-indent-offset'. If region isn't
-active, the current line is shifted. The shifted region includes
-the lines in which START and END lie. An error is signaled if
-any lines in the region are indented less than COUNT columns."
- (interactive
- (if mark-active
- (list (region-beginning) (region-end) current-prefix-arg)
- (list (line-beginning-position) (line-end-position) current-prefix-arg)))
- (if count
- (setq count (prefix-numeric-value count))
- (setq count gdscript-indent-offset))
- (when (> count 0)
- (let ((deactivate-mark nil))
- (save-excursion
- (goto-char start)
- (while (< (point) end)
- (if (and (< (current-indentation) count)
- (not (looking-at "[ \t]*$")))
- (user-error "Can't shift all lines enough"))
- (forward-line))
- (indent-rigidly start end (- count))))))
-
-(defun gdscript-indent-shift-right (start end &optional count)
- "Shift lines contained in region START END by COUNT columns to the right.
-COUNT defaults to `gdscript-indent-offset'. If region isn't
-active, the current line is shifted. The shifted region includes
-the lines in which START and END lie."
- (interactive
- (if mark-active
- (list (region-beginning) (region-end) current-prefix-arg)
- (list (line-beginning-position) (line-end-position) current-prefix-arg)))
- (let ((deactivate-mark nil))
- (setq count (if count (prefix-numeric-value count)
- gdscript-indent-offset))
- (indent-rigidly start end count)))
-
-(defun gdscript-indent-post-self-insert-function ()
- "Adjust indentation after insertion of some characters.
-This function is intended to be added to `post-self-insert-hook.'
-If a line renders a paren alone, after adding a char before it,
-the line will be re-indented automatically if needed."
- (when (and electric-indent-mode
- (eq (char-before) last-command-event)
- (not (gdscript-syntax-context 'string))
- (save-excursion
- (beginning-of-line)
- (not (gdscript-syntax-context 'string (syntax-ppss)))))
- (cond
- ;; Electric indent inside parens
- ((and
- (not (bolp))
- (let ((paren-start (gdscript-syntax-context 'paren)))
- ;; Check that point is inside parens.
- (when paren-start
- (not
- ;; Filter the case where input is happening in the same
- ;; line where the open paren is.
- (= (line-number-at-pos)
- (line-number-at-pos paren-start)))))
- ;; When content has been added before the closing paren or a
- ;; comma has been inserted, it's ok to do the trick.
- (or
- (memq (char-after) '(?\) ?\] ?\}))
- (eq (char-before) ?,)))
- (save-excursion
- (goto-char (line-beginning-position))
- (let ((indentation (gdscript-indent-calculate-indentation)))
- (when (and (numberp indentation) (< (current-indentation) indentation))
- (indent-line-to indentation)))))
- ;; Electric colon
- ((and (eq ?: last-command-event)
- (memq ?: electric-indent-chars)
- (not current-prefix-arg)
- ;; Trigger electric colon only at end of line
- (eolp)
- ;; Avoid re-indenting on extra colon
- (not (equal ?: (char-before (1- (point)))))
- (not (gdscript-syntax-comment-or-string-p)))
- ;; Just re-indent dedenters
- (let ((dedenter-pos (gdscript-info-dedenter-statement-p)))
- (when dedenter-pos
- (let ((start (copy-marker dedenter-pos))
- (end (point-marker)))
- (save-excursion
- (goto-char start)
- (gdscript-indent-line)
- (unless (= (line-number-at-pos start)
- (line-number-at-pos end))
- ;; Reindent region if this is a multiline statement
- (gdscript-indent-region start end))))))))))
-
-;;; Misc helpers
-(defun gdscript-info-current-defun (&optional include-type)
- "Return name of surrounding function with Gdscript compatible dotty syntax.
-Optional argument INCLUDE-TYPE indicates to include the type of the defun.
-This function can be used as the value of `add-log-current-defun-function'
-since it returns nil if point is not inside a defun."
- (save-restriction
- (widen)
- (save-excursion
- (end-of-line 1)
- (let ((names)
- (starting-indentation (current-indentation))
- (starting-pos (point))
- (first-run t)
- (last-indent)
- (type))
- (catch 'exit
- (while (gdscript-nav-beginning-of-defun 1)
- (when (save-match-data
- (and
- (or (not last-indent)
- (< (current-indentation) last-indent))
- (or
- (and first-run
- (save-excursion
- ;; If this is the first run, we may add
- ;; the current defun at point.
- (setq first-run nil)
- (goto-char starting-pos)
- (gdscript-nav-beginning-of-statement)
- (beginning-of-line 1)
- (looking-at-p
- gdscript-nav-beginning-of-defun-regexp)))
- (< starting-pos
- (save-excursion
- (let ((min-indent
- (+ (current-indentation)
- gdscript-indent-offset)))
- (if (< starting-indentation min-indent)
- ;; If the starting indentation is not
- ;; within the min defun indent make the
- ;; check fail.
- starting-pos
- ;; Else go to the end of defun and add
- ;; up the current indentation to the
- ;; ending position.
- (gdscript-nav-end-of-defun)
- (+ (point)
- (if (>= (current-indentation) min-indent)
- (1+ (current-indentation))
- 0)))))))))
- (save-match-data (setq last-indent (current-indentation)))
- (if (or (not include-type) type)
- (setq names (cons (match-string-no-properties 1) names))
- (let ((match (split-string (match-string-no-properties 0))))
- (setq type (car match))
- (setq names (cons (cadr match) names)))))
- ;; Stop searching ASAP.
- (and (= (current-indentation) 0) (throw 'exit t))))
- (and names
- (concat (and type (format "%s " type))
- (mapconcat #'identity names ".")))))))
-
-(defun gdscript-info-current-symbol (&optional replace-self)
- "Return current symbol using dotty syntax.
-With optional argument REPLACE-SELF convert \"self\" to current
-parent defun name."
- (let ((name
- (and (not (gdscript-syntax-comment-or-string-p))
- (with-syntax-table gdscript-dotty-syntax-table
- (let ((sym (symbol-at-point)))
- (and sym
- (substring-no-properties (symbol-name sym))))))))
- (when name
- (if (not replace-self)
- name
- (let ((current-defun (gdscript-info-current-defun)))
- (if (not current-defun)
- name
- (replace-regexp-in-string
- (gdscript-rx line-start word-start "self" word-end ?.)
- (concat
- (mapconcat 'identity
- (butlast (split-string current-defun "\\."))
- ".") ".")
- name)))))))
-
-(defun gdscript-info-statement-starts-block-p ()
- "Return non-nil if current statement opens a block."
- (save-excursion
- (gdscript-nav-beginning-of-statement)
- (looking-at (gdscript-rx block-start))))
-
-(defun gdscript-info-statement-ends-block-p ()
- "Return non-nil if point is at end of block."
- (let ((end-of-block-pos (save-excursion
- (gdscript-nav-end-of-block)))
- (end-of-statement-pos (save-excursion
- (gdscript-nav-end-of-statement))))
- (and end-of-block-pos end-of-statement-pos
- (= end-of-block-pos end-of-statement-pos))))
-
-(defun gdscript-info-beginning-of-statement-p ()
- "Return non-nil if point is at beginning of statement."
- (= (point) (save-excursion
- (gdscript-nav-beginning-of-statement)
- (point))))
-
-(defun gdscript-info-end-of-statement-p ()
- "Return non-nil if point is at end of statement."
- (= (point) (save-excursion
- (gdscript-nav-end-of-statement)
- (point))))
-
-(defun gdscript-info-beginning-of-block-p ()
- "Return non-nil if point is at beginning of block."
- (and (gdscript-info-beginning-of-statement-p)
- (gdscript-info-statement-starts-block-p)))
-
-(defun gdscript-info-end-of-block-p ()
- "Return non-nil if point is at end of block."
- (and (gdscript-info-end-of-statement-p)
- (gdscript-info-statement-ends-block-p)))
-
-(defun gdscript-info-dedenter-opening-block-position ()
- "Return the point of the closest block the current line closes.
-Returns nil if point is not on a dedenter statement or no opening
-block can be detected. The latter case meaning current file is
-likely an invalid gdscript file."
- (let ((positions (gdscript-info-dedenter-opening-block-positions))
- (indentation (current-indentation))
- (position))
- (while (and (not position)
- positions)
- (save-excursion
- (goto-char (car positions))
- (if (<= (current-indentation) indentation)
- (setq position (car positions))
- (setq positions (cdr positions)))))
- position))
-
-(defun gdscript-indent--nav-block ()
- "Search backward for block start."
- (re-search-backward (gdscript-rx block-start) nil t)
- ;; At this point `(match-string-no-properties 0)' doesn't match whole gdscript's block expression,
- ;; due to how `re-search-backward' works.
- ;; To make `(match-string-no-properties 0)' match whole gdscript's block expression
- ;; let's use `re-search-forward' from beginning of a statement.
- (gdscript-nav-beginning-of-statement)
- (prog1
- (re-search-forward (gdscript-rx block-start) nil t)
- ;; And let's put point at the beginning of the statement.
- (gdscript-nav-beginning-of-statement)))
-
-(defun gdscript-info-dedenter-opening-block-positions ()
- "Return points of blocks the current line may close sorted by closer.
-Returns nil if point is not on a dedenter statement or no opening
-block can be detected. The latter case meaning current file is
-likely an invalid gdscript file."
- (save-excursion
- (let ((dedenter-pos (gdscript-info-dedenter-statement-p)))
- (when dedenter-pos
- (goto-char dedenter-pos)
- (let* ((cur-line (line-beginning-position))
- (pairs '(("elif" "elif" "if")
- ("else" "if" "elif")))
- (dedenter (match-string-no-properties 0))
- (possible-opening-blocks (cdr (assoc-string dedenter pairs)))
- (collected-indentations)
- (opening-blocks))
- (catch 'exit
- (while (gdscript-nav--syntactically #'gdscript-indent--nav-block #'<)
- (let ((indentation (current-indentation)))
- (when (and (not (memq indentation collected-indentations))
- (or (not collected-indentations)
- (< indentation (apply #'min collected-indentations)))
- ;; There must be no line with indentation
- ;; smaller than `indentation' (except for
- ;; blank lines) between the found opening
- ;; block and the current line, otherwise it
- ;; is not an opening block.
- (save-excursion
- (forward-line)
- (let ((no-back-indent t))
- (save-match-data
- (while (and (< (point) cur-line)
- (setq no-back-indent
- (or (> (current-indentation) indentation)
- (gdscript-info-current-line-empty-p)
- (gdscript-info-current-line-comment-p)
- (not (equal :after-line (car (gdscript-indent-context)))))))
- (forward-line)))
- no-back-indent)))
- (setq collected-indentations
- (cons indentation collected-indentations))
- (when
- (seq-contains-p possible-opening-blocks
- (string-trim (match-string-no-properties 0))
- (lambda (elt e) (string-prefix-p elt e)))
- (setq opening-blocks (cons (point) opening-blocks))))
- (when (zerop indentation)
- (throw 'exit nil)))))
- ;; sort by closer
- (nreverse opening-blocks))))))
-
-(defun gdscript-info-dedenter-opening-block-message ()
- "Message the first line of the block the current statement closes."
- (let ((point (gdscript-info-dedenter-opening-block-position)))
- (when point
- (message "Closes %s" (save-excursion
- (goto-char point)
- (buffer-substring
- (point) (line-end-position)))))))
-
-(defun gdscript-info-dedenter-statement-p ()
- "Return point if current statement is a dedenter.
-Sets `match-data' to the keyword that starts the dedenter
-statement."
- (save-excursion
- (gdscript-nav-beginning-of-statement)
- (when (and (not (gdscript-syntax-context-type))
- (looking-at (gdscript-rx dedenter)))
- (point))))
-
-(defun gdscript-info-line-ends-backslash-p (&optional line-number)
- "Return non-nil if current line ends with backslash.
-With optional argument LINE-NUMBER, check that line instead."
- (save-excursion
- (when line-number
- (gdscript--util-goto-line line-number))
- (while (and (not (eobp))
- (goto-char (line-end-position))
- (gdscript-syntax-context 'paren)
- (not (equal (char-before (point)) ?\\)))
- (forward-line 1))
- (when (equal (char-before) ?\\)
- (point-marker))))
-
-(defun gdscript-info-beginning-of-backslash (&optional line-number)
- "Return the point where the backslashed line start.
-Optional argument LINE-NUMBER forces the line number to check against."
- (save-excursion
- (when line-number
- (gdscript--util-goto-line line-number))
- (when (gdscript-info-line-ends-backslash-p)
- (while (save-excursion
- (goto-char (line-beginning-position))
- (gdscript-syntax-context 'paren))
- (forward-line -1))
- (back-to-indentation)
- (point-marker))))
-
-(defun gdscript-info-continuation-line-p ()
- "Check if current line is continuation of another.
-When current line is continuation of another return the point
-where the continued line ends."
- (save-excursion
- (let* ((context-type (progn
- (back-to-indentation)
- (gdscript-syntax-context-type)))
- (line-start (line-number-at-pos))
- (context-start (when context-type
- (gdscript-syntax-context context-type))))
- (cond ((equal context-type 'paren)
- ;; Lines inside a paren are always a continuation line
- ;; (except the first one).
- (gdscript--util-forward-comment -1)
- (point-marker))
- ((member context-type '(string comment))
- ;; move forward an roll again
- (goto-char context-start)
- (gdscript--util-forward-comment)
- (gdscript-info-continuation-line-p))
- (t
- ;; Not within a paren, string or comment, the only way
- ;; we are dealing with a continuation line is that
- ;; previous line contains a backslash, and this can
- ;; only be the previous line from current
- (back-to-indentation)
- (gdscript--util-forward-comment -1)
- (when (and (equal (1- line-start) (line-number-at-pos))
- (gdscript-info-line-ends-backslash-p))
- (point-marker)))))))
-
-(defun gdscript-info-block-continuation-line-p ()
- "Return non-nil if current line is a continuation of a block."
- (save-excursion
- (when (gdscript-info-continuation-line-p)
- (forward-line -1)
- (back-to-indentation)
- (when (looking-at (gdscript-rx block-start))
- (point-marker)))))
-
-(defun gdscript-info-assignment-statement-p (&optional current-line-only)
- "Check if current line is an assignment.
-With argument CURRENT-LINE-ONLY is non-nil, don't follow any
-continuations, just check the if current line is an assignment."
- (save-excursion
- (let ((found nil))
- (if current-line-only
- (back-to-indentation)
- (gdscript-nav-beginning-of-statement))
- (while (and
- (re-search-forward (gdscript-rx not-simple-operator
- assignment-operator
- (group not-simple-operator))
- (line-end-position) t)
- (not found))
- (save-excursion
- ;; The assignment operator should not be inside a string.
- (backward-char (length (match-string-no-properties 1)))
- (setq found (not (gdscript-syntax-context-type)))))
- (when found
- (skip-syntax-forward " ")
- (point-marker)))))
-
-;; TODO: rename to clarify this is only for the first continuation
-;; line or remove it and move its body to `gdscript-indent-context'.
-(defun gdscript-info-assignment-continuation-line-p ()
- "Check if current line is the first continuation of an assignment.
-When current line is continuation of another with an assignment
-return the point of the first non-blank character after the
-operator."
- (save-excursion
- (when (gdscript-info-continuation-line-p)
- (forward-line -1)
- (gdscript-info-assignment-statement-p t))))
-
-(defun gdscript-info-looking-at-beginning-of-defun (&optional syntax-ppss)
- "Check if point is at `beginning-of-defun' using SYNTAX-PPSS."
- (and (not (gdscript-syntax-context-type (or syntax-ppss (syntax-ppss))))
- (save-excursion
- (beginning-of-line 1)
- (looking-at gdscript-nav-beginning-of-defun-regexp))))
-
-(defun gdscript-info-current-line-comment-p ()
- "Return non-nil if current line is a comment line."
- (char-equal
- (or (char-after (+ (line-beginning-position) (/ (current-indentation) gdscript-indent-offset))) ?_)
- ?#))
-
-(defun gdscript-info-current-line-empty-p ()
- "Return non-nil if current line is empty, ignoring whitespace."
- (save-excursion
- (beginning-of-line 1)
- (looking-at
- (gdscript-rx line-start (* whitespace)
- (group (* not-newline))
- (* whitespace) line-end))
- (string-equal "" (match-string-no-properties 1))))
-
-;;; Navigation
-
-(defun gdscript-nav--beginning-of-defun (&optional arg)
- "Internal implementation of `gdscript-nav-beginning-of-defun'.
-With positive ARG search backwards, else search forwards."
- (when (or (null arg) (= arg 0)) (setq arg 1))
- (let* ((re-search-fn (if (> arg 0)
- #'re-search-backward
- #'re-search-forward))
- (line-beg-pos (line-beginning-position))
- (line-content-start (+ line-beg-pos (current-indentation)))
- (pos (point-marker))
- (beg-indentation
- (and (> arg 0)
- (save-excursion
- (while (and
- (not (gdscript-info-looking-at-beginning-of-defun))
- (gdscript-nav-backward-block)))
- (or (and (gdscript-info-looking-at-beginning-of-defun)
- (+ (current-indentation) gdscript-indent-offset))
- 0))))
- (found
- (progn
- (when (and (< arg 0)
- (gdscript-info-looking-at-beginning-of-defun))
- (end-of-line 1))
- (while (and (funcall re-search-fn
- gdscript-nav-beginning-of-defun-regexp nil t)
- (or (gdscript-syntax-context-type)
- ;; Handle nested defuns when moving
- ;; backwards by checking indentation.
- (and (> arg 0)
- (not (= (current-indentation) 0))
- (>= (current-indentation) beg-indentation)))))
- (and (gdscript-info-looking-at-beginning-of-defun)
- (or (not (= (line-number-at-pos pos)
- (line-number-at-pos)))
- (and (>= (point) line-beg-pos)
- (<= (point) line-content-start)
- (> pos line-content-start)))))))
- (if found
- (or (beginning-of-line 1) t)
- (and (goto-char pos) nil))))
-
-(defun gdscript-nav-beginning-of-defun (&optional arg)
- "Move point to `beginning-of-defun'.
-With positive ARG search backwards else search forward.
-ARG nil or 0 defaults to 1. When searching backwards,
-nested defuns are handled with care depending on current
-point position. Return non-nil if point is moved to
-`beginning-of-defun'."
- (when (or (null arg) (= arg 0)) (setq arg 1))
- (let ((found))
- (while (and (not (= arg 0))
- (let ((keep-searching-p
- (gdscript-nav--beginning-of-defun arg)))
- (when (and keep-searching-p (null found))
- (setq found t))
- keep-searching-p))
- (setq arg (if (> arg 0) (1- arg) (1+ arg))))
- found))
-
-(defun gdscript-nav-end-of-defun ()
- "Move point to the end of def or class.
-Returns nil if point is not in a def or class."
- (interactive)
- (let ((beg-defun-indent)
- (beg-pos (point)))
- (when (or (gdscript-info-looking-at-beginning-of-defun)
- (gdscript-nav-beginning-of-defun 1)
- (gdscript-nav-beginning-of-defun -1))
- (setq beg-defun-indent (current-indentation))
- (while (progn
- (gdscript-nav-end-of-statement)
- (gdscript--util-forward-comment 1)
- (and (> (current-indentation) beg-defun-indent)
- (not (eobp)))))
- (gdscript--util-forward-comment -1)
- (forward-line 1)
- ;; Ensure point moves forward.
- (and (> beg-pos (point)) (goto-char beg-pos)))))
-
-(defun gdscript-nav--syntactically (fn poscompfn &optional contextfn)
- "Move point using FN avoiding places with specific context.
-FN must take no arguments. POSCOMPFN is a two arguments function
-used to compare current and previous point after it is moved
-using FN, this is normally a less-than or greater-than
-comparison. Optional argument CONTEXTFN defaults to
-`gdscript-syntax-context-type' and is used for checking current
-point context, it must return a non-nil value if this point must
-be skipped."
- (let ((contextfn (or contextfn 'gdscript-syntax-context-type))
- (start-pos (point-marker))
- (prev-pos))
- (catch 'found
- (while t
- (let* ((newpos
- (and (funcall fn) (point-marker)))
- (context (funcall contextfn)))
- (cond ((and (not context) newpos
- (or (and (not prev-pos) newpos)
- (and prev-pos newpos
- (funcall poscompfn newpos prev-pos))))
- (throw 'found (point-marker)))
- ((and newpos context)
- (setq prev-pos (point)))
- (t (unless newpos (goto-char start-pos))
- (throw 'found nil))))))))
-
-(defun gdscript-nav--forward-defun (arg)
- "Internal implementation of gdscript-nav-{backward,forward}-defun.
-Uses ARG to define which function to call, and how many times
-repeat it."
- (let ((found))
- (while (and (> arg 0)
- (setq found
- (gdscript-nav--syntactically
- (lambda ()
- (re-search-forward
- gdscript-nav-beginning-of-defun-regexp nil t))
- '>)))
- (setq arg (1- arg)))
- (while (and (< arg 0)
- (setq found
- (gdscript-nav--syntactically
- (lambda ()
- (re-search-backward
- gdscript-nav-beginning-of-defun-regexp nil t))
- '<)))
- (setq arg (1+ arg)))
- found))
-
-(defun gdscript-nav-backward-defun (&optional arg)
- "Navigate to closer defun backward ARG times.
-Unlikely `gdscript-nav-beginning-of-defun' this doesn't care about
-nested definitions."
- (interactive "^p")
- (gdscript-nav--forward-defun (- (or arg 1))))
-
-(defun gdscript-nav-forward-defun (&optional arg)
- "Navigate to closer defun forward ARG times.
-Unlikely `gdscript-nav-beginning-of-defun' this doesn't care about
-nested definitions."
- (interactive "^p")
- (gdscript-nav--forward-defun (or arg 1)))
-
-(defun gdscript-nav-beginning-of-statement ()
- "Move to start of current statement."
- (interactive "^")
- (forward-line 0)
- (let* ((ppss (syntax-ppss))
- (context-point
- (or
- (gdscript-syntax-context 'paren ppss)
- (gdscript-syntax-context 'string ppss))))
- (cond ((bobp))
- (context-point
- (goto-char context-point)
- (gdscript-nav-beginning-of-statement))
- ((save-excursion
- (forward-line -1)
- (gdscript-info-line-ends-backslash-p))
- (forward-line -1)
- (gdscript-nav-beginning-of-statement))))
- (back-to-indentation)
- (point-marker))
-
-(defun gdscript-nav-end-of-statement (&optional noend)
- "Move to end of current statement.
-Optional argument NOEND is internal and makes the logic to not
-jump to the end of line when moving forward searching for the end
-of the statement."
- (interactive "^")
- (let (string-start bs-pos (last-string-end 0))
- (while (and (or noend (goto-char (line-end-position)))
- (not (eobp))
- (cond ((setq string-start (gdscript-syntax-context 'string))
- ;; The assertion can only fail if syntax table
- ;; text properties and the `syntax-ppss' cache
- ;; are somehow out of whack. This has been
- ;; observed when using `syntax-ppss' during
- ;; narrowing.
- (cl-assert (>= string-start last-string-end)
- :show-args
- "\
-Overlapping strings detected (start=%d, last-end=%d)")
- (goto-char string-start)
- (if (gdscript-syntax-context 'paren)
- ;; Ended up inside a paren, roll again.
- (gdscript-nav-end-of-statement t)
- ;; This is not inside a paren, move to the
- ;; end of this string.
- (goto-char (+ (point)
- (gdscript-syntax-count-quotes
- (char-after (point)) (point))))
- (setq last-string-end
- (or (re-search-forward
- (rx (syntax string-delimiter)) nil t)
- (goto-char (point-max))))))
- ((gdscript-syntax-context 'paren)
- ;; The statement won't end before we've escaped
- ;; at least one level of parenthesis.
- (condition-case err
- (goto-char (scan-lists (point) 1 -1))
- (scan-error (goto-char (nth 3 err)))))
- ((setq bs-pos (gdscript-info-line-ends-backslash-p))
- (goto-char bs-pos)
- (forward-line 1))))))
- (point-marker))
-
-(defun gdscript-nav-backward-statement (&optional arg)
- "Move backward to previous statement.
-With ARG, repeat. See `gdscript-nav-forward-statement'."
- (interactive "^p")
- (or arg (setq arg 1))
- (gdscript-nav-forward-statement (- arg)))
-
-(defun gdscript-nav-forward-statement (&optional arg)
- "Move forward to next statement.
-With ARG, repeat. With negative argument, move ARG times
-backward to previous statement."
- (interactive "^p")
- (or arg (setq arg 1))
- (while (> arg 0)
- (gdscript-nav-end-of-statement)
- (gdscript--util-forward-comment)
- (gdscript-nav-beginning-of-statement)
- (setq arg (1- arg)))
- (while (< arg 0)
- (gdscript-nav-beginning-of-statement)
- (gdscript--util-forward-comment -1)
- (gdscript-nav-beginning-of-statement)
- (setq arg (1+ arg))))
-
-(defun gdscript-nav-beginning-of-block ()
- "Move to start of current block."
- (interactive "^")
- (let ((starting-pos (point)))
- (if (progn
- (gdscript-nav-beginning-of-statement)
- (looking-at (gdscript-rx block-start)))
- (point-marker)
- ;; Go to first line beginning a statement
- (while (and (not (bobp))
- (or (and (gdscript-nav-beginning-of-statement) nil)
- (gdscript-info-current-line-comment-p)
- (gdscript-info-current-line-empty-p)))
- (forward-line -1))
- (let ((block-matching-indent
- (- (current-indentation) gdscript-indent-offset)))
- (while
- (and (gdscript-nav-backward-block)
- (> (current-indentation) block-matching-indent)))
- (if (and (looking-at (gdscript-rx block-start))
- (= (current-indentation) block-matching-indent))
- (point-marker)
- (and (goto-char starting-pos) nil))))))
-
-(defun gdscript-nav-end-of-block ()
- "Move to end of current block."
- (interactive "^")
- (when (gdscript-nav-beginning-of-block)
- (let ((block-indentation (current-indentation)))
- (gdscript-nav-end-of-statement)
- (while (and (forward-line 1)
- (not (eobp))
- (or (and (> (current-indentation) block-indentation)
- (or (gdscript-nav-end-of-statement) t))
- (gdscript-info-current-line-comment-p)
- (gdscript-info-current-line-empty-p))))
- (gdscript--util-forward-comment -1)
- (point-marker))))
-
-(defun gdscript-nav-backward-block (&optional arg)
- "Move backward to previous block of code.
-With ARG, repeat. See `gdscript-nav-forward-block'."
- (interactive "^p")
- (or arg (setq arg 1))
- (gdscript-nav-forward-block (- arg)))
-
-(defun gdscript-nav-forward-block (&optional arg)
- "Move forward to next block of code.
-With ARG, repeat. With negative argument, move ARG times
-backward to previous block."
- (interactive "^p")
- (or arg (setq arg 1))
- (let ((block-start-regexp
- (gdscript-rx line-start (* whitespace) block-start))
- (starting-pos (point)))
- (while (> arg 0)
- (gdscript-nav-end-of-statement)
- (while (and
- (re-search-forward block-start-regexp nil t)
- (gdscript-syntax-context-type)))
- (setq arg (1- arg)))
- (while (< arg 0)
- (gdscript-nav-beginning-of-statement)
- (while (and
- (re-search-backward block-start-regexp nil t)
- (gdscript-syntax-context-type)))
- (setq arg (1+ arg)))
- (gdscript-nav-beginning-of-statement)
- (if (not (looking-at (gdscript-rx block-start)))
- (and (goto-char starting-pos) nil)
- (and (not (= (point) starting-pos)) (point-marker)))))
-
-(defun gdscript-nav--lisp-forward-sexp (&optional arg)
- "Standard version `forward-sexp'.
-It ignores completely the value of `forward-sexp-function' by
-setting it to nil before calling `forward-sexp'. With positive
-ARG move forward only one sexp, else move backwards."
- (let ((forward-sexp-function)
- (arg (if (or (not arg) (> arg 0)) 1 -1)))
- (forward-sexp arg)))
-
-(defun gdscript-nav--lisp-forward-sexp-safe (&optional arg)
- "Safe version of standard `forward-sexp'.
-When at end of sexp (i.e. looking at an opening/closing paren)
-skips it instead of throwing an error. With positive ARG move
-forward only one sexp, else move backwards."
- (let* ((arg (if (or (not arg) (> arg 0)) 1 -1))
- (paren-regexp
- (if (> arg 0) (gdscript-rx close-paren) (gdscript-rx open-paren)))
- (search-fn
- (if (> arg 0) #'re-search-forward #'re-search-backward)))
- (condition-case nil
- (gdscript-nav--lisp-forward-sexp arg)
- (error
- (while (and (funcall search-fn paren-regexp nil t)
- (gdscript-syntax-context 'paren)))))))
-
-(defun gdscript-nav--forward-sexp (&optional dir safe skip-parens-p)
- "Move to forward sexp.
-With positive optional argument DIR direction move forward, else
-backwards. When optional argument SAFE is non-nil do not throw
-errors when at end of sexp, skip it instead. With optional
-argument SKIP-PARENS-P force sexp motion to ignore parenthesized
-expressions when looking at them in either direction."
- (setq dir (or dir 1))
- (unless (= dir 0)
- (let* ((forward-p (if (> dir 0)
- (and (setq dir 1) t)
- (and (setq dir -1) nil)))
- (context-type (gdscript-syntax-context-type)))
- (cond
- ((memq context-type '(string comment))
- ;; Inside of a string, get out of it.
- (let ((forward-sexp-function))
- (forward-sexp dir)))
- ((and (not skip-parens-p)
- (or (eq context-type 'paren)
- (if forward-p
- (eq (syntax-class (syntax-after (point)))
- (car (string-to-syntax "(")))
- (eq (syntax-class (syntax-after (1- (point))))
- (car (string-to-syntax ")"))))))
- ;; Inside a paren or looking at it, lisp knows what to do.
- (if safe
- (gdscript-nav--lisp-forward-sexp-safe dir)
- (gdscript-nav--lisp-forward-sexp dir)))
- (t
- ;; This part handles the lispy feel of
- ;; `gdscript-nav-forward-sexp'. Knowing everything about the
- ;; current context and the context of the next sexp tries to
- ;; follow the lisp sexp motion commands in a symmetric manner.
- (let* ((context
- (cond
- ((gdscript-info-beginning-of-block-p) 'block-start)
- ((gdscript-info-end-of-block-p) 'block-end)
- ((gdscript-info-beginning-of-statement-p) 'statement-start)
- ((gdscript-info-end-of-statement-p) 'statement-end)))
- (next-sexp-pos
- (save-excursion
- (if safe
- (gdscript-nav--lisp-forward-sexp-safe dir)
- (gdscript-nav--lisp-forward-sexp dir))
- (point)))
- (next-sexp-context
- (save-excursion
- (goto-char next-sexp-pos)
- (cond
- ((gdscript-info-beginning-of-block-p) 'block-start)
- ((gdscript-info-end-of-block-p) 'block-end)
- ((gdscript-info-beginning-of-statement-p) 'statement-start)
- ((gdscript-info-end-of-statement-p) 'statement-end)
- ((gdscript-info-statement-starts-block-p) 'starts-block)
- ((gdscript-info-statement-ends-block-p) 'ends-block)))))
- (if forward-p
- (cond ((and (not (eobp))
- (gdscript-info-current-line-empty-p))
- (gdscript--util-forward-comment dir)
- (gdscript-nav--forward-sexp dir safe skip-parens-p))
- ((eq context 'block-start)
- (gdscript-nav-end-of-block))
- ((eq context 'statement-start)
- (gdscript-nav-end-of-statement))
- ((and (memq context '(statement-end block-end))
- (eq next-sexp-context 'ends-block))
- (goto-char next-sexp-pos)
- (gdscript-nav-end-of-block))
- ((and (memq context '(statement-end block-end))
- (eq next-sexp-context 'starts-block))
- (goto-char next-sexp-pos)
- (gdscript-nav-end-of-block))
- ((memq context '(statement-end block-end))
- (goto-char next-sexp-pos)
- (gdscript-nav-end-of-statement))
- (t (goto-char next-sexp-pos)))
- (cond ((and (not (bobp))
- (gdscript-info-current-line-empty-p))
- (gdscript--util-forward-comment dir)
- (gdscript-nav--forward-sexp dir safe skip-parens-p))
- ((eq context 'block-end)
- (gdscript-nav-beginning-of-block))
- ((eq context 'statement-end)
- (gdscript-nav-beginning-of-statement))
- ((and (memq context '(statement-start block-start))
- (eq next-sexp-context 'starts-block))
- (goto-char next-sexp-pos)
- (gdscript-nav-beginning-of-block))
- ((and (memq context '(statement-start block-start))
- (eq next-sexp-context 'ends-block))
- (goto-char next-sexp-pos)
- (gdscript-nav-beginning-of-block))
- ((memq context '(statement-start block-start))
- (goto-char next-sexp-pos)
- (gdscript-nav-beginning-of-statement))
- (t (goto-char next-sexp-pos))))))))))
-
-(defun gdscript-nav-forward-sexp (&optional arg safe skip-parens-p)
- "Move forward across expressions.
-With ARG, do it that many times. Negative arg -N means move
-backward N times. When optional argument SAFE is non-nil do not
-throw errors when at end of sexp, skip it instead. With optional
-argument SKIP-PARENS-P force sexp motion to ignore parenthesized
-expressions when looking at them in either direction (forced to t
-in interactive calls)."
- (interactive "^p")
- (or arg (setq arg 1))
- ;; Do not follow parens on interactive calls. This hack to detect
- ;; if the function was called interactively copes with the way
- ;; `forward-sexp' works by calling `forward-sexp-function', losing
- ;; interactive detection by checking `current-prefix-arg'. The
- ;; reason to make this distinction is that lisp functions like
- ;; `blink-matching-open' get confused causing issues like the one in
- ;; Bug#16191. With this approach the user gets a symmetric behavior
- ;; when working interactively while called functions expecting
- ;; paren-based sexp motion work just fine.
- (or
- skip-parens-p
- (setq skip-parens-p
- (memq real-this-command
- (list
- #'forward-sexp #'backward-sexp
- #'gdscript-nav-forward-sexp #'gdscript-nav-backward-sexp
- #'gdscript-nav-forward-sexp-safe #'gdscript-nav-backward-sexp))))
- (while (> arg 0)
- (gdscript-nav--forward-sexp 1 safe skip-parens-p)
- (setq arg (1- arg)))
- (while (< arg 0)
- (gdscript-nav--forward-sexp -1 safe skip-parens-p)
- (setq arg (1+ arg))))
-
-(defun gdscript-nav-backward-sexp (&optional arg safe skip-parens-p)
- "Move backward across expressions.
-With ARG, do it that many times. Negative arg -N means move
-forward N times. When optional argument SAFE is non-nil do not
-throw errors when at end of sexp, skip it instead. With optional
-argument SKIP-PARENS-P force sexp motion to ignore parenthesized
-expressions when looking at them in either direction (forced to t
-in interactive calls)."
- (interactive "^p")
- (or arg (setq arg 1))
- (gdscript-nav-forward-sexp (- arg) safe skip-parens-p))
-
-(defun gdscript-nav-forward-sexp-safe (&optional arg skip-parens-p)
- "Move forward safely across expressions.
-With ARG, do it that many times. Negative arg -N means move
-backward N times. With optional argument SKIP-PARENS-P force
-sexp motion to ignore parenthesized expressions when looking at
-them in either direction (forced to t in interactive calls)."
- (interactive "^p")
- (gdscript-nav-forward-sexp arg t skip-parens-p))
-
-(defun gdscript-nav-backward-sexp-safe (&optional arg skip-parens-p)
- "Move backward safely across expressions.
-With ARG, do it that many times. Negative arg -N means move
-forward N times. With optional argument SKIP-PARENS-P force sexp
-motion to ignore parenthesized expressions when looking at them in
-either direction (forced to t in interactive calls)."
- (interactive "^p")
- (gdscript-nav-backward-sexp arg t skip-parens-p))
-
-(defun gdscript-nav--up-list (&optional dir)
- "Internal implementation of `gdscript-nav-up-list'.
-DIR is always 1 or -1 and comes sanitized from
-`gdscript-nav-up-list' calls."
- (let ((context (gdscript-syntax-context-type))
- (forward-p (> dir 0)))
- (cond
- ((memq context '(string comment)))
- ((eq context 'paren)
- (let ((forward-sexp-function))
- (up-list dir)))
- ((and forward-p (gdscript-info-end-of-block-p))
- (let ((parent-end-pos
- (save-excursion
- (let ((indentation (and
- (gdscript-nav-beginning-of-block)
- (current-indentation))))
- (while (and indentation
- (> indentation 0)
- (>= (current-indentation) indentation)
- (gdscript-nav-backward-block)))
- (gdscript-nav-end-of-block)))))
- (and (> (or parent-end-pos (point)) (point))
- (goto-char parent-end-pos))))
- (forward-p (gdscript-nav-end-of-block))
- ((and (not forward-p)
- (> (current-indentation) 0)
- (gdscript-info-beginning-of-block-p))
- (let ((prev-block-pos
- (save-excursion
- (let ((indentation (current-indentation)))
- (while (and (gdscript-nav-backward-block)
- (>= (current-indentation) indentation))))
- (point))))
- (and (> (point) prev-block-pos)
- (goto-char prev-block-pos))))
- ((not forward-p) (gdscript-nav-beginning-of-block)))))
-
-(defun gdscript-nav-up-list (&optional arg)
- "Move forward out of one level of parentheses (or blocks).
-With ARG, do this that many times.
-A negative argument means move backward but still to a less deep spot.
-This command assumes point is not in a string or comment."
- (interactive "^p")
- (or arg (setq arg 1))
- (while (> arg 0)
- (gdscript-nav--up-list 1)
- (setq arg (1- arg)))
- (while (< arg 0)
- (gdscript-nav--up-list -1)
- (setq arg (1+ arg))))
-
-(defun gdscript-nav-backward-up-list (&optional arg)
- "Move backward out of one level of parentheses (or blocks).
-With ARG, do this that many times.
-A negative argument means move forward but still to a less deep spot.
-This command assumes point is not in a string or comment."
- (interactive "^p")
- (or arg (setq arg 1))
- (gdscript-nav-up-list (- arg)))
-
-(provide 'gdscript-indent-and-nav)
-;;; gdscript-indent-and-nav.el ends here
+;;; gdscript-indent-and-nav.el --- Syntax highlighting for GDScript -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2026 GDQuest and contributors
+
+;; Author: Nathan Lovato
+;; Fabián E. Gallina
+;; Maintainer: Jen-Chieh Shen
+
+;; This program 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.
+
+;; This program 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 this program. If not, see .
+
+;;; Commentary:
+;;
+;; Functions copies from Emacs 27's built-in python.el to handle indents,
+;; querying info about the language's symbols or the context at point, and for
+;; navigation.
+;; There are dependencies between the *indent*, *info*, and *nav* functions that prevent us from splitting them into separate .el files at the moment.
+;;
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'seq)
+
+(require 'gdscript-customization)
+(require 'gdscript-utils)
+(require 'gdscript-rx)
+(require 'gdscript-syntax)
+
+(defvar gdscript-nav-beginning-of-defun-regexp
+ (gdscript-rx line-start (* space) defun (+ space) (group symbol-name))
+ "Regexp matching class or function definition.
+The name of the defun should be grouped so it can be retrieved
+via `match-string'.")
+
+(defun gdscript-indent-context ()
+ "Get information about the current indentation context.
+Context is returned in a cons with the form (STATUS . START).
+
+STATUS can be one of the following:
+
+keyword
+-------
+
+:after-comment
+ - Point is after a comment line.
+ - START is the position of the \"#\" character.
+:inside-string
+ - Point is inside string.
+ - START is the position of the first quote that starts it.
+:no-indent
+ - No possible indentation case matches.
+ - START is always zero.
+
+:inside-paren
+ - Fallback case when point is inside paren.
+ - START is the first non space char position *after* the open paren.
+:inside-paren-at-closing-nested-paren
+ - Point is on a line that contains a nested paren closer.
+ - START is the position of the open paren it closes.
+:inside-paren-at-closing-paren
+ - Point is on a line that contains a paren closer.
+ - START is the position of the open paren.
+:inside-paren-newline-start
+ - Point is inside a paren with items starting in their own line.
+ - START is the position of the open paren.
+:inside-paren-newline-start-from-block
+ - Point is inside a paren with items starting in their own line
+ from a block start.
+ - START is the position of the open paren.
+
+:after-backslash
+ - Fallback case when point is after backslash.
+ - START is the char after the position of the backslash.
+:after-backslash-assignment-continuation
+ - Point is after a backslashed assignment.
+ - START is the char after the position of the backslash.
+:after-backslash-block-continuation
+ - Point is after a backslashed block continuation.
+ - START is the char after the position of the backslash.
+:after-backslash-dotted-continuation
+ - Point is after a backslashed dotted continuation. Previous
+ line must contain a dot to align with.
+ - START is the char after the position of the backslash.
+:after-backslash-first-line
+ - First line following a backslashed continuation.
+ - START is the char after the position of the backslash.
+
+:after-block-end
+ - Point is after a line containing a block ender.
+ - START is the position where the ender starts.
+:after-block-start
+ - Point is after a line starting a block.
+ - START is the position where the block starts.
+:after-line
+ - Point is after a simple line.
+ - START is the position where the previous line starts.
+:at-dedenter-block-start
+ - Point is on a line starting a dedenter block.
+ - START is the position where the dedenter block starts."
+ (let ((ppss (save-excursion
+ (beginning-of-line)
+ (syntax-ppss))))
+ (cond
+ ;; Beginning of buffer.
+ ((= (line-number-at-pos) 1)
+ (cons :no-indent 0))
+ ;; Inside a string.
+ ((let ((start (gdscript-syntax-context 'string ppss)))
+ (when start
+ (cons :inside-string start))))
+ ;; Inside a paren.
+ ((let* ((start (gdscript-syntax-context 'paren ppss))
+ (starts-in-newline
+ (when start
+ (save-excursion
+ (goto-char start)
+ (forward-char)
+ (not
+ (= (line-number-at-pos)
+ (progn
+ (gdscript--util-forward-comment)
+ (line-number-at-pos))))))))
+ (when start
+ (cond
+ ;; Current line only holds the closing paren.
+ ((save-excursion
+ (skip-syntax-forward " ")
+ (when (and (gdscript-syntax-closing-paren-p)
+ (progn
+ (forward-char 1)
+ (not (gdscript-syntax-context 'paren))))
+ (cons :inside-paren-at-closing-paren start))))
+ ;; Current line only holds a closing paren for nested.
+ ((save-excursion
+ (back-to-indentation)
+ (gdscript-syntax-closing-paren-p))
+ (cons :inside-paren-at-closing-nested-paren start))
+ ;; This line starts from an opening block in its own line.
+ ((save-excursion
+ (goto-char start)
+ (when (and
+ starts-in-newline
+ (save-excursion
+ (back-to-indentation)
+ (looking-at (gdscript-rx block-start))))
+ (cons
+ :inside-paren-newline-start-from-block start))))
+ (starts-in-newline
+ (cons :inside-paren-newline-start start))
+ ;; General case.
+ (t (cons :inside-paren
+ (save-excursion
+ (goto-char (1+ start))
+ (skip-syntax-forward "(" 1)
+ (skip-syntax-forward " ")
+ (point))))))))
+ ;; After backslash.
+ ((let ((start (unless (gdscript-syntax-comment-or-string-p (syntax-ppss))
+ (gdscript-info-line-ends-backslash-p
+ (1- (line-number-at-pos))))))
+ (when start
+ (cond
+ ;; Continuation of dotted expression.
+ ((save-excursion
+ (back-to-indentation)
+ (when (eq (char-after) ?\.)
+ ;; Move point back until it's not inside a paren.
+ (while (prog2
+ (forward-line -1)
+ (and (not (bobp))
+ (gdscript-syntax-context 'paren))))
+ (goto-char (line-end-position))
+ (while (and (search-backward
+ "." (line-beginning-position) t)
+ (gdscript-syntax-context-type)))
+ ;; Ensure previous statement has dot to align with.
+ (when (and (eq (char-after) ?\.)
+ (not (gdscript-syntax-context-type)))
+ (cons :after-backslash-dotted-continuation (point))))))
+ ;; Continuation of block definition.
+ ((let ((block-continuation-start
+ (gdscript-info-block-continuation-line-p)))
+ (when block-continuation-start
+ (save-excursion
+ (goto-char block-continuation-start)
+ (re-search-forward
+ (gdscript-rx block-start (* space))
+ (line-end-position) t)
+ (cons :after-backslash-block-continuation (point))))))
+ ;; Continuation of assignment.
+ ((let ((assignment-continuation-start
+ (gdscript-info-assignment-continuation-line-p)))
+ (when assignment-continuation-start
+ (save-excursion
+ (goto-char assignment-continuation-start)
+ (cons :after-backslash-assignment-continuation (point))))))
+ ;; First line after backslash continuation start.
+ ((save-excursion
+ (goto-char start)
+ (when (or (= (line-number-at-pos) 1)
+ (not (gdscript-info-beginning-of-backslash
+ (1- (line-number-at-pos)))))
+ (cons :after-backslash-first-line start))))
+ ;; General case.
+ (t (cons :after-backslash start))))))
+ ;; After beginning of block.
+ ((let ((start (save-excursion
+ (back-to-indentation)
+ (gdscript--util-forward-comment -1)
+ (when (equal (char-before) ?:)
+ (gdscript-nav-beginning-of-block)))))
+ (when start
+ (cons :after-block-start start))))
+ ;; At dedenter statement.
+ ((let ((start (gdscript-info-dedenter-statement-p)))
+ (when start
+ (cons :at-dedenter-block-start start))))
+ ;; After normal line, comment or ender (default case).
+ ((save-excursion
+ (back-to-indentation)
+ (skip-chars-backward " \t\n")
+ (if (bobp)
+ (cons :no-indent 0)
+ (gdscript-nav-beginning-of-statement)
+ (cons
+ (cond ((gdscript-info-current-line-comment-p)
+ :after-comment)
+ ((save-excursion
+ (goto-char (line-end-position))
+ (gdscript--util-forward-comment -1)
+ (gdscript-nav-beginning-of-statement)
+ (looking-at (gdscript-rx block-ender)))
+ :after-block-end)
+ (t :after-line))
+ (point))))))))
+
+(defun gdscript-indent--calculate-indentation ()
+ "Internal implementation of `gdscript-indent-calculate-indentation'.
+May return an integer for the maximum possible indentation at
+current context or a list of integers. The latter case is only
+happening for :at-dedenter-block-start context since the
+possibilities can be narrowed to specific indentation points."
+ (save-excursion
+ (pcase (gdscript-indent-context)
+ (`(:no-indent . ,_) (prog-first-column)) ; usually 0
+ (`(,(or :after-line
+ :after-comment
+ :inside-string
+ :after-backslash) . ,start)
+ ;; Copy previous indentation.
+ (goto-char start)
+ (current-indentation))
+ (`(,(or :inside-paren-at-closing-paren
+ :inside-paren-at-closing-nested-paren) . ,start)
+ (goto-char (+ 1 start))
+ (if (looking-at "[ \t]*\\(?:#\\|$\\)")
+ ;; Copy previous indentation.
+ (current-indentation)
+ ;; Align with opening paren.
+ (current-column)))
+ (`(,(or :after-block-start
+ :after-backslash-first-line
+ :inside-paren-newline-start) . ,start)
+ ;; Add one indentation level.
+ (goto-char start)
+ (+ (current-indentation) gdscript-indent-offset))
+ (`(,(or :inside-paren
+ :after-backslash-block-continuation
+ :after-backslash-dotted-continuation
+ :after-backslash-assignment-continuation) . ,start)
+ ;; Use (possibly extra) indentation given by the configuration
+ (goto-char start)
+ (+ (current-indentation)
+ (* gdscript-indent-offset
+ gdscript-indent-line-continuation-scale)))
+ (`(:after-block-end . ,start)
+ ;; Subtract one indentation level.
+ (goto-char start)
+ (- (current-indentation) gdscript-indent-offset))
+ (`(:at-dedenter-block-start . ,_)
+ ;; List all possible indentation levels from opening blocks.
+ (let ((opening-block-start-points
+ (gdscript-info-dedenter-opening-block-positions)))
+ (if (not opening-block-start-points)
+ (prog-first-column) ; if not found default to first column
+ (mapcar (lambda (pos)
+ (save-excursion
+ (goto-char pos)
+ (current-indentation)))
+ opening-block-start-points))))
+ (`(,(or :inside-paren-newline-start-from-block) . ,start)
+ (goto-char start)
+ (+ (current-indentation)
+ (* gdscript-indent-offset gdscript-indent-def-block-scale))))))
+
+(defun gdscript-indent--calculate-levels (indentation)
+ "Calculate levels list given INDENTATION.
+Argument INDENTATION can either be an integer or a list of
+integers. Levels are returned in ascending order, and in the
+case INDENTATION is a list, this order is enforced."
+ (if (listp indentation)
+ (sort (copy-sequence indentation) #'<)
+ (nconc (number-sequence (prog-first-column) (1- indentation)
+ gdscript-indent-offset)
+ (list indentation))))
+
+(defun gdscript-indent--previous-level (levels indentation)
+ "Return previous level from LEVELS relative to INDENTATION."
+ (let* ((levels (sort (copy-sequence levels) #'>))
+ (default (car levels)))
+ (catch 'return
+ (dolist (level levels)
+ (when (funcall #'< level indentation)
+ (throw 'return level)))
+ default)))
+
+(defun gdscript-indent-calculate-indentation (&optional previous)
+ "Calculate indentation.
+Get indentation of PREVIOUS level when argument is non-nil.
+Return the max level of the cycle when indentation reaches the
+minimum."
+ (let* ((indentation (gdscript-indent--calculate-indentation))
+ (levels (gdscript-indent--calculate-levels indentation)))
+ (if previous
+ (gdscript-indent--previous-level levels (current-indentation))
+ (if levels
+ (apply #'max levels)
+ (prog-first-column)))))
+
+(defun gdscript-indent-line (&optional previous)
+ "Internal implementation of `gdscript-indent-line-function'.
+Use the PREVIOUS level when argument is non-nil, otherwise indent
+to the maximum available level. When indentation is the minimum
+possible and PREVIOUS is non-nil, cycle back to the maximum
+level."
+ (let ((follow-indentation-p
+ (<= (current-column) (current-indentation))))
+ (save-excursion
+ (indent-line-to
+ (gdscript-indent-calculate-indentation previous))
+ (gdscript-info-dedenter-opening-block-message))
+ (when follow-indentation-p
+ (back-to-indentation))))
+
+(defun gdscript-indent-calculate-levels ()
+ "Return possible indentation levels."
+ (gdscript-indent--calculate-levels
+ (gdscript-indent--calculate-indentation)))
+
+(defun gdscript-indent-line-function ()
+ "`indent-line-function' for Gdscript mode.
+When the variable `last-command' is equal to one of the symbols
+inside `gdscript-indent-trigger-commands' it cycles possible
+indentation levels from right to left."
+ (gdscript-indent-line
+ (and (memq this-command gdscript-indent-trigger-commands)
+ (eq last-command this-command))))
+
+(defun gdscript-indent-dedent-line ()
+ "De-indent current line."
+ (interactive "*")
+ (when (and (not (bolp))
+ (not (gdscript-syntax-comment-or-string-p))
+ (= (current-indentation) (current-column)))
+ (gdscript-indent-line t)
+ t))
+
+(defun gdscript-indent-dedent-line-backspace (arg)
+ "De-indent current line.
+Argument ARG is passed to `backward-delete-char-untabify' when
+point is not in between the indentation."
+ (interactive "*p")
+ (unless (gdscript-indent-dedent-line)
+ (backward-delete-char-untabify arg)))
+
+(put 'gdscript-indent-dedent-line-backspace 'delete-selection 'supersede)
+
+(defun gdscript-indent-region (start end)
+ "Indent a Gdscript region automagically.
+
+Called from a program, START and END specify the region to indent."
+ (let ((deactivate-mark nil))
+ (save-excursion
+ (goto-char end)
+ (setq end (point-marker))
+ (goto-char start)
+ (or (bolp) (forward-line 1))
+ (while (< (point) end)
+ (or (and (bolp) (eolp))
+ (when (and
+ ;; Skip if previous line is empty or a comment.
+ (save-excursion
+ (let ((line-is-comment-p
+ (gdscript-info-current-line-comment-p)))
+ (forward-line -1)
+ (not
+ (or (and (gdscript-info-current-line-comment-p)
+ ;; Unless this line is a comment too.
+ (not line-is-comment-p))
+ (gdscript-info-current-line-empty-p)))))
+ ;; Skip if current line is a block start, a
+ ;; dedenter or block ender.
+ (save-excursion
+ (back-to-indentation)
+ (not (looking-at
+ (gdscript-rx
+ (or block-start dedenter block-ender))))))
+ (gdscript-indent-line)))
+ (forward-line 1))
+ (move-marker end nil))))
+
+(defun gdscript-indent-shift-left (start end &optional count)
+ "Shift lines contained in region START END by COUNT columns to the left.
+COUNT defaults to `gdscript-indent-offset'. If region isn't
+active, the current line is shifted. The shifted region includes
+the lines in which START and END lie. An error is signaled if
+any lines in the region are indented less than COUNT columns."
+ (interactive
+ (if mark-active
+ (list (region-beginning) (region-end) current-prefix-arg)
+ (list (line-beginning-position) (line-end-position) current-prefix-arg)))
+ (if count
+ (setq count (prefix-numeric-value count))
+ (setq count gdscript-indent-offset))
+ (when (> count 0)
+ (let ((deactivate-mark nil))
+ (save-excursion
+ (goto-char start)
+ (while (< (point) end)
+ (if (and (< (current-indentation) count)
+ (not (looking-at "[ \t]*$")))
+ (user-error "Can't shift all lines enough"))
+ (forward-line))
+ (indent-rigidly start end (- count))))))
+
+(defun gdscript-indent-shift-right (start end &optional count)
+ "Shift lines contained in region START END by COUNT columns to the right.
+COUNT defaults to `gdscript-indent-offset'. If region isn't
+active, the current line is shifted. The shifted region includes
+the lines in which START and END lie."
+ (interactive
+ (if mark-active
+ (list (region-beginning) (region-end) current-prefix-arg)
+ (list (line-beginning-position) (line-end-position) current-prefix-arg)))
+ (let ((deactivate-mark nil))
+ (setq count (if count (prefix-numeric-value count)
+ gdscript-indent-offset))
+ (indent-rigidly start end count)))
+
+(defun gdscript-indent-post-self-insert-function ()
+ "Adjust indentation after insertion of some characters.
+This function is intended to be added to `post-self-insert-hook.'
+If a line renders a paren alone, after adding a char before it,
+the line will be re-indented automatically if needed."
+ (when (and electric-indent-mode
+ (eq (char-before) last-command-event)
+ (not (gdscript-syntax-context 'string))
+ (save-excursion
+ (beginning-of-line)
+ (not (gdscript-syntax-context 'string (syntax-ppss)))))
+ (cond
+ ;; Electric indent inside parens
+ ((and
+ (not (bolp))
+ (let ((paren-start (gdscript-syntax-context 'paren)))
+ ;; Check that point is inside parens.
+ (when paren-start
+ (not
+ ;; Filter the case where input is happening in the same
+ ;; line where the open paren is.
+ (= (line-number-at-pos)
+ (line-number-at-pos paren-start)))))
+ ;; When content has been added before the closing paren or a
+ ;; comma has been inserted, it's ok to do the trick.
+ (or
+ (memq (char-after) '(?\) ?\] ?\}))
+ (eq (char-before) ?,)))
+ (save-excursion
+ (goto-char (line-beginning-position))
+ (let ((indentation (gdscript-indent-calculate-indentation)))
+ (when (and (numberp indentation) (< (current-indentation) indentation))
+ (indent-line-to indentation)))))
+ ;; Electric colon
+ ((and (eq ?: last-command-event)
+ (memq ?: electric-indent-chars)
+ (not current-prefix-arg)
+ ;; Trigger electric colon only at end of line
+ (eolp)
+ ;; Avoid re-indenting on extra colon
+ (not (equal ?: (char-before (1- (point)))))
+ (not (gdscript-syntax-comment-or-string-p)))
+ ;; Just re-indent dedenters
+ (let ((dedenter-pos (gdscript-info-dedenter-statement-p)))
+ (when dedenter-pos
+ (let ((start (copy-marker dedenter-pos))
+ (end (point-marker)))
+ (save-excursion
+ (goto-char start)
+ (gdscript-indent-line)
+ (unless (= (line-number-at-pos start)
+ (line-number-at-pos end))
+ ;; Reindent region if this is a multiline statement
+ (gdscript-indent-region start end))))))))))
+
+;;; Misc helpers
+(defun gdscript-info-current-defun (&optional include-type)
+ "Return name of surrounding function with Gdscript compatible dotty syntax.
+Optional argument INCLUDE-TYPE indicates to include the type of the defun.
+This function can be used as the value of `add-log-current-defun-function'
+since it returns nil if point is not inside a defun."
+ (save-restriction
+ (widen)
+ (save-excursion
+ (end-of-line 1)
+ (let ((names)
+ (starting-indentation (current-indentation))
+ (starting-pos (point))
+ (first-run t)
+ (last-indent)
+ (type))
+ (catch 'exit
+ (while (gdscript-nav-beginning-of-defun 1)
+ (when (save-match-data
+ (and
+ (or (not last-indent)
+ (< (current-indentation) last-indent))
+ (or
+ (and first-run
+ (save-excursion
+ ;; If this is the first run, we may add
+ ;; the current defun at point.
+ (setq first-run nil)
+ (goto-char starting-pos)
+ (gdscript-nav-beginning-of-statement)
+ (beginning-of-line 1)
+ (looking-at-p
+ gdscript-nav-beginning-of-defun-regexp)))
+ (< starting-pos
+ (save-excursion
+ (let ((min-indent
+ (+ (current-indentation)
+ gdscript-indent-offset)))
+ (if (< starting-indentation min-indent)
+ ;; If the starting indentation is not
+ ;; within the min defun indent make the
+ ;; check fail.
+ starting-pos
+ ;; Else go to the end of defun and add
+ ;; up the current indentation to the
+ ;; ending position.
+ (gdscript-nav-end-of-defun)
+ (+ (point)
+ (if (>= (current-indentation) min-indent)
+ (1+ (current-indentation))
+ 0)))))))))
+ (save-match-data (setq last-indent (current-indentation)))
+ (if (or (not include-type) type)
+ (setq names (cons (match-string-no-properties 1) names))
+ (let ((match (split-string (match-string-no-properties 0))))
+ (setq type (car match))
+ (setq names (cons (cadr match) names)))))
+ ;; Stop searching ASAP.
+ (and (= (current-indentation) 0) (throw 'exit t))))
+ (and names
+ (concat (and type (format "%s " type))
+ (mapconcat #'identity names ".")))))))
+
+(defun gdscript-info-current-symbol (&optional replace-self)
+ "Return current symbol using dotty syntax.
+With optional argument REPLACE-SELF convert \"self\" to current
+parent defun name."
+ (let ((name
+ (and (not (gdscript-syntax-comment-or-string-p))
+ (with-syntax-table gdscript-dotty-syntax-table
+ (let ((sym (symbol-at-point)))
+ (and sym
+ (substring-no-properties (symbol-name sym))))))))
+ (when name
+ (if (not replace-self)
+ name
+ (let ((current-defun (gdscript-info-current-defun)))
+ (if (not current-defun)
+ name
+ (replace-regexp-in-string
+ (gdscript-rx line-start word-start "self" word-end ?.)
+ (concat
+ (mapconcat 'identity
+ (butlast (split-string current-defun "\\."))
+ ".") ".")
+ name)))))))
+
+(defun gdscript-info-statement-starts-block-p ()
+ "Return non-nil if current statement opens a block."
+ (save-excursion
+ (gdscript-nav-beginning-of-statement)
+ (looking-at (gdscript-rx block-start))))
+
+(defun gdscript-info-statement-ends-block-p ()
+ "Return non-nil if point is at end of block."
+ (let ((end-of-block-pos (save-excursion
+ (gdscript-nav-end-of-block)))
+ (end-of-statement-pos (save-excursion
+ (gdscript-nav-end-of-statement))))
+ (and end-of-block-pos end-of-statement-pos
+ (= end-of-block-pos end-of-statement-pos))))
+
+(defun gdscript-info-beginning-of-statement-p ()
+ "Return non-nil if point is at beginning of statement."
+ (= (point) (save-excursion
+ (gdscript-nav-beginning-of-statement)
+ (point))))
+
+(defun gdscript-info-end-of-statement-p ()
+ "Return non-nil if point is at end of statement."
+ (= (point) (save-excursion
+ (gdscript-nav-end-of-statement)
+ (point))))
+
+(defun gdscript-info-beginning-of-block-p ()
+ "Return non-nil if point is at beginning of block."
+ (and (gdscript-info-beginning-of-statement-p)
+ (gdscript-info-statement-starts-block-p)))
+
+(defun gdscript-info-end-of-block-p ()
+ "Return non-nil if point is at end of block."
+ (and (gdscript-info-end-of-statement-p)
+ (gdscript-info-statement-ends-block-p)))
+
+(defun gdscript-info-dedenter-opening-block-position ()
+ "Return the point of the closest block the current line closes.
+Returns nil if point is not on a dedenter statement or no opening
+block can be detected. The latter case meaning current file is
+likely an invalid gdscript file."
+ (let ((positions (gdscript-info-dedenter-opening-block-positions))
+ (indentation (current-indentation))
+ (position))
+ (while (and (not position)
+ positions)
+ (save-excursion
+ (goto-char (car positions))
+ (if (<= (current-indentation) indentation)
+ (setq position (car positions))
+ (setq positions (cdr positions)))))
+ position))
+
+(defun gdscript-indent--nav-block ()
+ "Search backward for block start."
+ (re-search-backward (gdscript-rx block-start) nil t)
+ ;; At this point `(match-string-no-properties 0)' doesn't match whole gdscript's block expression,
+ ;; due to how `re-search-backward' works.
+ ;; To make `(match-string-no-properties 0)' match whole gdscript's block expression
+ ;; let's use `re-search-forward' from beginning of a statement.
+ (gdscript-nav-beginning-of-statement)
+ (prog1
+ (re-search-forward (gdscript-rx block-start) nil t)
+ ;; And let's put point at the beginning of the statement.
+ (gdscript-nav-beginning-of-statement)))
+
+(defun gdscript-info-dedenter-opening-block-positions ()
+ "Return points of blocks the current line may close sorted by closer.
+Returns nil if point is not on a dedenter statement or no opening
+block can be detected. The latter case meaning current file is
+likely an invalid gdscript file."
+ (save-excursion
+ (let ((dedenter-pos (gdscript-info-dedenter-statement-p)))
+ (when dedenter-pos
+ (goto-char dedenter-pos)
+ (let* ((cur-line (line-beginning-position))
+ (pairs '(("elif" "elif" "if")
+ ("else" "if" "elif")))
+ (dedenter (match-string-no-properties 0))
+ (possible-opening-blocks (cdr (assoc-string dedenter pairs)))
+ (collected-indentations)
+ (opening-blocks))
+ (catch 'exit
+ (while (gdscript-nav--syntactically #'gdscript-indent--nav-block #'<)
+ (let ((indentation (current-indentation)))
+ (when (and (not (memq indentation collected-indentations))
+ (or (not collected-indentations)
+ (< indentation (apply #'min collected-indentations)))
+ ;; There must be no line with indentation
+ ;; smaller than `indentation' (except for
+ ;; blank lines) between the found opening
+ ;; block and the current line, otherwise it
+ ;; is not an opening block.
+ (save-excursion
+ (forward-line)
+ (let ((no-back-indent t))
+ (save-match-data
+ (while (and (< (point) cur-line)
+ (setq no-back-indent
+ (or (> (current-indentation) indentation)
+ (gdscript-info-current-line-empty-p)
+ (gdscript-info-current-line-comment-p)
+ (not (equal :after-line (car (gdscript-indent-context)))))))
+ (forward-line)))
+ no-back-indent)))
+ (setq collected-indentations
+ (cons indentation collected-indentations))
+ (when
+ (seq-contains-p possible-opening-blocks
+ (string-trim (match-string-no-properties 0))
+ (lambda (elt e) (string-prefix-p elt e)))
+ (setq opening-blocks (cons (point) opening-blocks))))
+ (when (zerop indentation)
+ (throw 'exit nil)))))
+ ;; sort by closer
+ (nreverse opening-blocks))))))
+
+(defun gdscript-info-dedenter-opening-block-message ()
+ "Message the first line of the block the current statement closes."
+ (let ((point (gdscript-info-dedenter-opening-block-position)))
+ (when point
+ (message "Closes %s" (save-excursion
+ (goto-char point)
+ (buffer-substring
+ (point) (line-end-position)))))))
+
+(defun gdscript-info-dedenter-statement-p ()
+ "Return point if current statement is a dedenter.
+Sets `match-data' to the keyword that starts the dedenter
+statement."
+ (save-excursion
+ (gdscript-nav-beginning-of-statement)
+ (when (and (not (gdscript-syntax-context-type))
+ (looking-at (gdscript-rx dedenter)))
+ (point))))
+
+(defun gdscript-info-line-ends-backslash-p (&optional line-number)
+ "Return non-nil if current line ends with backslash.
+With optional argument LINE-NUMBER, check that line instead."
+ (save-excursion
+ (when line-number
+ (gdscript--util-goto-line line-number))
+ (while (and (not (eobp))
+ (goto-char (line-end-position))
+ (gdscript-syntax-context 'paren)
+ (not (equal (char-before (point)) ?\\)))
+ (forward-line 1))
+ (when (equal (char-before) ?\\)
+ (point-marker))))
+
+(defun gdscript-info-beginning-of-backslash (&optional line-number)
+ "Return the point where the backslashed line start.
+Optional argument LINE-NUMBER forces the line number to check against."
+ (save-excursion
+ (when line-number
+ (gdscript--util-goto-line line-number))
+ (when (gdscript-info-line-ends-backslash-p)
+ (while (save-excursion
+ (goto-char (line-beginning-position))
+ (gdscript-syntax-context 'paren))
+ (forward-line -1))
+ (back-to-indentation)
+ (point-marker))))
+
+(defun gdscript-info-continuation-line-p ()
+ "Check if current line is continuation of another.
+When current line is continuation of another return the point
+where the continued line ends."
+ (save-excursion
+ (let* ((context-type (progn
+ (back-to-indentation)
+ (gdscript-syntax-context-type)))
+ (line-start (line-number-at-pos))
+ (context-start (when context-type
+ (gdscript-syntax-context context-type))))
+ (cond ((equal context-type 'paren)
+ ;; Lines inside a paren are always a continuation line
+ ;; (except the first one).
+ (gdscript--util-forward-comment -1)
+ (point-marker))
+ ((member context-type '(string comment))
+ ;; move forward an roll again
+ (goto-char context-start)
+ (gdscript--util-forward-comment)
+ (gdscript-info-continuation-line-p))
+ (t
+ ;; Not within a paren, string or comment, the only way
+ ;; we are dealing with a continuation line is that
+ ;; previous line contains a backslash, and this can
+ ;; only be the previous line from current
+ (back-to-indentation)
+ (gdscript--util-forward-comment -1)
+ (when (and (equal (1- line-start) (line-number-at-pos))
+ (gdscript-info-line-ends-backslash-p))
+ (point-marker)))))))
+
+(defun gdscript-info-block-continuation-line-p ()
+ "Return non-nil if current line is a continuation of a block."
+ (save-excursion
+ (when (gdscript-info-continuation-line-p)
+ (forward-line -1)
+ (back-to-indentation)
+ (when (looking-at (gdscript-rx block-start))
+ (point-marker)))))
+
+(defun gdscript-info-assignment-statement-p (&optional current-line-only)
+ "Check if current line is an assignment.
+With argument CURRENT-LINE-ONLY is non-nil, don't follow any
+continuations, just check the if current line is an assignment."
+ (save-excursion
+ (let ((found nil))
+ (if current-line-only
+ (back-to-indentation)
+ (gdscript-nav-beginning-of-statement))
+ (while (and
+ (re-search-forward (gdscript-rx not-simple-operator
+ assignment-operator
+ (group not-simple-operator))
+ (line-end-position) t)
+ (not found))
+ (save-excursion
+ ;; The assignment operator should not be inside a string.
+ (backward-char (length (match-string-no-properties 1)))
+ (setq found (not (gdscript-syntax-context-type)))))
+ (when found
+ (skip-syntax-forward " ")
+ (point-marker)))))
+
+;; TODO: rename to clarify this is only for the first continuation
+;; line or remove it and move its body to `gdscript-indent-context'.
+(defun gdscript-info-assignment-continuation-line-p ()
+ "Check if current line is the first continuation of an assignment.
+When current line is continuation of another with an assignment
+return the point of the first non-blank character after the
+operator."
+ (save-excursion
+ (when (gdscript-info-continuation-line-p)
+ (forward-line -1)
+ (gdscript-info-assignment-statement-p t))))
+
+(defun gdscript-info-looking-at-beginning-of-defun (&optional syntax-ppss)
+ "Check if point is at `beginning-of-defun' using SYNTAX-PPSS."
+ (and (not (gdscript-syntax-context-type (or syntax-ppss (syntax-ppss))))
+ (save-excursion
+ (beginning-of-line 1)
+ (looking-at gdscript-nav-beginning-of-defun-regexp))))
+
+(defun gdscript-info-current-line-comment-p ()
+ "Return non-nil if current line is a comment line."
+ (char-equal
+ (or (char-after (+ (line-beginning-position) (/ (current-indentation) gdscript-indent-offset))) ?_)
+ ?#))
+
+(defun gdscript-info-current-line-empty-p ()
+ "Return non-nil if current line is empty, ignoring whitespace."
+ (save-excursion
+ (beginning-of-line 1)
+ (looking-at
+ (gdscript-rx line-start (* whitespace)
+ (group (* not-newline))
+ (* whitespace) line-end))
+ (string-equal "" (match-string-no-properties 1))))
+
+;;; Navigation
+
+(defun gdscript-nav--beginning-of-defun (&optional arg)
+ "Internal implementation of `gdscript-nav-beginning-of-defun'.
+With positive ARG search backwards, else search forwards."
+ (when (or (null arg) (= arg 0)) (setq arg 1))
+ (let* ((re-search-fn (if (> arg 0)
+ #'re-search-backward
+ #'re-search-forward))
+ (line-beg-pos (line-beginning-position))
+ (line-content-start (+ line-beg-pos (current-indentation)))
+ (pos (point-marker))
+ (beg-indentation
+ (and (> arg 0)
+ (save-excursion
+ (while (and
+ (not (gdscript-info-looking-at-beginning-of-defun))
+ (gdscript-nav-backward-block)))
+ (or (and (gdscript-info-looking-at-beginning-of-defun)
+ (+ (current-indentation) gdscript-indent-offset))
+ 0))))
+ (found
+ (progn
+ (when (and (< arg 0)
+ (gdscript-info-looking-at-beginning-of-defun))
+ (end-of-line 1))
+ (while (and (funcall re-search-fn
+ gdscript-nav-beginning-of-defun-regexp nil t)
+ (or (gdscript-syntax-context-type)
+ ;; Handle nested defuns when moving
+ ;; backwards by checking indentation.
+ (and (> arg 0)
+ (not (= (current-indentation) 0))
+ (>= (current-indentation) beg-indentation)))))
+ (and (gdscript-info-looking-at-beginning-of-defun)
+ (or (not (= (line-number-at-pos pos)
+ (line-number-at-pos)))
+ (and (>= (point) line-beg-pos)
+ (<= (point) line-content-start)
+ (> pos line-content-start)))))))
+ (if found
+ (or (beginning-of-line 1) t)
+ (and (goto-char pos) nil))))
+
+(defun gdscript-nav-beginning-of-defun (&optional arg)
+ "Move point to `beginning-of-defun'.
+With positive ARG search backwards else search forward.
+ARG nil or 0 defaults to 1. When searching backwards,
+nested defuns are handled with care depending on current
+point position. Return non-nil if point is moved to
+`beginning-of-defun'."
+ (when (or (null arg) (= arg 0)) (setq arg 1))
+ (let ((found))
+ (while (and (not (= arg 0))
+ (let ((keep-searching-p
+ (gdscript-nav--beginning-of-defun arg)))
+ (when (and keep-searching-p (null found))
+ (setq found t))
+ keep-searching-p))
+ (setq arg (if (> arg 0) (1- arg) (1+ arg))))
+ found))
+
+(defun gdscript-nav-end-of-defun ()
+ "Move point to the end of def or class.
+Returns nil if point is not in a def or class."
+ (interactive)
+ (let ((beg-defun-indent)
+ (beg-pos (point)))
+ (when (or (gdscript-info-looking-at-beginning-of-defun)
+ (gdscript-nav-beginning-of-defun 1)
+ (gdscript-nav-beginning-of-defun -1))
+ (setq beg-defun-indent (current-indentation))
+ (while (progn
+ (gdscript-nav-end-of-statement)
+ (gdscript--util-forward-comment 1)
+ (and (> (current-indentation) beg-defun-indent)
+ (not (eobp)))))
+ (gdscript--util-forward-comment -1)
+ (forward-line 1)
+ ;; Ensure point moves forward.
+ (and (> beg-pos (point)) (goto-char beg-pos)))))
+
+(defun gdscript-nav--syntactically (fn poscompfn &optional contextfn)
+ "Move point using FN avoiding places with specific context.
+FN must take no arguments. POSCOMPFN is a two arguments function
+used to compare current and previous point after it is moved
+using FN, this is normally a less-than or greater-than
+comparison. Optional argument CONTEXTFN defaults to
+`gdscript-syntax-context-type' and is used for checking current
+point context, it must return a non-nil value if this point must
+be skipped."
+ (let ((contextfn (or contextfn 'gdscript-syntax-context-type))
+ (start-pos (point-marker))
+ (prev-pos))
+ (catch 'found
+ (while t
+ (let* ((newpos
+ (and (funcall fn) (point-marker)))
+ (context (funcall contextfn)))
+ (cond ((and (not context) newpos
+ (or (and (not prev-pos) newpos)
+ (and prev-pos newpos
+ (funcall poscompfn newpos prev-pos))))
+ (throw 'found (point-marker)))
+ ((and newpos context)
+ (setq prev-pos (point)))
+ (t (unless newpos (goto-char start-pos))
+ (throw 'found nil))))))))
+
+(defun gdscript-nav--forward-defun (arg)
+ "Internal implementation of gdscript-nav-{backward,forward}-defun.
+Uses ARG to define which function to call, and how many times
+repeat it."
+ (let ((found))
+ (while (and (> arg 0)
+ (setq found
+ (gdscript-nav--syntactically
+ (lambda ()
+ (re-search-forward
+ gdscript-nav-beginning-of-defun-regexp nil t))
+ '>)))
+ (setq arg (1- arg)))
+ (while (and (< arg 0)
+ (setq found
+ (gdscript-nav--syntactically
+ (lambda ()
+ (re-search-backward
+ gdscript-nav-beginning-of-defun-regexp nil t))
+ '<)))
+ (setq arg (1+ arg)))
+ found))
+
+(defun gdscript-nav-backward-defun (&optional arg)
+ "Navigate to closer defun backward ARG times.
+Unlikely `gdscript-nav-beginning-of-defun' this doesn't care about
+nested definitions."
+ (interactive "^p")
+ (gdscript-nav--forward-defun (- (or arg 1))))
+
+(defun gdscript-nav-forward-defun (&optional arg)
+ "Navigate to closer defun forward ARG times.
+Unlikely `gdscript-nav-beginning-of-defun' this doesn't care about
+nested definitions."
+ (interactive "^p")
+ (gdscript-nav--forward-defun (or arg 1)))
+
+(defun gdscript-nav-beginning-of-statement ()
+ "Move to start of current statement."
+ (interactive "^")
+ (forward-line 0)
+ (let* ((ppss (syntax-ppss))
+ (context-point
+ (or
+ (gdscript-syntax-context 'paren ppss)
+ (gdscript-syntax-context 'string ppss))))
+ (cond ((bobp))
+ (context-point
+ (goto-char context-point)
+ (gdscript-nav-beginning-of-statement))
+ ((save-excursion
+ (forward-line -1)
+ (gdscript-info-line-ends-backslash-p))
+ (forward-line -1)
+ (gdscript-nav-beginning-of-statement))))
+ (back-to-indentation)
+ (point-marker))
+
+(defun gdscript-nav-end-of-statement (&optional noend)
+ "Move to end of current statement.
+Optional argument NOEND is internal and makes the logic to not
+jump to the end of line when moving forward searching for the end
+of the statement."
+ (interactive "^")
+ (let (string-start bs-pos (last-string-end 0))
+ (while (and (or noend (goto-char (line-end-position)))
+ (not (eobp))
+ (cond ((setq string-start (gdscript-syntax-context 'string))
+ ;; The assertion can only fail if syntax table
+ ;; text properties and the `syntax-ppss' cache
+ ;; are somehow out of whack. This has been
+ ;; observed when using `syntax-ppss' during
+ ;; narrowing.
+ (cl-assert (>= string-start last-string-end)
+ :show-args
+ "\
+Overlapping strings detected (start=%d, last-end=%d)")
+ (goto-char string-start)
+ (if (gdscript-syntax-context 'paren)
+ ;; Ended up inside a paren, roll again.
+ (gdscript-nav-end-of-statement t)
+ ;; This is not inside a paren, move to the
+ ;; end of this string.
+ (goto-char (+ (point)
+ (gdscript-syntax-count-quotes
+ (char-after (point)) (point))))
+ (setq last-string-end
+ (or (re-search-forward
+ (rx (syntax string-delimiter)) nil t)
+ (goto-char (point-max))))))
+ ((gdscript-syntax-context 'paren)
+ ;; The statement won't end before we've escaped
+ ;; at least one level of parenthesis.
+ (condition-case err
+ (goto-char (scan-lists (point) 1 -1))
+ (scan-error (goto-char (nth 3 err)))))
+ ((setq bs-pos (gdscript-info-line-ends-backslash-p))
+ (goto-char bs-pos)
+ (forward-line 1))))))
+ (point-marker))
+
+(defun gdscript-nav-backward-statement (&optional arg)
+ "Move backward to previous statement.
+With ARG, repeat. See `gdscript-nav-forward-statement'."
+ (interactive "^p")
+ (or arg (setq arg 1))
+ (gdscript-nav-forward-statement (- arg)))
+
+(defun gdscript-nav-forward-statement (&optional arg)
+ "Move forward to next statement.
+With ARG, repeat. With negative argument, move ARG times
+backward to previous statement."
+ (interactive "^p")
+ (or arg (setq arg 1))
+ (while (> arg 0)
+ (gdscript-nav-end-of-statement)
+ (gdscript--util-forward-comment)
+ (gdscript-nav-beginning-of-statement)
+ (setq arg (1- arg)))
+ (while (< arg 0)
+ (gdscript-nav-beginning-of-statement)
+ (gdscript--util-forward-comment -1)
+ (gdscript-nav-beginning-of-statement)
+ (setq arg (1+ arg))))
+
+(defun gdscript-nav-beginning-of-block ()
+ "Move to start of current block."
+ (interactive "^")
+ (let ((starting-pos (point)))
+ (if (progn
+ (gdscript-nav-beginning-of-statement)
+ (looking-at (gdscript-rx block-start)))
+ (point-marker)
+ ;; Go to first line beginning a statement
+ (while (and (not (bobp))
+ (or (and (gdscript-nav-beginning-of-statement) nil)
+ (gdscript-info-current-line-comment-p)
+ (gdscript-info-current-line-empty-p)))
+ (forward-line -1))
+ (let ((block-matching-indent
+ (- (current-indentation) gdscript-indent-offset)))
+ (while
+ (and (gdscript-nav-backward-block)
+ (> (current-indentation) block-matching-indent)))
+ (if (and (looking-at (gdscript-rx block-start))
+ (= (current-indentation) block-matching-indent))
+ (point-marker)
+ (and (goto-char starting-pos) nil))))))
+
+(defun gdscript-nav-end-of-block ()
+ "Move to end of current block."
+ (interactive "^")
+ (when (gdscript-nav-beginning-of-block)
+ (let ((block-indentation (current-indentation)))
+ (gdscript-nav-end-of-statement)
+ (while (and (forward-line 1)
+ (not (eobp))
+ (or (and (> (current-indentation) block-indentation)
+ (or (gdscript-nav-end-of-statement) t))
+ (gdscript-info-current-line-comment-p)
+ (gdscript-info-current-line-empty-p))))
+ (gdscript--util-forward-comment -1)
+ (point-marker))))
+
+(defun gdscript-nav-backward-block (&optional arg)
+ "Move backward to previous block of code.
+With ARG, repeat. See `gdscript-nav-forward-block'."
+ (interactive "^p")
+ (or arg (setq arg 1))
+ (gdscript-nav-forward-block (- arg)))
+
+(defun gdscript-nav-forward-block (&optional arg)
+ "Move forward to next block of code.
+With ARG, repeat. With negative argument, move ARG times
+backward to previous block."
+ (interactive "^p")
+ (or arg (setq arg 1))
+ (let ((block-start-regexp
+ (gdscript-rx line-start (* whitespace) block-start))
+ (starting-pos (point)))
+ (while (> arg 0)
+ (gdscript-nav-end-of-statement)
+ (while (and
+ (re-search-forward block-start-regexp nil t)
+ (gdscript-syntax-context-type)))
+ (setq arg (1- arg)))
+ (while (< arg 0)
+ (gdscript-nav-beginning-of-statement)
+ (while (and
+ (re-search-backward block-start-regexp nil t)
+ (gdscript-syntax-context-type)))
+ (setq arg (1+ arg)))
+ (gdscript-nav-beginning-of-statement)
+ (if (not (looking-at (gdscript-rx block-start)))
+ (and (goto-char starting-pos) nil)
+ (and (not (= (point) starting-pos)) (point-marker)))))
+
+(defun gdscript-nav--lisp-forward-sexp (&optional arg)
+ "Standard version `forward-sexp'.
+It ignores completely the value of `forward-sexp-function' by
+setting it to nil before calling `forward-sexp'. With positive
+ARG move forward only one sexp, else move backwards."
+ (let ((forward-sexp-function)
+ (arg (if (or (not arg) (> arg 0)) 1 -1)))
+ (forward-sexp arg)))
+
+(defun gdscript-nav--lisp-forward-sexp-safe (&optional arg)
+ "Safe version of standard `forward-sexp'.
+When at end of sexp (i.e. looking at an opening/closing paren)
+skips it instead of throwing an error. With positive ARG move
+forward only one sexp, else move backwards."
+ (let* ((arg (if (or (not arg) (> arg 0)) 1 -1))
+ (paren-regexp
+ (if (> arg 0) (gdscript-rx close-paren) (gdscript-rx open-paren)))
+ (search-fn
+ (if (> arg 0) #'re-search-forward #'re-search-backward)))
+ (condition-case nil
+ (gdscript-nav--lisp-forward-sexp arg)
+ (error
+ (while (and (funcall search-fn paren-regexp nil t)
+ (gdscript-syntax-context 'paren)))))))
+
+(defun gdscript-nav--forward-sexp (&optional dir safe skip-parens-p)
+ "Move to forward sexp.
+With positive optional argument DIR direction move forward, else
+backwards. When optional argument SAFE is non-nil do not throw
+errors when at end of sexp, skip it instead. With optional
+argument SKIP-PARENS-P force sexp motion to ignore parenthesized
+expressions when looking at them in either direction."
+ (setq dir (or dir 1))
+ (unless (= dir 0)
+ (let* ((forward-p (if (> dir 0)
+ (and (setq dir 1) t)
+ (and (setq dir -1) nil)))
+ (context-type (gdscript-syntax-context-type)))
+ (cond
+ ((memq context-type '(string comment))
+ ;; Inside of a string, get out of it.
+ (let ((forward-sexp-function))
+ (forward-sexp dir)))
+ ((and (not skip-parens-p)
+ (or (eq context-type 'paren)
+ (if forward-p
+ (eq (syntax-class (syntax-after (point)))
+ (car (string-to-syntax "(")))
+ (eq (syntax-class (syntax-after (1- (point))))
+ (car (string-to-syntax ")"))))))
+ ;; Inside a paren or looking at it, lisp knows what to do.
+ (if safe
+ (gdscript-nav--lisp-forward-sexp-safe dir)
+ (gdscript-nav--lisp-forward-sexp dir)))
+ (t
+ ;; This part handles the lispy feel of
+ ;; `gdscript-nav-forward-sexp'. Knowing everything about the
+ ;; current context and the context of the next sexp tries to
+ ;; follow the lisp sexp motion commands in a symmetric manner.
+ (let* ((context
+ (cond
+ ((gdscript-info-beginning-of-block-p) 'block-start)
+ ((gdscript-info-end-of-block-p) 'block-end)
+ ((gdscript-info-beginning-of-statement-p) 'statement-start)
+ ((gdscript-info-end-of-statement-p) 'statement-end)))
+ (next-sexp-pos
+ (save-excursion
+ (if safe
+ (gdscript-nav--lisp-forward-sexp-safe dir)
+ (gdscript-nav--lisp-forward-sexp dir))
+ (point)))
+ (next-sexp-context
+ (save-excursion
+ (goto-char next-sexp-pos)
+ (cond
+ ((gdscript-info-beginning-of-block-p) 'block-start)
+ ((gdscript-info-end-of-block-p) 'block-end)
+ ((gdscript-info-beginning-of-statement-p) 'statement-start)
+ ((gdscript-info-end-of-statement-p) 'statement-end)
+ ((gdscript-info-statement-starts-block-p) 'starts-block)
+ ((gdscript-info-statement-ends-block-p) 'ends-block)))))
+ (if forward-p
+ (cond ((and (not (eobp))
+ (gdscript-info-current-line-empty-p))
+ (gdscript--util-forward-comment dir)
+ (gdscript-nav--forward-sexp dir safe skip-parens-p))
+ ((eq context 'block-start)
+ (gdscript-nav-end-of-block))
+ ((eq context 'statement-start)
+ (gdscript-nav-end-of-statement))
+ ((and (memq context '(statement-end block-end))
+ (eq next-sexp-context 'ends-block))
+ (goto-char next-sexp-pos)
+ (gdscript-nav-end-of-block))
+ ((and (memq context '(statement-end block-end))
+ (eq next-sexp-context 'starts-block))
+ (goto-char next-sexp-pos)
+ (gdscript-nav-end-of-block))
+ ((memq context '(statement-end block-end))
+ (goto-char next-sexp-pos)
+ (gdscript-nav-end-of-statement))
+ (t (goto-char next-sexp-pos)))
+ (cond ((and (not (bobp))
+ (gdscript-info-current-line-empty-p))
+ (gdscript--util-forward-comment dir)
+ (gdscript-nav--forward-sexp dir safe skip-parens-p))
+ ((eq context 'block-end)
+ (gdscript-nav-beginning-of-block))
+ ((eq context 'statement-end)
+ (gdscript-nav-beginning-of-statement))
+ ((and (memq context '(statement-start block-start))
+ (eq next-sexp-context 'starts-block))
+ (goto-char next-sexp-pos)
+ (gdscript-nav-beginning-of-block))
+ ((and (memq context '(statement-start block-start))
+ (eq next-sexp-context 'ends-block))
+ (goto-char next-sexp-pos)
+ (gdscript-nav-beginning-of-block))
+ ((memq context '(statement-start block-start))
+ (goto-char next-sexp-pos)
+ (gdscript-nav-beginning-of-statement))
+ (t (goto-char next-sexp-pos))))))))))
+
+(defun gdscript-nav-forward-sexp (&optional arg safe skip-parens-p)
+ "Move forward across expressions.
+With ARG, do it that many times. Negative arg -N means move
+backward N times. When optional argument SAFE is non-nil do not
+throw errors when at end of sexp, skip it instead. With optional
+argument SKIP-PARENS-P force sexp motion to ignore parenthesized
+expressions when looking at them in either direction (forced to t
+in interactive calls)."
+ (interactive "^p")
+ (or arg (setq arg 1))
+ ;; Do not follow parens on interactive calls. This hack to detect
+ ;; if the function was called interactively copes with the way
+ ;; `forward-sexp' works by calling `forward-sexp-function', losing
+ ;; interactive detection by checking `current-prefix-arg'. The
+ ;; reason to make this distinction is that lisp functions like
+ ;; `blink-matching-open' get confused causing issues like the one in
+ ;; Bug#16191. With this approach the user gets a symmetric behavior
+ ;; when working interactively while called functions expecting
+ ;; paren-based sexp motion work just fine.
+ (or
+ skip-parens-p
+ (setq skip-parens-p
+ (memq real-this-command
+ (list
+ #'forward-sexp #'backward-sexp
+ #'gdscript-nav-forward-sexp #'gdscript-nav-backward-sexp
+ #'gdscript-nav-forward-sexp-safe #'gdscript-nav-backward-sexp))))
+ (while (> arg 0)
+ (gdscript-nav--forward-sexp 1 safe skip-parens-p)
+ (setq arg (1- arg)))
+ (while (< arg 0)
+ (gdscript-nav--forward-sexp -1 safe skip-parens-p)
+ (setq arg (1+ arg))))
+
+(defun gdscript-nav-backward-sexp (&optional arg safe skip-parens-p)
+ "Move backward across expressions.
+With ARG, do it that many times. Negative arg -N means move
+forward N times. When optional argument SAFE is non-nil do not
+throw errors when at end of sexp, skip it instead. With optional
+argument SKIP-PARENS-P force sexp motion to ignore parenthesized
+expressions when looking at them in either direction (forced to t
+in interactive calls)."
+ (interactive "^p")
+ (or arg (setq arg 1))
+ (gdscript-nav-forward-sexp (- arg) safe skip-parens-p))
+
+(defun gdscript-nav-forward-sexp-safe (&optional arg skip-parens-p)
+ "Move forward safely across expressions.
+With ARG, do it that many times. Negative arg -N means move
+backward N times. With optional argument SKIP-PARENS-P force
+sexp motion to ignore parenthesized expressions when looking at
+them in either direction (forced to t in interactive calls)."
+ (interactive "^p")
+ (gdscript-nav-forward-sexp arg t skip-parens-p))
+
+(defun gdscript-nav-backward-sexp-safe (&optional arg skip-parens-p)
+ "Move backward safely across expressions.
+With ARG, do it that many times. Negative arg -N means move
+forward N times. With optional argument SKIP-PARENS-P force sexp
+motion to ignore parenthesized expressions when looking at them in
+either direction (forced to t in interactive calls)."
+ (interactive "^p")
+ (gdscript-nav-backward-sexp arg t skip-parens-p))
+
+(defun gdscript-nav--up-list (&optional dir)
+ "Internal implementation of `gdscript-nav-up-list'.
+DIR is always 1 or -1 and comes sanitized from
+`gdscript-nav-up-list' calls."
+ (let ((context (gdscript-syntax-context-type))
+ (forward-p (> dir 0)))
+ (cond
+ ((memq context '(string comment)))
+ ((eq context 'paren)
+ (let ((forward-sexp-function))
+ (up-list dir)))
+ ((and forward-p (gdscript-info-end-of-block-p))
+ (let ((parent-end-pos
+ (save-excursion
+ (let ((indentation (and
+ (gdscript-nav-beginning-of-block)
+ (current-indentation))))
+ (while (and indentation
+ (> indentation 0)
+ (>= (current-indentation) indentation)
+ (gdscript-nav-backward-block)))
+ (gdscript-nav-end-of-block)))))
+ (and (> (or parent-end-pos (point)) (point))
+ (goto-char parent-end-pos))))
+ (forward-p (gdscript-nav-end-of-block))
+ ((and (not forward-p)
+ (> (current-indentation) 0)
+ (gdscript-info-beginning-of-block-p))
+ (let ((prev-block-pos
+ (save-excursion
+ (let ((indentation (current-indentation)))
+ (while (and (gdscript-nav-backward-block)
+ (>= (current-indentation) indentation))))
+ (point))))
+ (and (> (point) prev-block-pos)
+ (goto-char prev-block-pos))))
+ ((not forward-p) (gdscript-nav-beginning-of-block)))))
+
+(defun gdscript-nav-up-list (&optional arg)
+ "Move forward out of one level of parentheses (or blocks).
+With ARG, do this that many times.
+A negative argument means move backward but still to a less deep spot.
+This command assumes point is not in a string or comment."
+ (interactive "^p")
+ (or arg (setq arg 1))
+ (while (> arg 0)
+ (gdscript-nav--up-list 1)
+ (setq arg (1- arg)))
+ (while (< arg 0)
+ (gdscript-nav--up-list -1)
+ (setq arg (1+ arg))))
+
+(defun gdscript-nav-backward-up-list (&optional arg)
+ "Move backward out of one level of parentheses (or blocks).
+With ARG, do this that many times.
+A negative argument means move forward but still to a less deep spot.
+This command assumes point is not in a string or comment."
+ (interactive "^p")
+ (or arg (setq arg 1))
+ (gdscript-nav-up-list (- arg)))
+
+(provide 'gdscript-indent-and-nav)
+;;; gdscript-indent-and-nav.el ends here
diff --git a/gdscript-rx.el b/gdscript-rx.el
index e38e50a..4dc827b 100644
--- a/gdscript-rx.el
+++ b/gdscript-rx.el
@@ -1,76 +1,76 @@
-;;; gdscript-rx.el --- Regex for GDScript -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2020-2026 GDQuest and contributors
-
-;; Author: Nathan Lovato
-;; Fabián E. Gallina
-;; Maintainer: Jen-Chieh Shen
-
-;; This program 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.
-
-;; This program 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 this program. If not, see .
-
-;;; Commentary:
-
-;; Provides `gdscript-rx',
-;; which binds additional syntax useful for GDScript-based REGEXP.
-
-;;; Code:
-
-(require 'rx)
-
-(defmacro gdscript-rx (&rest regexps)
- "Gdscript mode specialized rx macro.
-This variant of `rx' supports common Gdscript named REGEXPS."
- `(rx-let
- ((block-start
- (or (seq (or "if" "elif" "while" "func") (+? (not ":")) ":")
- (seq (zero-or-more nonl)
- ":"
- (or (seq (zero-or-more " ") eol)
- (seq (zero-or-more " ") "#" (zero-or-more nonl) eol)))))
- (dedenter (seq symbol-start
- (or "elif" "else")
- symbol-end))
- (block-ender (seq symbol-start
- (or "break" "continue" "pass" "return")
- symbol-end))
- (defun (seq symbol-start
- (or "func" "class" "static func")
- symbol-end))
- (symbol-name (seq (any letter ?_) (* (any word ?_))))
- (open-paren (or "{" "[" "("))
- (close-paren (or "}" "]" ")"))
- (simple-operator (any ?+ ?- ?/ ?& ?^ ?~ ?| ?* ?< ?> ?= ?%))
- (not-simple-operator (not simple-operator))
- ;; TODO: clean up operators that don't exist in GDScript
- (operator (or "==" ">=" "is" "not"
- "**" "//" "<<" ">>" "<=" "!="
- "+" "-" "/" "&" "^" "~" "|" "*" "<" ">"
- "=" "%"))
- (assignment-operator (or "+=" "-=" "*=" "/=" "//=" "%=" "**="
- ">>=" "<<=" "&=" "^=" "|="
- "="))
- (string-delimiter (seq
- ;; Match even number of backslashes.
- (or (not (any ?\\ ?\' ?\")) point
- ;; Quotes might be preceded by an
- ;; escaped quote.
- (and (or (not (any ?\\)) point) ?\\
- (* ?\\ ?\\) (any ?\' ?\")))
- (* ?\\ ?\\)
- ;; Match single or triple quotes of any kind.
- (group (or "\"\"\"" "\"" "'''" "'")))))
- (rx ,@regexps)))
-
-(provide 'gdscript-rx)
-;;; gdscript-rx.el ends here
+;;; gdscript-rx.el --- Regex for GDScript -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2026 GDQuest and contributors
+
+;; Author: Nathan Lovato
+;; Fabián E. Gallina
+;; Maintainer: Jen-Chieh Shen
+
+;; This program 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.
+
+;; This program 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 this program. If not, see .
+
+;;; Commentary:
+
+;; Provides `gdscript-rx',
+;; which binds additional syntax useful for GDScript-based REGEXP.
+
+;;; Code:
+
+(require 'rx)
+
+(defmacro gdscript-rx (&rest regexps)
+ "Gdscript mode specialized rx macro.
+This variant of `rx' supports common Gdscript named REGEXPS."
+ `(rx-let
+ ((block-start
+ (or (seq (or "if" "elif" "while" "func") (+? (not ":")) ":")
+ (seq (zero-or-more nonl)
+ ":"
+ (or (seq (zero-or-more " ") eol)
+ (seq (zero-or-more " ") "#" (zero-or-more nonl) eol)))))
+ (dedenter (seq symbol-start
+ (or "elif" "else")
+ symbol-end))
+ (block-ender (seq symbol-start
+ (or "break" "continue" "pass" "return")
+ symbol-end))
+ (defun (seq symbol-start
+ (or "func" "class" "static func")
+ symbol-end))
+ (symbol-name (seq (any letter ?_) (* (any word ?_))))
+ (open-paren (or "{" "[" "("))
+ (close-paren (or "}" "]" ")"))
+ (simple-operator (any ?+ ?- ?/ ?& ?^ ?~ ?| ?* ?< ?> ?= ?%))
+ (not-simple-operator (not simple-operator))
+ ;; TODO: clean up operators that don't exist in GDScript
+ (operator (or "==" ">=" "is" "not"
+ "**" "//" "<<" ">>" "<=" "!="
+ "+" "-" "/" "&" "^" "~" "|" "*" "<" ">"
+ "=" "%"))
+ (assignment-operator (or "+=" "-=" "*=" "/=" "//=" "%=" "**="
+ ">>=" "<<=" "&=" "^=" "|="
+ "="))
+ (string-delimiter (seq
+ ;; Match even number of backslashes.
+ (or (not (any ?\\ ?\' ?\")) point
+ ;; Quotes might be preceded by an
+ ;; escaped quote.
+ (and (or (not (any ?\\)) point) ?\\
+ (* ?\\ ?\\) (any ?\' ?\")))
+ (* ?\\ ?\\)
+ ;; Match single or triple quotes of any kind.
+ (group (or "\"\"\"" "\"" "'''" "'")))))
+ (rx ,@regexps)))
+
+(provide 'gdscript-rx)
+;;; gdscript-rx.el ends here
diff --git a/gdscript-syntax.el b/gdscript-syntax.el
index 9b6ef62..afb2590 100644
--- a/gdscript-syntax.el
+++ b/gdscript-syntax.el
@@ -1,188 +1,188 @@
-;;; gdscript-syntax.el --- Syntax highlighting for GDScript -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2020-2026 GDQuest
-
-;; Author: Nathan Lovato
-;; Fabián E. Gallina
-;; Maintainer: Jen-Chieh Shen
-
-;; This program 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.
-
-;; This program 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 this program. If not, see .
-
-;;; Commentary:
-;;
-;; Adds syntax highlighting and builds the syntax table for GDScript.
-;;
-
-;;; Code:
-
-(require 'cl-lib)
-(require 'gdscript-keywords)
-
-(defun gdscript-syntax-regex-maker (words)
- (regexp-opt words 'symbols))
-
-;;; Font-lock and syntax
-(eval-and-compile (defun gdscript-syntax--context-compiler-macro (form type &optional syntax-ppss)
- (pcase type
- (''comment
- `(let ((ppss (or ,syntax-ppss
- (syntax-ppss))))
- (and (nth 4 ppss)
- (nth 8 ppss))))
- (''string
- `(let ((ppss (or ,syntax-ppss
- (syntax-ppss))))
- (and (nth 3 ppss)
- (nth 8 ppss))))
- (''paren
- `(nth 1
- (or ,syntax-ppss
- (syntax-ppss))))
- (_ form))))
-
-;; Controls font-face mappings or colors to highlight groups of keywords
-(defvar gdscript-font-lock `((,(rx (and "$" (one-or-more (or "/" (one-or-more word)))))
- (0 font-lock-constant-face))
- (,(rx (and (group "$") "\"" (zero-or-more nonl) "\""))
- (1 font-lock-constant-face))
- (,(gdscript-syntax-regex-maker gdscript-keywords)
- 1
- font-lock-keyword-face)
- (,(rx (group "@" (0+ (or word ?_))))
- 1
- font-lock-preprocessor-face)
- (,(gdscript-syntax-regex-maker (append gdscript-built-in-constants
- gdscript-built-in-types gdscript-built-in-functions))
- 1
- font-lock-builtin-face)
- (,(gdscript-syntax-regex-maker gdscript-built-in-classes)
- 1
- font-lock-type-face)
- (,(rx symbol-start
- "func"
- (1+ space)
- (group (1+ (or word ?_))))
- (1 font-lock-function-name-face))
- (,(rx symbol-start
- (or "var" "const")
- (1+ space)
- (group (1+ (or word ?_))))
- (1 font-lock-variable-name-face))
- ;; Function call
- (,(rx (group (1+ (or word ?_)))
- (0+ space)
- "(")
- (1 font-lock-function-name-face))))
-
-(defun gdscript-syntax-context (type &optional syntax-ppss)
- "Return non-nil if point is on TYPE using SYNTAX-PPSS.
-TYPE can be `comment', `string' or `paren'. It returns the start
-character address of the specified TYPE."
- (declare (compiler-macro gdscript-syntax--context-compiler-macro))
- (let ((ppss (or syntax-ppss (syntax-ppss))))
- (pcase type
- ('comment (and (nth 4 ppss) (nth 8 ppss)))
- ('string (and (nth 3 ppss) (nth 8 ppss)))
- ('paren (nth 1 ppss))
- (_ nil))))
-
-(defun gdscript-syntax-context-type (&optional syntax-ppss)
- "Return the context type using SYNTAX-PPSS.
-The type returned can be `comment', `string' or `paren'."
- (let ((ppss (or syntax-ppss (syntax-ppss))))
- (cond
- ((nth 8 ppss) (if (nth 4 ppss) 'comment 'string))
- ((nth 1 ppss) 'paren))))
-
-(define-inline gdscript-syntax-comment-or-string-p (&optional ppss)
- "Return non-nil if PPSS is inside comment or string."
- (nth 8 (or ppss (syntax-ppss))))
-
-(define-inline gdscript-syntax-closing-paren-p ()
- "Return non-nil if char after point is a closing paren."
- (eq (syntax-class (syntax-after (point)))
- (syntax-class (string-to-syntax ")"))))
-
-(defconst gdscript-syntax-propertize-function
- (syntax-propertize-rules
- ((rx (or "\"\"\"" "'''"))
- (0 (ignore (gdscript-syntax-stringify))))))
-
-(defun gdscript-syntax-count-quotes (quote-char &optional point limit)
- "Count number of quotes around point (max is 3).
-QUOTE-CHAR is the quote char to count. Optional argument POINT is
-the point where scan starts (defaults to current point), and LIMIT
-is used to limit the scan."
- (let ((i 0))
- (while (and (< i 3)
- (or (not limit) (< (+ point i) limit))
- (eq (char-after (+ point i)) quote-char))
- (setq i (1+ i)))
- i))
-
-(defun gdscript-syntax-stringify ()
- "Put `syntax-table' property correctly on single/triple quotes."
- (let* ((ppss (save-excursion (backward-char 3) (syntax-ppss)))
- (string-start (and (eq t (nth 3 ppss)) (nth 8 ppss)))
- (quote-starting-pos (- (point) 3))
- (quote-ending-pos (point)))
- (cond ((or (nth 4 ppss) ;Inside a comment
- (and string-start
- ;; Inside of a string quoted with different triple quotes.
- (not (eq (char-after string-start)
- (char-after quote-starting-pos)))))
- ;; Do nothing.
- nil)
- ((nth 5 ppss)
- ;; The first quote is escaped, so it's not part of a triple quote!
- (goto-char (1+ quote-starting-pos)))
- ((null string-start)
- ;; This set of quotes delimit the start of a string.
- (put-text-property quote-starting-pos (1+ quote-starting-pos)
- 'syntax-table (string-to-syntax "|")))
- (t
- ;; This set of quotes delimit the end of a string.
- (put-text-property (1- quote-ending-pos) quote-ending-pos
- 'syntax-table (string-to-syntax "|"))))))
-
-(defvar gdscript-mode-syntax-table
- (let ((table (make-syntax-table)))
- ;; Give punctuation syntax to ASCII that normally has symbol
- ;; syntax or has word syntax and isn't a letter.
- (let ((symbol (string-to-syntax "_"))
- (sst (standard-syntax-table)))
- (dotimes (i 128)
- (unless (= i ?_)
- (if (equal symbol (aref sst i))
- (modify-syntax-entry i "." table)))))
- (modify-syntax-entry ?$ "." table)
- (modify-syntax-entry ?% "." table)
- ;; exceptions
- (modify-syntax-entry ?# "<" table)
- (modify-syntax-entry ?\n ">" table)
- (modify-syntax-entry ?' "\"" table)
- (modify-syntax-entry ?` "$" table)
- table)
- "Syntax table for Gdscript files.")
-
-(defvar gdscript-dotty-syntax-table
- (let ((table (make-syntax-table gdscript-mode-syntax-table)))
- (modify-syntax-entry ?. "w" table)
- (modify-syntax-entry ?_ "w" table)
- table)
- "Dotty syntax table for Gdscript files.
-It makes underscores and dots word constituent chars.")
-
-(provide 'gdscript-syntax)
-;;; gdscript-syntax.el ends here
+;;; gdscript-syntax.el --- Syntax highlighting for GDScript -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2026 GDQuest
+
+;; Author: Nathan Lovato
+;; Fabián E. Gallina
+;; Maintainer: Jen-Chieh Shen
+
+;; This program 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.
+
+;; This program 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 this program. If not, see .
+
+;;; Commentary:
+;;
+;; Adds syntax highlighting and builds the syntax table for GDScript.
+;;
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'gdscript-keywords)
+
+(defun gdscript-syntax-regex-maker (words)
+ (regexp-opt words 'symbols))
+
+;;; Font-lock and syntax
+(eval-and-compile (defun gdscript-syntax--context-compiler-macro (form type &optional syntax-ppss)
+ (pcase type
+ (''comment
+ `(let ((ppss (or ,syntax-ppss
+ (syntax-ppss))))
+ (and (nth 4 ppss)
+ (nth 8 ppss))))
+ (''string
+ `(let ((ppss (or ,syntax-ppss
+ (syntax-ppss))))
+ (and (nth 3 ppss)
+ (nth 8 ppss))))
+ (''paren
+ `(nth 1
+ (or ,syntax-ppss
+ (syntax-ppss))))
+ (_ form))))
+
+;; Controls font-face mappings or colors to highlight groups of keywords
+(defvar gdscript-font-lock `((,(rx (and "$" (one-or-more (or "/" (one-or-more word)))))
+ (0 font-lock-constant-face))
+ (,(rx (and (group "$") "\"" (zero-or-more nonl) "\""))
+ (1 font-lock-constant-face))
+ (,(gdscript-syntax-regex-maker gdscript-keywords)
+ 1
+ font-lock-keyword-face)
+ (,(rx (group "@" (0+ (or word ?_))))
+ 1
+ font-lock-preprocessor-face)
+ (,(gdscript-syntax-regex-maker (append gdscript-built-in-constants
+ gdscript-built-in-types gdscript-built-in-functions))
+ 1
+ font-lock-builtin-face)
+ (,(gdscript-syntax-regex-maker gdscript-built-in-classes)
+ 1
+ font-lock-type-face)
+ (,(rx symbol-start
+ "func"
+ (1+ space)
+ (group (1+ (or word ?_))))
+ (1 font-lock-function-name-face))
+ (,(rx symbol-start
+ (or "var" "const")
+ (1+ space)
+ (group (1+ (or word ?_))))
+ (1 font-lock-variable-name-face))
+ ;; Function call
+ (,(rx (group (1+ (or word ?_)))
+ (0+ space)
+ "(")
+ (1 font-lock-function-name-face))))
+
+(defun gdscript-syntax-context (type &optional syntax-ppss)
+ "Return non-nil if point is on TYPE using SYNTAX-PPSS.
+TYPE can be `comment', `string' or `paren'. It returns the start
+character address of the specified TYPE."
+ (declare (compiler-macro gdscript-syntax--context-compiler-macro))
+ (let ((ppss (or syntax-ppss (syntax-ppss))))
+ (pcase type
+ ('comment (and (nth 4 ppss) (nth 8 ppss)))
+ ('string (and (nth 3 ppss) (nth 8 ppss)))
+ ('paren (nth 1 ppss))
+ (_ nil))))
+
+(defun gdscript-syntax-context-type (&optional syntax-ppss)
+ "Return the context type using SYNTAX-PPSS.
+The type returned can be `comment', `string' or `paren'."
+ (let ((ppss (or syntax-ppss (syntax-ppss))))
+ (cond
+ ((nth 8 ppss) (if (nth 4 ppss) 'comment 'string))
+ ((nth 1 ppss) 'paren))))
+
+(define-inline gdscript-syntax-comment-or-string-p (&optional ppss)
+ "Return non-nil if PPSS is inside comment or string."
+ (nth 8 (or ppss (syntax-ppss))))
+
+(define-inline gdscript-syntax-closing-paren-p ()
+ "Return non-nil if char after point is a closing paren."
+ (eq (syntax-class (syntax-after (point)))
+ (syntax-class (string-to-syntax ")"))))
+
+(defconst gdscript-syntax-propertize-function
+ (syntax-propertize-rules
+ ((rx (or "\"\"\"" "'''"))
+ (0 (ignore (gdscript-syntax-stringify))))))
+
+(defun gdscript-syntax-count-quotes (quote-char &optional point limit)
+ "Count number of quotes around point (max is 3).
+QUOTE-CHAR is the quote char to count. Optional argument POINT is
+the point where scan starts (defaults to current point), and LIMIT
+is used to limit the scan."
+ (let ((i 0))
+ (while (and (< i 3)
+ (or (not limit) (< (+ point i) limit))
+ (eq (char-after (+ point i)) quote-char))
+ (setq i (1+ i)))
+ i))
+
+(defun gdscript-syntax-stringify ()
+ "Put `syntax-table' property correctly on single/triple quotes."
+ (let* ((ppss (save-excursion (backward-char 3) (syntax-ppss)))
+ (string-start (and (eq t (nth 3 ppss)) (nth 8 ppss)))
+ (quote-starting-pos (- (point) 3))
+ (quote-ending-pos (point)))
+ (cond ((or (nth 4 ppss) ;Inside a comment
+ (and string-start
+ ;; Inside of a string quoted with different triple quotes.
+ (not (eq (char-after string-start)
+ (char-after quote-starting-pos)))))
+ ;; Do nothing.
+ nil)
+ ((nth 5 ppss)
+ ;; The first quote is escaped, so it's not part of a triple quote!
+ (goto-char (1+ quote-starting-pos)))
+ ((null string-start)
+ ;; This set of quotes delimit the start of a string.
+ (put-text-property quote-starting-pos (1+ quote-starting-pos)
+ 'syntax-table (string-to-syntax "|")))
+ (t
+ ;; This set of quotes delimit the end of a string.
+ (put-text-property (1- quote-ending-pos) quote-ending-pos
+ 'syntax-table (string-to-syntax "|"))))))
+
+(defvar gdscript-mode-syntax-table
+ (let ((table (make-syntax-table)))
+ ;; Give punctuation syntax to ASCII that normally has symbol
+ ;; syntax or has word syntax and isn't a letter.
+ (let ((symbol (string-to-syntax "_"))
+ (sst (standard-syntax-table)))
+ (dotimes (i 128)
+ (unless (= i ?_)
+ (if (equal symbol (aref sst i))
+ (modify-syntax-entry i "." table)))))
+ (modify-syntax-entry ?$ "." table)
+ (modify-syntax-entry ?% "." table)
+ ;; exceptions
+ (modify-syntax-entry ?# "<" table)
+ (modify-syntax-entry ?\n ">" table)
+ (modify-syntax-entry ?' "\"" table)
+ (modify-syntax-entry ?` "$" table)
+ table)
+ "Syntax table for Gdscript files.")
+
+(defvar gdscript-dotty-syntax-table
+ (let ((table (make-syntax-table gdscript-mode-syntax-table)))
+ (modify-syntax-entry ?. "w" table)
+ (modify-syntax-entry ?_ "w" table)
+ table)
+ "Dotty syntax table for Gdscript files.
+It makes underscores and dots word constituent chars.")
+
+(provide 'gdscript-syntax)
+;;; gdscript-syntax.el ends here
diff --git a/gdscript-utils.el b/gdscript-utils.el
index be092da..d3a3ee9 100644
--- a/gdscript-utils.el
+++ b/gdscript-utils.el
@@ -1,175 +1,175 @@
-;;; gdscript-utils.el --- Utility functions for gdscript-mode -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2020-2026 GDQuest and contributors
-
-;; Author: Nathan Lovato
-;; Fabián E. Gallina
-;; Maintainer: Jen-Chieh Shen
-
-;; This program 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.
-
-;; This program 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 this program. If not, see .
-
-;;; Commentary:
-;;
-;; Misc utility functions for GDScript mode.
-;;
-
-;;; Code:
-
-(require 'gdscript-syntax)
-(require 'gdscript-customization)
-
-(declare-function projectile-completing-read (prompt collections) "ext:projectile.el")
-
-(defun gdscript--util-goto-line (line-number)
- "Move point to LINE-NUMBER."
- (goto-char (point-min))
- (forward-line (1- line-number)))
-
-(defun gdscript--util-forward-comment (&optional direction)
- "Gdscript mode specific version of `forward-comment'.
-Optional argument DIRECTION defines the direction to move to."
- (let ((comment-start (gdscript-syntax-context 'comment))
- (factor (if (< (or direction 0) 0)
- -99999
- 99999)))
- (when comment-start
- (goto-char comment-start))
- (forward-comment factor)))
-
-(defun gdscript--util-list-directories (directory &optional predicate max-depth)
- "List DIRECTORY subdirs, filtered by PREDICATE and limited by MAX-DEPTH.
-Argument PREDICATE defaults to `identity' and must be a function
-that takes one argument (a full path) and returns non-nil for
-allowed files. When optional argument MAX-DEPTH is non-nil, stop
-searching when depth is reached, else don't limit."
- (let* ((dir (expand-file-name directory))
- (dir-length (length dir))
- (predicate (or predicate #'identity))
- (to-scan (list dir))
- (tally nil))
- (while to-scan
- (let ((current-dir (car to-scan)))
- (when (funcall predicate current-dir)
- (setq tally (cons current-dir tally)))
- (setq to-scan (append (cdr to-scan)
- (gdscript--util-list-files
- current-dir #'file-directory-p)
- nil))
- (when (and max-depth
- (<= max-depth
- (length (split-string
- (substring current-dir dir-length)
- "/\\|\\\\" t))))
- (setq to-scan nil))))
- (nreverse tally)))
-
-(defun gdscript--util-list-files (dir &optional predicate)
- "List files in DIR, filtering with PREDICATE.
-Argument PREDICATE defaults to `identity' and must be a function
-that takes one argument (a full path) and returns non-nil for
-allowed files."
- (let ((dir-name (file-name-as-directory dir)))
- (apply #'nconc
- (mapcar (lambda (file-name)
- (let ((full-file-name (expand-file-name file-name dir-name)))
- (when (and
- (not (member file-name '("." "..")))
- (funcall (or predicate #'identity) full-file-name))
- (list full-file-name))))
- (directory-files dir-name)))))
-
-
-(defun gdscript-util--find-project-configuration-file (&optional start-path)
- "Return the path to the file \"project.godot\".
-
-Start the search from START-PATH if provided. Otherwise, the search
-starts from the current buffer path.
-
-WARNING: the Godot project must exist for this function to work."
- (let* ((base-path (or start-path default-directory))
- (dominating-file
- (locate-dominating-file base-path
- (lambda (parent)
- (directory-files parent t "project.godot")))))
- (when dominating-file (expand-file-name dominating-file))))
-
-(defun gdscript-util--get-godot-project-name ()
- "Retrieve the project name from Godot's configuration file."
- (with-temp-buffer
- (insert-file-contents (concat (gdscript-util--find-project-configuration-file) "project.godot"))
- (goto-char (point-min))
- (if (re-search-forward "config/name=\"\\([^\"]*\\)\"" nil t)
- (match-string 1)
- (error "Could not find the name of the project"))))
-
-(defun gdscript-util--get-godot-project-version ()
- "Retrieve the project name from Godot's configuration file."
- (with-temp-buffer
- (insert-file-contents (concat (gdscript-util--find-project-configuration-file) "project.godot"))
- (goto-char (point-min))
- (if (re-search-forward "config_version=\\(3\\|4\\)" nil t) ; Godot 3.0 uses 3, Godot 3.6.2 uses 4
- "3"
- (if (re-search-forward "config/features=PackedStringArray\(\"\\([^,\)]*\\)\"" nil t)
- (let ((version (match-string 1)))
- (if (string-prefix-p "4.0" version) "4" version))
- (error "Could not find the project version")))))
-
-(defun gdscript-util--get-godot-buffer-name (&optional editor)
- "Return buffer name for godot's stdout/stderr output."
- (format (if editor "*godot - %s - Editor*" "*godot - %s*") (gdscript-util--get-godot-project-name)))
-
-(defun gdscript-util--get-gdformat-buffer-name ()
- "Return buffer name for godot's stdout/stderr output."
- (format "*gdformat - %s*" (gdscript-util--get-godot-project-name)))
-
-(defun gdscript-util--get-godot-project-file-path-relative (file-path)
- "Return the relative path of `FILE-PATH' to Godot's configuration file."
- (let ((project-configuration-file (gdscript-util--find-project-configuration-file)))
- (when project-configuration-file
- (concat (file-name-sans-extension
- (file-relative-name file-path project-configuration-file))))))
-
-(defun gdscript-util--flatten (xs)
- "Flatten deeply nested list.
-
-For example:
-> (gdscript-util--flatten (list 1 2 (list 3 (list 4 5)) nil))
-> (1 2 3 4 5)
-"
- (cond
- ((null xs) nil)
- ((listp xs) (append (gdscript-util--flatten (car xs)) (gdscript-util--flatten (cdr xs))))
- (t (list xs))))
-
-(defun gdscript-util--read (items &optional prompt)
- "Let's choose single item from ITEMS from mini-buffer.
-PROMPT is prompt for read command. Return `nil' if user aborts."
- (let* ((p (if prompt prompt "Options"))
- (result (cond ((featurep 'projectile)
- (projectile-completing-read (format "%s: " p) items))
- (ido-mode
- (ido-completing-read (format "%s: " p) items))
- (t
- (completing-read (format "%s: " p) items nil t)))))
- (if quit-flag nil result)))
-
-(defmacro gdscript-util--with-available-hydra (&rest body)
- ""
- `(progn
- (when (not (featurep 'hydra))
- (error "No `hydra.el' available. To execute `gdscript-hydra-show' command you need to install hydra.el"))
- ,@body))
-
-(provide 'gdscript-utils)
-;;; gdscript-utils.el ends here
+;;; gdscript-utils.el --- Utility functions for gdscript-mode -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2026 GDQuest and contributors
+
+;; Author: Nathan Lovato
+;; Fabián E. Gallina
+;; Maintainer: Jen-Chieh Shen
+
+;; This program 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.
+
+;; This program 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 this program. If not, see .
+
+;;; Commentary:
+;;
+;; Misc utility functions for GDScript mode.
+;;
+
+;;; Code:
+
+(require 'gdscript-syntax)
+(require 'gdscript-customization)
+
+(declare-function projectile-completing-read (prompt collections) "ext:projectile.el")
+
+(defun gdscript--util-goto-line (line-number)
+ "Move point to LINE-NUMBER."
+ (goto-char (point-min))
+ (forward-line (1- line-number)))
+
+(defun gdscript--util-forward-comment (&optional direction)
+ "Gdscript mode specific version of `forward-comment'.
+Optional argument DIRECTION defines the direction to move to."
+ (let ((comment-start (gdscript-syntax-context 'comment))
+ (factor (if (< (or direction 0) 0)
+ -99999
+ 99999)))
+ (when comment-start
+ (goto-char comment-start))
+ (forward-comment factor)))
+
+(defun gdscript--util-list-directories (directory &optional predicate max-depth)
+ "List DIRECTORY subdirs, filtered by PREDICATE and limited by MAX-DEPTH.
+Argument PREDICATE defaults to `identity' and must be a function
+that takes one argument (a full path) and returns non-nil for
+allowed files. When optional argument MAX-DEPTH is non-nil, stop
+searching when depth is reached, else don't limit."
+ (let* ((dir (expand-file-name directory))
+ (dir-length (length dir))
+ (predicate (or predicate #'identity))
+ (to-scan (list dir))
+ (tally nil))
+ (while to-scan
+ (let ((current-dir (car to-scan)))
+ (when (funcall predicate current-dir)
+ (setq tally (cons current-dir tally)))
+ (setq to-scan (append (cdr to-scan)
+ (gdscript--util-list-files
+ current-dir #'file-directory-p)
+ nil))
+ (when (and max-depth
+ (<= max-depth
+ (length (split-string
+ (substring current-dir dir-length)
+ "/\\|\\\\" t))))
+ (setq to-scan nil))))
+ (nreverse tally)))
+
+(defun gdscript--util-list-files (dir &optional predicate)
+ "List files in DIR, filtering with PREDICATE.
+Argument PREDICATE defaults to `identity' and must be a function
+that takes one argument (a full path) and returns non-nil for
+allowed files."
+ (let ((dir-name (file-name-as-directory dir)))
+ (apply #'nconc
+ (mapcar (lambda (file-name)
+ (let ((full-file-name (expand-file-name file-name dir-name)))
+ (when (and
+ (not (member file-name '("." "..")))
+ (funcall (or predicate #'identity) full-file-name))
+ (list full-file-name))))
+ (directory-files dir-name)))))
+
+
+(defun gdscript-util--find-project-configuration-file (&optional start-path)
+ "Return the path to the file \"project.godot\".
+
+Start the search from START-PATH if provided. Otherwise, the search
+starts from the current buffer path.
+
+WARNING: the Godot project must exist for this function to work."
+ (let* ((base-path (or start-path default-directory))
+ (dominating-file
+ (locate-dominating-file base-path
+ (lambda (parent)
+ (directory-files parent t "project.godot")))))
+ (when dominating-file (expand-file-name dominating-file))))
+
+(defun gdscript-util--get-godot-project-name ()
+ "Retrieve the project name from Godot's configuration file."
+ (with-temp-buffer
+ (insert-file-contents (concat (gdscript-util--find-project-configuration-file) "project.godot"))
+ (goto-char (point-min))
+ (if (re-search-forward "config/name=\"\\([^\"]*\\)\"" nil t)
+ (match-string 1)
+ (error "Could not find the name of the project"))))
+
+(defun gdscript-util--get-godot-project-version ()
+ "Retrieve the project name from Godot's configuration file."
+ (with-temp-buffer
+ (insert-file-contents (concat (gdscript-util--find-project-configuration-file) "project.godot"))
+ (goto-char (point-min))
+ (if (re-search-forward "config_version=\\(3\\|4\\)" nil t) ; Godot 3.0 uses 3, Godot 3.6.2 uses 4
+ "3"
+ (if (re-search-forward "config/features=PackedStringArray\(\"\\([^,\)]*\\)\"" nil t)
+ (let ((version (match-string 1)))
+ (if (string-prefix-p "4.0" version) "4" version))
+ (error "Could not find the project version")))))
+
+(defun gdscript-util--get-godot-buffer-name (&optional editor)
+ "Return buffer name for godot's stdout/stderr output."
+ (format (if editor "*godot - %s - Editor*" "*godot - %s*") (gdscript-util--get-godot-project-name)))
+
+(defun gdscript-util--get-gdformat-buffer-name ()
+ "Return buffer name for godot's stdout/stderr output."
+ (format "*gdformat - %s*" (gdscript-util--get-godot-project-name)))
+
+(defun gdscript-util--get-godot-project-file-path-relative (file-path)
+ "Return the relative path of `FILE-PATH' to Godot's configuration file."
+ (let ((project-configuration-file (gdscript-util--find-project-configuration-file)))
+ (when project-configuration-file
+ (concat (file-name-sans-extension
+ (file-relative-name file-path project-configuration-file))))))
+
+(defun gdscript-util--flatten (xs)
+ "Flatten deeply nested list.
+
+For example:
+> (gdscript-util--flatten (list 1 2 (list 3 (list 4 5)) nil))
+> (1 2 3 4 5)
+"
+ (cond
+ ((null xs) nil)
+ ((listp xs) (append (gdscript-util--flatten (car xs)) (gdscript-util--flatten (cdr xs))))
+ (t (list xs))))
+
+(defun gdscript-util--read (items &optional prompt)
+ "Let's choose single item from ITEMS from mini-buffer.
+PROMPT is prompt for read command. Return `nil' if user aborts."
+ (let* ((p (if prompt prompt "Options"))
+ (result (cond ((featurep 'projectile)
+ (projectile-completing-read (format "%s: " p) items))
+ (ido-mode
+ (ido-completing-read (format "%s: " p) items))
+ (t
+ (completing-read (format "%s: " p) items nil t)))))
+ (if quit-flag nil result)))
+
+(defmacro gdscript-util--with-available-hydra (&rest body)
+ ""
+ `(progn
+ (when (not (featurep 'hydra))
+ (error "No `hydra.el' available. To execute `gdscript-hydra-show' command you need to install hydra.el"))
+ ,@body))
+
+(provide 'gdscript-utils)
+;;; gdscript-utils.el ends here