From c8e895930b6157454640c08676b0a595cc915e9f Mon Sep 17 00:00:00 2001 From: Ben Finney Date: Wed, 4 Oct 2023 13:34:24 +1100 Subject: [PATCH 1/3] Specify EditorConfig settings for this code base. --- .editorconfig | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9aa14f0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,46 @@ +# debian/.editorconfig +# EditorConfig settings for Debian packaging files. +# Documentation: . + +# Is this the project-root configuration file? +root = true + + +# Match all file names, unless more specific match later. +[*] + +# Text encoding name. +charset = utf-8 + +# Character to use for indentation (“tab” for U+0009, “space” for U+0020). +indent_style = space + +# How many columns (integer) for each indentation level. +indent_size = 4 + +# How many columns (integer) to represent a U+0009 TAB. +tab_width = 8 + +# How many characters (integer) before enforcing line wrap. +max_line_length = 79 + +# Remove trailing whitespace on each line? +trim_trailing_whitespace = true + +# End-of-line style (“lf”, “cr”, “crlf”). +end_of_line = lf + +# Ensure file ends with a line break? +insert_final_newline = true + + +[{*.mk,Makefile,rules}] +indent_style = tab + + +# Local variables: +# coding: utf-8 +# mode: conf-editorconfig +# mode: conf +# End: +# vim: fileencoding=utf-8 filetype=dosini : From 419787d78d1c987a4d12051882453a5772fc4072 Mon Sep 17 00:00:00 2001 From: John Charman <102669158+charjr@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:19:06 +0100 Subject: [PATCH 2/3] Reduce strictness of editorconfig The purpose of this set of commits is to use utf-8 encoding. So I do not want to overstep the scope. Also indent size does not play nicely with elisp indentation, which has a lot of contextually different indentation levels: i.e. `let' and `if' statements. --- .editorconfig | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9aa14f0..3cf1000 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,46 +1,25 @@ -# debian/.editorconfig -# EditorConfig settings for Debian packaging files. # Documentation: . -# Is this the project-root configuration file? +# Stop searching for .editorconfig files after reading this one. root = true # Match all file names, unless more specific match later. [*] - -# Text encoding name. -charset = utf-8 - -# Character to use for indentation (“tab” for U+0009, “space” for U+0020). indent_style = space - -# How many columns (integer) for each indentation level. -indent_size = 4 - -# How many columns (integer) to represent a U+0009 TAB. -tab_width = 8 - -# How many characters (integer) before enforcing line wrap. -max_line_length = 79 - -# Remove trailing whitespace on each line? -trim_trailing_whitespace = true - -# End-of-line style (“lf”, “cr”, “crlf”). -end_of_line = lf - -# Ensure file ends with a line break? 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 -# mode: conf-editorconfig -# mode: conf # End: # vim: fileencoding=utf-8 filetype=dosini : From f14ae6f363b0f8893263c995ca732eb03800e78f Mon Sep 17 00:00:00 2001 From: John Charman <102669158+charjr@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:22:07 +0100 Subject: [PATCH 3/3] Ensure all files use UTF-8 Encoding --- gdscript-customization.el | 312 ++-- gdscript-imenu.el | 366 ++--- gdscript-indent-and-nav.el | 2844 ++++++++++++++++++------------------ gdscript-rx.el | 152 +- gdscript-syntax.el | 376 ++--- gdscript-utils.el | 350 ++--- 6 files changed, 2200 insertions(+), 2200 deletions(-) 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