diff --git a/.gitignore b/.gitignore index 7fdbd37..15fdd9a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ yarn-error.log .node-version .shadow-cljs .nrepl-port -nohup.out \ No newline at end of file +nohup.out +lib/**/*.elc \ No newline at end of file diff --git a/lib/extensions/godot/godot-gdscript.el b/lib/extensions/godot/godot-gdscript.el new file mode 100644 index 0000000..6202318 --- /dev/null +++ b/lib/extensions/godot/godot-gdscript.el @@ -0,0 +1,3983 @@ +;;; godot-gdscript.el --- Major mode for editing Godot Engine GDScript files + +;; Original code Python Mode (from `python.el'): +;; Copyright (C) 2003--2015 Free Software Foundation, Inc. +;; Godot-GDScript Mode: +;; Copyright (C) 2015--2017 Franco Eusébio Garcia + +;; Author: Franco Eusébio Garcia +;; URL: https://github.com/francogarcia/godot-gdscript.el +;; Version: 0.0.1 +;; Keywords: languages + +;;; License: + +;; This file not shipped as part of GNU Emacs. + +;; 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: + +;; This is a draft to add support for GDScript in Emacs. GDScript is the +;; language which Godot Game Engine uses to prototype and implement games. Godot +;; is an open-source game engine, available at: . + +;; The mode uses Fabián E. Gallina's `python.el' as the basis and reference for +;; the implementation, due to the similarities between GDScript and Python +;; syntax. However, as some keywords and operators do differ, `python-mode' is +;; not derived; instead, its code is changed to support the GDScript language. + +;; Package-Requires: ((emacs "24.3")) + +;;; Code: + +(require 'ansi-color) +(require 'cl-lib) +(require 'comint) +(require 'json) + +;; Avoid compiler warnings (disable due to package-lint-current-buffer). +;; (defvar view-return-to-alist) +;; (defvar compilation-error-regexp-alist) +;; (defvar outline-heading-end-regexp) +;; (defvar ffap-alist) +;; (defvar electric-indent-inhibit) +;; (autoload 'comint-mode "comint") + +;;;###autoload +(add-to-list 'auto-mode-alist (cons (purecopy "\\.gd\\'") 'godot-gdscript-mode)) +;;;###autoload +(add-to-list 'interpreter-mode-alist (cons (purecopy "godot-gdscript[0-9.]*") 'godot-gdscript-mode)) + +(defgroup godot-gdscript nil + "Godot Engine GDScript Language support for developing games using Emacs." + :group 'languages + :version "24.3" + :link '(emacs-commentary-link "godot-gdscript")) + +;;; Bindings + +(defvar godot-gdscript-mode-map + (let ((map (make-sparse-keymap))) + ;; Movement + (define-key map [remap backward-sentence] 'godot-gdscript-nav-backward-block) + (define-key map [remap forward-sentence] 'godot-gdscript-nav-forward-block) + (define-key map [remap backward-up-list] 'godot-gdscript-nav-backward-up-list) + (define-key map "\C-c\C-j" 'imenu) + ;; Indent specific + (define-key map "\177" 'godot-gdscript-indent-dedent-line-backspace) + (define-key map (kbd "") 'godot-gdscript-indent-dedent-line) + (define-key map "\C-c<" 'godot-gdscript-indent-shift-left) + (define-key map "\C-c>" 'godot-gdscript-indent-shift-right) + ;; Skeletons + (define-key map "\C-c\C-tc" 'godot-gdscript-skeleton-class) + (define-key map "\C-c\C-td" 'godot-gdscript-skeleton-def) + (define-key map "\C-c\C-tf" 'godot-gdscript-skeleton-for) + (define-key map "\C-c\C-ti" 'godot-gdscript-skeleton-if) + (define-key map "\C-c\C-tt" 'godot-gdscript-skeleton-try) + (define-key map "\C-c\C-tw" 'godot-gdscript-skeleton-while) + ;; Shell interaction + (define-key map "\C-c\C-g" 'godot-gdscript-run-godot-editor) + (define-key map "\C-c\C-p" 'godot-gdscript-run-project-in-godot) + (define-key map "\C-c\C-s" 'godot-gdscript-run-current-scene-in-godot) + (define-key map "\C-c\C-e" 'godot-gdscript-edit-current-scene-in-godot) + (define-key map "\C-c\C-r" 'godot-gdscript-run-current-script-in-godot) + (define-key map "\C-c\C-dp" 'godot-gdscript-run-project-in-godot-debug-mode) + (define-key map "\C-c\C-ds" 'godot-gdscript-run-current-scene-in-godot-debug-mode) + (define-key map "\C-c\C-z" 'godot-gdscript-shell-switch-to-shell) + ;; Some util commands + (define-key map "\C-c\C-v" 'godot-gdscript-check) + ;; Utilities + (substitute-key-definition 'complete-symbol 'completion-at-point + map global-map) + (easy-menu-define godot-gdscript-menu map "Godot-Gdscript Mode menu" + `("Godot-Gdscript" + :help "Godot-Gdscript-specific Features" + ["Shift region left" godot-gdscript-indent-shift-left :active mark-active + :help "Shift region left by a single indentation step"] + ["Shift region right" godot-gdscript-indent-shift-right :active mark-active + :help "Shift region right by a single indentation step"] + "-" + ["Start of def/class" beginning-of-defun + :help "Go to start of outermost definition around point"] + ["End of def/class" end-of-defun + :help "Go to end of definition around point"] + ["Mark def/class" mark-defun + :help "Mark outermost definition around point"] + ["Jump to def/class" imenu + :help "Jump to a class or function definition"] + "--" + ("Skeletons") + "---" + ["Switch to shell" godot-gdscript-shell-switch-to-shell + :help "Switch to running inferior Godot-Gdscript process"] + ["Run Godot Engine editor" godot-gdscript-run-godot-editor + :help "Run Godot Editor as a subprocess of Emacs, loading this project into it"] + ["Run project" godot-gdscript-run-project-in-godot + :help "Run the current project in Godot Engine"] + ["Run current scene" godot-gdscript-run-current-scene-in-godot + :help "Run the current scene in Godot Engine"] + ["Edit scene in Godot Engine" godot-gdscript-edit-current-scene-in-godot + :help "Edit the current scene in Godot Engine"] + ["Run current script" godot-gdscript-run-current-script-in-godot + :help "Run the current script in Godot Engine"] + ["Run project (debug mode)" godot-gdscript-run-project-in-godot-debug-mode + :help "Run the project in Godot Engine, using the debug mode option"] + ["Run scene (debug mode)" godot-gdscript-run-current-scene-in-godot-debug-mode + :help "Run the current scene in Godot Engine, using the debug mode option"] + "----" + ["Check file" godot-gdscript-check + :help "Check file for errors"] + ["Complete symbol" completion-at-point + :help "Complete symbol before point"])) + map) + "Keymap for function `godot-gdscript-mode'.") + + + + + +;;; Godot-Gdscript specialized rx + +(eval-when-compile + (defconst godot-gdscript-rx-constituents + `((block-start . ,(rx symbol-start + (or "class" "elif" "else" "except" "finally" "for" + "func" "if" "try" "while" "with" + ;; Multiplayer stuff + "puppet" "master" "remote" "remotesync") + symbol-end)) + (dedenter . ,(rx symbol-start + (or "elif" "else" "except" "finally") + symbol-end)) + (block-ender . ,(rx symbol-start + (or + "break" "continue" "pass" "raise" "return") + symbol-end)) + (decorator . ,(rx line-start + (* space) ?@ (any letter ?_) + (* (any word ?_)))) + (defun . ,(rx symbol-start (or "func" "class") symbol-end)) + (if-name-main . ,(rx line-start "if" (+ space) "__name__" + (+ space) "==" (+ space) + (any ?' ?\") "__main__" (any ?' ?\") + (* space) ?:)) + (symbol-name . ,(rx (any letter ?_) (* (any word ?_)))) + (variable-declaration . ,(rx (or "const" "var"))) + (open-paren . ,(rx (or "{" "[" "("))) + (close-paren . ,(rx (or "}" "]" ")"))) + (simple-operator . ,(rx (any ?+ ?- ?/ ?& ?^ ?~ ?| ?* ?< ?> ?= ?%))) + ;; FIXME: rx should support (not simple-operator). + (not-simple-operator . ,(rx + (not + (any ?+ ?- ?/ ?& ?^ ?~ ?| ?* ?< ?> ?= ?%)))) + ;; FIXME: Use regexp-opt. + (operator . ,(rx (or "+" "-" "/" "&" "^" "~" "|" "*" "<" ">" + "=" "%" "//" "<<" ">>" "<=" "!" "!=" + "==" ">=" "||" "&&" "is" "not"))) + ;; FIXME: Use regexp-opt. + (assignment-operator . ,(rx (or "=" "+=" "-=" "*=" "/=" "//=" "%=" + ">>=" "<<=" "&=" "^=" "|="))) + (string-delimiter . ,(rx (and + ;; Match even number of backslashes. + (or (not (any ?\\ ?\' ?\")) point + ;; Quotes might be preceded by a escaped quote. + (and (or (not (any ?\\)) point) ?\\ + (* ?\\ ?\\) (any ?\' ?\"))) + (* ?\\ ?\\) + ;; Match single or triple quotes of any kind. + (group (or "\"" "\"\"\"" "'" "'''"))))) + (coding-cookie . ,(rx line-start ?# (* space) + (or + ;; # coding= + (: "coding" (or ?: ?=) (* space) (group-n 1 (+ (or word ?-)))) + ;; # -*- coding: -*- + (: "-*-" (* space) "coding:" (* space) + (group-n 1 (+ (or word ?-))) (* space) "-*-"))))) + "Additional Godot-Gdscript specific sexps for `godot-gdscript-rx'") + + (defmacro godot-gdscript-rx (&rest regexps) + "Godot-Gdscript mode specialized rx macro. +This variant of `rx' supports common Godot-Gdscript named REGEXPS." + (let ((rx-constituents (append godot-gdscript-rx-constituents rx-constituents))) + (cond ((null regexps) + (error "No regexp")) + ((cdr regexps) + (rx-to-string `(and ,@regexps) t)) + (t + (rx-to-string (car regexps) t)))))) + + +;;; Font-lock and syntax + +(eval-when-compile + (defun godot-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)))) + +(defun godot-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 godot-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 godot-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)))) + +(defsubst godot-gdscript-syntax-comment-or-string-p (&optional ppss) + "Return non-nil if PPSS is inside 'comment or 'string." + (nth 8 (or ppss (syntax-ppss)))) + +(defsubst godot-gdscript-syntax-closing-paren-p () + "Return non-nil if char after point is a closing paren." + (= (syntax-class (syntax-after (point))) + (syntax-class (string-to-syntax ")")))) + +(define-obsolete-function-alias + 'godot-gdscript-info-ppss-context #'godot-gdscript-syntax-context "24.3") + +(define-obsolete-function-alias + 'godot-gdscript-info-ppss-context-type #'godot-gdscript-syntax-context-type "24.3") + +(define-obsolete-function-alias + 'godot-gdscript-info-ppss-comment-or-string-p + #'godot-gdscript-syntax-comment-or-string-p "24.3") + +(defvar godot-gdscript-font-lock-keywords + ;; Keywords + `(,(rx symbol-start + (or + "and" "in" "is" "not" "or" + "null" "self" + "String" "bool" "float" "int" + ;; Functions + ;; Variant types + "AABB" "Array" "Basis" "ByteArray" "Color" + "ColorArray" "Dictionary" "Image" "InputEvent" "IntArray" + "Matrix3" "Matrix32" "NodePath" "Object" "Plane" + "Quat" "RID" "RealArray" "Rect2" "StringArray" + "Transform" "Vector2" "Vector2Array" "Vector3" "Vector3Array" + ;; Language keywords + "assert" "break" "breakpoint" "class" "const" "continue" + "default" "do" "elif" "else" "enum" + "export" "extends" "for" "func" "if" "onready" + ;; Multiplayer stuff + "puppet" "master" "remote" "remotesync" + "pass" "preload" "resume" "return" "setget" + "signal" "static" "tool" "var" "while" "yield") + symbol-end) + ;; functions + (,(rx symbol-start "func" (1+ space) (group (1+ (or word ?_)))) + (1 font-lock-function-name-face)) + ;; classes + (,(rx symbol-start "class" (1+ space) (group (1+ (or word ?_)))) + (1 font-lock-type-face)) + ;; Constants + (,(rx symbol-start + (or + "PI" "false" "null" "true") + symbol-end) . font-lock-constant-face) + ;; Decorators. + (,(rx line-start (* (any " \t")) (group "@" (1+ (or word ?_)) + (0+ "." (1+ (or word ?_))))) + (1 font-lock-type-face)) + ;; Builtin Exceptions + (,(rx symbol-start + (or + "OK" "FAILED" + "ERR_UNAVAILABLE" "ERR_UNCONFIGURED" "ERR_UNAUTHORIZED" + "ERR_PARAMETER_RANGE_ERROR" "ERR_OUT_OF_MEMORY" "ERR_FILE_NOT_FOUND" + "ERR_FILE_BAD_DRIVE" "ERR_FILE_BAD_PATH" "ERR_FILE_NO_PERMISSION" + "ERR_FILE_ALREADY_IN_USE" "ERR_FILE_CANT_OPEN" "ERR_FILE_CANT_WRITE" + "ERR_FILE_CANT_READ" "ERR_FILE_UNRECOGNIZED" "ERR_FILE_CORRUPT" + "ERR_FILE_MISSING_DEPENDENCIES" "ERR_FILE_EOF" "ERR_CANT_OPEN" + "ERR_CANT_CREATE" "ERROR_QUERY_FAILED" "ERR_ALREADY_IN_USE" + "ERR_LOCKED" "ERR_TIMEOUT" "ERR_CANT_CONNECT" "ERR_CANT_RESOLVE" + "ERR_CONNECTION_ERROR" "ERR_CANT_AQUIRE_RESOURCE" "ERR_CANT_FORK" + "ERR_INVALID_DATA" "ERR_INVALID_PARAMETER" "ERR_ALREADY_EXISTS" + "ERR_DOES_NOT_EXIST" "ERR_DATABASE_CANT_READ" "ERR_DATABASE_CANT_WRITE" + "ERR_COMPILATION_FAILED" "ERR_METHOD_NOT_FOUND" "ERR_LINK_FAILED" + "ERR_SCRIPT_FAILED" "ERR_CYCLIC_LINK" "ERR_INVALID_DECLARATION" + "ERR_DUPLICATE_SYMBOL" "ERR_PARSE_ERROR" "ERR_BUSY" + "ERR_SKIP" "ERR_HELP" "ERR_BUG" "ERR_PRINTER_ON_FIRE" + "ERR_OMFG_THIS_IS_VERY_VERY_BAD" "ERR_WTF") + symbol-end) . font-lock-type-face) + ;; Builtins + (,(rx symbol-start + (or + ;; Inherited methods from Object + "connect" "emit" "get" "set_signal" + ;; Inherited methods from Node + ;; get_node() shorthand + ;; + "$" + ;; () + "_draw" "_enter_tree" "_enter_world" "_exit_tree" "_exit_world" + "_fixed_process" "_init" "_input" "_input" "_physics_process" + "_process" "_process" "_ready" "_unhandled_input" + "_unhandled_input" "_unhandled_key_input" "_unhandled_key_input" + ;; () + "add_child" "add_to_group" "can_process" "duplicate" "find_node" + "get_child" "get_child_count" "get_children" "get_filename" + "get_groups" "get_index" "get_name" "get_node" + "get_node_and_resource" "get_owner" "get_parent" "get_path" + "get_path_to" "get_pause_mode" "get_physics_process_delta_time" + "get_position_in_parent" "get_process_delta_time" + "get_scene_instance_load_placeholder" "get_tree" "get_viewport" + "has_node" "has_node_and_resource" "is_a_parent_of" + "is_displayed_folded" "is_greater_than" "is_in_group" + "is_inside_tree" "is_physics_processing" + "is_physics_processing_internal" "is_processing" + "is_processing_input" "is_processing_internal" + "is_processing_unhandled_input" + "is_processing_unhandled_key_input" + "move_child" "print_stray_nodes" "print_tree" "print_tree_pretty" + "propagate_call" "propagate_notification" "queue_free" "raise" + "remove_and_skip" "remove_child" "remove_from_group" "replace_by" + "request_ready" "set_display_folded" "set_filename" "set_name" + "set_owner" "set_pause_mode" "set_physics_process" + "set_physics_process_internal" "set_process" "set_process_input" + "set_process_internal" "set_process_unhandled_input" + "set_process_unhandled_key_input" + "set_scene_instance_load_placeholder" + ;; Missing functions from header + "basefunc" "call" "new" "instance" + ;; Exported functions + ;; () + "Color8" "ColorN" "abs" "acos" "asin" "atan" "atan2" "bytes2var" + "cartesian2polar" "ceil" "char" "clamp" "convert" "cos" "cosh" + "db2linear" "decimals" "dectime" "deg2rad" "dict2inst" "ease" + "exp" "floor" "fmod" "fposmod" "funcref" "hash" "inst2dict" + "instance_from_id" "inverse_lerp" "is_inf" "is_instance_valid" + "is_nan" "len" "lerp" "linear2db" "load" "log" "max" "min" + "nearest_po2" "parse_json" "polar2cartesian" "pow" "print" + "print_stack" "printerr" "printraw" "prints" "printt" "rad2deg" + "rand_range" "rand_seed" "randf" "randi" "randomize" "range" + "range_lerp" "round" "seed" "sign" "sinh" "sqrt" "stepify" "str" + "str2var" "tan" "tanh" "to_json" "type_exists" "typeof" + "validate_json" "var2bytes" "var2str" "weakref" "wrapf" "wrapi" + "sin") + symbol-end) . font-lock-builtin-face) + ;; assignments + ;; support for a = b = c = 5 + (,(lambda (limit) + (let ((re (godot-gdscript-rx (group (+ (any word ?. ?_))) + (? ?\[ (+ (not (any ?\]))) ?\]) (* space) + assignment-operator)) + (res nil)) + (while (and (setq res (re-search-forward re limit t)) + (or (godot-gdscript-syntax-context 'paren) + (equal (char-after (point-marker)) ?=)))) + res)) + (1 font-lock-variable-name-face nil nil)) + ;; support for a, b, c = (1, 2, 3) + (,(lambda (limit) + (let ((re (godot-gdscript-rx (group (+ (any word ?. ?_))) (* space) + (* ?, (* space) (+ (any word ?. ?_)) (* space)) + ?, (* space) (+ (any word ?. ?_)) (* space) + assignment-operator)) + (res nil)) + (while (and (setq res (re-search-forward re limit t)) + (goto-char (match-end 1)) + (godot-gdscript-syntax-context 'paren))) + res)) + (1 font-lock-variable-name-face nil nil)))) + +(defconst godot-gdscript-syntax-propertize-function + (syntax-propertize-rules + ((godot-gdscript-rx string-delimiter) + (0 (ignore (godot-gdscript-syntax-stringify)))))) + +(defsubst godot-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 godot-gdscript-syntax-stringify () + "Put `syntax-table' property correctly on single/triple quotes." + (let* ((num-quotes (length (match-string-no-properties 1))) + (ppss (prog2 + (backward-char num-quotes) + (syntax-ppss) + (forward-char num-quotes))) + (string-start (and (not (nth 4 ppss)) (nth 8 ppss))) + (quote-starting-pos (- (point) num-quotes)) + (quote-ending-pos (point)) + (num-closing-quotes + (and string-start + (godot-gdscript-syntax-count-quotes + (char-before) string-start quote-starting-pos)))) + (cond ((and string-start (= num-closing-quotes 0)) + ;; This set of quotes doesn't match the string starting + ;; kind. Do nothing. + nil) + ((not 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 "|"))) + ((= num-quotes num-closing-quotes) + ;; 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 "|"))) + ((> num-quotes num-closing-quotes) + ;; This may only happen whenever a triple quote is closing + ;; a single quoted string. Add string delimiter syntax to + ;; all three quotes. + (put-text-property quote-starting-pos quote-ending-pos + 'syntax-table (string-to-syntax "|")))))) + +(defvar godot-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 Godot-Gdscript files.") + +(defvar godot-gdscript-dotty-syntax-table + (let ((table (make-syntax-table godot-gdscript-mode-syntax-table))) + (modify-syntax-entry ?. "w" table) + (modify-syntax-entry ?_ "w" table) + table) + "Dotty syntax table for Godot-Gdscript files. +It makes underscores and dots word constituent chars.") + +;;; Indentation + +(defcustom godot-gdscript-indent-offset 4 + "Default indentation offset for Godot-Gdscript." + :group 'godot-gdscript + :type 'integer + :safe 'integerp) + +(defcustom godot-gdscript-indent-guess-indent-offset t + "Non-nil tells Godot-Gdscript mode to guess `godot-gdscript-indent-offset' value." + :type 'boolean + :group 'godot-gdscript + :safe 'booleanp) + +(defcustom godot-gdscript-indent-trigger-commands + '(indent-for-tab-command yas-expand yas/expand) + "Commands that might trigger a `godot-gdscript-indent-line' call." + :type '(repeat symbol) + :group 'godot-gdscript) + +(define-obsolete-variable-alias + 'godot-gdscript-indent 'godot-gdscript-indent-offset "24.3") + +(define-obsolete-variable-alias + 'godot-gdscript-guess-indent 'godot-gdscript-indent-guess-indent-offset "24.3") + +(defvar godot-gdscript-indent-current-level 0 + "Deprecated var available for compatibility.") + +(defvar godot-gdscript-indent-levels '(0) + "Deprecated var available for compatibility.") + +(make-obsolete-variable + 'godot-gdscript-indent-current-level + "The indentation API changed to avoid global state. +The function `godot-gdscript-indent-calculate-levels' does not use it +anymore. If you were defadvising it and or depended on this +variable for indentation customizations, refactor your code to +work on `godot-gdscript-indent-calculate-indentation' instead." + "24.5") + +(make-obsolete-variable + 'godot-gdscript-indent-levels + "The indentation API changed to avoid global state. +The function `godot-gdscript-indent-calculate-levels' does not use it +anymore. If you were defadvising it and or depended on this +variable for indentation customizations, refactor your code to +work on `godot-gdscript-indent-calculate-indentation' instead." + "24.5") + +(defun godot-gdscript-indent-guess-indent-offset () + "Guess and set `godot-gdscript-indent-offset' for the current buffer." + (interactive) + (save-excursion + (save-restriction + (widen) + (goto-char (point-min)) + (let ((block-end)) + (while (and (not block-end) + (re-search-forward + (godot-gdscript-rx line-start block-start) nil t)) + (when (and + (not (godot-gdscript-syntax-context-type)) + (progn + (goto-char (line-end-position)) + (godot-gdscript-util-forward-comment -1) + (if (equal (char-before) ?:) + t + (forward-line 1) + (when (godot-gdscript-info-block-continuation-line-p) + (while (and (godot-gdscript-info-continuation-line-p) + (not (eobp))) + (forward-line 1)) + (godot-gdscript-util-forward-comment -1) + (when (equal (char-before) ?:) + t))))) + (setq block-end (point-marker)))) + (let ((indentation + (when block-end + (goto-char block-end) + (godot-gdscript-util-forward-comment) + (current-indentation)))) + (if (and indentation (not (zerop indentation))) + (set (make-local-variable 'godot-gdscript-indent-offset) indentation) + (message "Can't guess godot-gdscript-indent-offset, using defaults: %s" + godot-gdscript-indent-offset))))))) + +(defun godot-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." + (save-restriction + (widen) + (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 (godot-gdscript-syntax-context 'string ppss))) + (when start + (cons :inside-string start)))) + ;; Inside a paren. + ((let* ((start (godot-gdscript-syntax-context 'paren ppss)) + (starts-in-newline + (when start + (save-excursion + (goto-char start) + (forward-char) + (not + (= (line-number-at-pos) + (progn + (godot-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 (godot-gdscript-syntax-closing-paren-p) + (progn + (forward-char 1) + (not (godot-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) + (godot-gdscript-syntax-closing-paren-p)) + (cons :inside-paren-at-closing-nested-paren start)) + ;; This line starts from a opening block in its own line. + ((save-excursion + (goto-char start) + (when (and + starts-in-newline + (save-excursion + (back-to-indentation) + (looking-at (godot-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 (when (not (godot-gdscript-syntax-comment-or-string-p ppss)) + (godot-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)) + (godot-gdscript-syntax-context 'paren)))) + (goto-char (line-end-position)) + (while (and (search-backward + "." (line-beginning-position) t) + (godot-gdscript-syntax-context-type))) + ;; Ensure previous statement has dot to align with. + (when (and (eq (char-after) ?\.) + (not (godot-gdscript-syntax-context-type))) + (cons :after-backslash-dotted-continuation (point)))))) + ;; Continuation of block definition. + ((let ((block-continuation-start + (godot-gdscript-info-block-continuation-line-p))) + (when block-continuation-start + (save-excursion + (goto-char block-continuation-start) + (re-search-forward + (godot-gdscript-rx block-start (* space)) + (line-end-position) t) + (cons :after-backslash-block-continuation (point)))))) + ;; Continuation of assignment. + ((let ((assignment-continuation-start + (godot-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 (godot-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) + (godot-gdscript-util-forward-comment -1) + (when (equal (char-before) ?:) + (godot-gdscript-nav-beginning-of-block))))) + (when start + (cons :after-block-start start)))) + ;; At dedenter statement. + ((let ((start (godot-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") + (godot-gdscript-nav-beginning-of-statement) + (cons + (cond ((godot-gdscript-info-current-line-comment-p) + :after-comment) + ((save-excursion + (goto-char (line-end-position)) + (godot-gdscript-util-forward-comment -1) + (godot-gdscript-nav-beginning-of-statement) + (looking-at (godot-gdscript-rx block-ender))) + :after-block-end) + (t :after-line)) + (point)))))))) + +(defun godot-gdscript-indent--calculate-indentation () + "Internal implementation of `godot-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-restriction + (widen) + (save-excursion + (pcase (godot-gdscript-indent-context) + (`(:no-indent . ,_) 0) + (`(,(or :after-line + :after-comment + :inside-string + :after-backslash + :inside-paren-at-closing-paren + :inside-paren-at-closing-nested-paren) . ,start) + ;; Copy previous indentation. + (goto-char start) + (current-indentation)) + (`(,(or :after-block-start + :after-backslash-first-line + :inside-paren-newline-start) . ,start) + ;; Add one indentation level. + (goto-char start) + (+ (current-indentation) godot-gdscript-indent-offset)) + (`(,(or :inside-paren + :after-backslash-block-continuation + :after-backslash-assignment-continuation + :after-backslash-dotted-continuation) . ,start) + ;; Use the column given by the context. + (goto-char start) + (current-column)) + (`(:after-block-end . ,start) + ;; Subtract one indentation level. + (goto-char start) + (- (current-indentation) godot-gdscript-indent-offset)) + (`(:at-dedenter-block-start . ,_) + ;; List all possible indentation levels from opening blocks. + (let ((opening-block-start-points + (godot-gdscript-info-dedenter-opening-block-positions))) + (if (not opening-block-start-points) + 0 ; 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) + ;; Add two indentation levels to make the suite stand out. + (goto-char start) + (+ (current-indentation) (* godot-gdscript-indent-offset 2))))))) + +(defun godot-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) #'<) + (let* ((remainder (% indentation godot-gdscript-indent-offset)) + (steps (/ (- indentation remainder) godot-gdscript-indent-offset)) + (levels (mapcar (lambda (step) + (* godot-gdscript-indent-offset step)) + (number-sequence steps 0 -1)))) + (reverse + (if (not (zerop remainder)) + (cons indentation levels) + levels))))) + +(defun godot-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 godot-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 (godot-gdscript-indent--calculate-indentation)) + (levels (godot-gdscript-indent--calculate-levels indentation))) + (if previous + (godot-gdscript-indent--previous-level levels (current-indentation)) + (apply #'max levels)))) + +(defun godot-gdscript-indent-line (&optional previous) + "Internal implementation of `godot-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 + ;; Check if point is within indentation. + (and (<= (line-beginning-position) (point)) + (>= (+ (line-beginning-position) + (current-indentation)) + (point))))) + (save-excursion + (indent-line-to + (godot-gdscript-indent-calculate-indentation previous)) + (godot-gdscript-info-dedenter-opening-block-message)) + (when follow-indentation-p + (back-to-indentation)))) + +(defun godot-gdscript-indent-calculate-levels () + "Return possible indentation levels." + (godot-gdscript-indent--calculate-levels + (godot-gdscript-indent--calculate-indentation))) + +(defun godot-gdscript-indent-line-function () + "`indent-line-function' for Godot-Gdscript mode. +When the variable `last-command' is equal to one of the symbols +inside `godot-gdscript-indent-trigger-commands' it cycles possible +indentation levels from right to left." + (godot-gdscript-indent-line + (and (memq this-command godot-gdscript-indent-trigger-commands) + (eq last-command this-command)))) + +(defun godot-gdscript-indent-dedent-line () + "De-indent current line." + (interactive "*") + (when (and (not (bolp)) + (not (godot-gdscript-syntax-comment-or-string-p)) + (= (current-indentation) (current-column))) + (godot-gdscript-indent-line t) + t)) + +(defun godot-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 (godot-gdscript-indent-dedent-line) + (backward-delete-char-untabify arg))) + +(put 'godot-gdscript-indent-dedent-line-backspace 'delete-selection 'supersede) + +(defun godot-gdscript-indent-region (start end) + "Indent a Godot-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 + (godot-gdscript-info-current-line-comment-p))) + (forward-line -1) + (not + (or (and (godot-gdscript-info-current-line-comment-p) + ;; Unless this line is a comment too. + (not line-is-comment-p)) + (godot-gdscript-info-current-line-empty-p))))) + ;; Don't mess with strings, unless it's the + ;; enclosing set of quotes. + (or (not (godot-gdscript-syntax-context 'string)) + (eq + (syntax-after + (+ (1- (point)) + (current-indentation) + (godot-gdscript-syntax-count-quotes (char-after) (point)))) + (string-to-syntax "|"))) + ;; Skip if current line is a block start, a + ;; dedenter or block ender. + (save-excursion + (back-to-indentation) + (not (looking-at + (godot-gdscript-rx + (or block-start dedenter block-ender)))))) + (godot-gdscript-indent-line))) + (forward-line 1)) + (move-marker end nil)))) + +(defun godot-gdscript-indent-shift-left (start end &optional count) + "Shift lines contained in region START END by COUNT columns to the left. +COUNT defaults to `godot-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 godot-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]*$"))) + (error "Can't shift all lines enough")) + (forward-line)) + (indent-rigidly start end (- count)))))) + +(add-to-list 'debug-ignored-errors "^Can't shift all lines enough") + +(defun godot-gdscript-indent-shift-right (start end &optional count) + "Shift lines contained in region START END by COUNT columns to the right. +COUNT defaults to `godot-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) + godot-gdscript-indent-offset)) + (indent-rigidly start end count))) + +(defun godot-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)) + (cond + ;; Electric indent inside parens + ((and + (not (bolp)) + (let ((paren-start (godot-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 (godot-gdscript-indent-calculate-indentation))) + (when (< (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 (godot-gdscript-syntax-comment-or-string-p))) + ;; Just re-indent dedenters + (let ((dedenter-pos (godot-gdscript-info-dedenter-statement-p)) + (current-pos (point))) + (when dedenter-pos + (save-excursion + (goto-char dedenter-pos) + (godot-gdscript-indent-line) + (unless (= (line-number-at-pos dedenter-pos) + (line-number-at-pos current-pos)) + ;; Reindent region if this is a multiline statement + (godot-gdscript-indent-region dedenter-pos current-pos))))))))) + +;;; Navigation + +(defvar godot-gdscript-nav-beginning-of-defun-regexp + (godot-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 godot-gdscript-nav--beginning-of-defun (&optional arg) + "Internal implementation of `godot-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 (godot-gdscript-info-looking-at-beginning-of-defun)) + (godot-gdscript-nav-backward-block))) + (or (and (godot-gdscript-info-looking-at-beginning-of-defun) + (+ (current-indentation) godot-gdscript-indent-offset)) + 0)))) + (found + (progn + (when (and (< arg 0) + (godot-gdscript-info-looking-at-beginning-of-defun)) + (end-of-line 1)) + (while (and (funcall re-search-fn + godot-gdscript-nav-beginning-of-defun-regexp nil t) + (or (godot-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 (godot-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 godot-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 + (godot-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 godot-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 (godot-gdscript-info-looking-at-beginning-of-defun) + (godot-gdscript-nav-beginning-of-defun 1) + (godot-gdscript-nav-beginning-of-defun -1)) + (setq beg-defun-indent (current-indentation)) + (while (progn + (godot-gdscript-nav-end-of-statement) + (godot-gdscript-util-forward-comment 1) + (and (> (current-indentation) beg-defun-indent) + (not (eobp))))) + (godot-gdscript-util-forward-comment -1) + (forward-line 1) + ;; Ensure point moves forward. + (and (> beg-pos (point)) (goto-char beg-pos))))) + +(defun godot-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 +`godot-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 'godot-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 (when (not newpos) (goto-char start-pos)) + (throw 'found nil)))))))) + +(defun godot-gdscript-nav--forward-defun (arg) + "Internal implementation of godot-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 + (godot-gdscript-nav--syntactically + (lambda () + (re-search-forward + godot-gdscript-nav-beginning-of-defun-regexp nil t)) + '>))) + (setq arg (1- arg))) + (while (and (< arg 0) + (setq found + (godot-gdscript-nav--syntactically + (lambda () + (re-search-backward + godot-gdscript-nav-beginning-of-defun-regexp nil t)) + '<))) + (setq arg (1+ arg))) + found)) + +(defun godot-gdscript-nav-backward-defun (&optional arg) + "Navigate to closer defun backward ARG times. +Unlikely `godot-gdscript-nav-beginning-of-defun' this doesn't care about +nested definitions." + (interactive "^p") + (godot-gdscript-nav--forward-defun (- (or arg 1)))) + +(defun godot-gdscript-nav-forward-defun (&optional arg) + "Navigate to closer defun forward ARG times. +Unlikely `godot-gdscript-nav-beginning-of-defun' this doesn't care about +nested definitions." + (interactive "^p") + (godot-gdscript-nav--forward-defun (or arg 1))) + +(defun godot-gdscript-nav-beginning-of-statement () + "Move to start of current statement." + (interactive "^") + (back-to-indentation) + (let* ((ppss (syntax-ppss)) + (context-point + (or + (godot-gdscript-syntax-context 'paren ppss) + (godot-gdscript-syntax-context 'string ppss)))) + (cond ((bobp)) + (context-point + (goto-char context-point) + (godot-gdscript-nav-beginning-of-statement)) + ((save-excursion + (forward-line -1) + (godot-gdscript-info-line-ends-backslash-p)) + (forward-line -1) + (godot-gdscript-nav-beginning-of-statement)))) + (point-marker)) + +(defun godot-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) + (while (and (or noend (goto-char (line-end-position))) + (not (eobp)) + (cond ((setq string-start (godot-gdscript-syntax-context 'string)) + (goto-char string-start) + (if (godot-gdscript-syntax-context 'paren) + ;; Ended up inside a paren, roll again. + (godot-gdscript-nav-end-of-statement t) + ;; This is not inside a paren, move to the + ;; end of this string. + (goto-char (+ (point) + (godot-gdscript-syntax-count-quotes + (char-after (point)) (point)))) + (or (re-search-forward (rx (syntax string-delimiter)) nil t) + (goto-char (point-max))))) + ((godot-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 (godot-gdscript-info-line-ends-backslash-p)) + (goto-char bs-pos) + (forward-line 1)))))) + (point-marker)) + +(defun godot-gdscript-nav-backward-statement (&optional arg) + "Move backward to previous statement. +With ARG, repeat. See `godot-gdscript-nav-forward-statement'." + (interactive "^p") + (or arg (setq arg 1)) + (godot-gdscript-nav-forward-statement (- arg))) + +(defun godot-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) + (godot-gdscript-nav-end-of-statement) + (godot-gdscript-util-forward-comment) + (godot-gdscript-nav-beginning-of-statement) + (setq arg (1- arg))) + (while (< arg 0) + (godot-gdscript-nav-beginning-of-statement) + (godot-gdscript-util-forward-comment -1) + (godot-gdscript-nav-beginning-of-statement) + (setq arg (1+ arg)))) + +(defun godot-gdscript-nav-beginning-of-block () + "Move to start of current block." + (interactive "^") + (let ((starting-pos (point))) + (if (progn + (godot-gdscript-nav-beginning-of-statement) + (looking-at (godot-gdscript-rx block-start))) + (point-marker) + ;; Go to first line beginning a statement + (while (and (not (bobp)) + (or (and (godot-gdscript-nav-beginning-of-statement) nil) + (godot-gdscript-info-current-line-comment-p) + (godot-gdscript-info-current-line-empty-p))) + (forward-line -1)) + (let ((block-matching-indent + (- (current-indentation) godot-gdscript-indent-offset))) + (while + (and (godot-gdscript-nav-backward-block) + (> (current-indentation) block-matching-indent))) + (if (and (looking-at (godot-gdscript-rx block-start)) + (= (current-indentation) block-matching-indent)) + (point-marker) + (and (goto-char starting-pos) nil)))))) + +(defun godot-gdscript-nav-end-of-block () + "Move to end of current block." + (interactive "^") + (when (godot-gdscript-nav-beginning-of-block) + (let ((block-indentation (current-indentation))) + (godot-gdscript-nav-end-of-statement) + (while (and (forward-line 1) + (not (eobp)) + (or (and (> (current-indentation) block-indentation) + (or (godot-gdscript-nav-end-of-statement) t)) + (godot-gdscript-info-current-line-comment-p) + (godot-gdscript-info-current-line-empty-p)))) + (godot-gdscript-util-forward-comment -1) + (point-marker)))) + +(defun godot-gdscript-nav-backward-block (&optional arg) + "Move backward to previous block of code. +With ARG, repeat. See `godot-gdscript-nav-forward-block'." + (interactive "^p") + (or arg (setq arg 1)) + (godot-gdscript-nav-forward-block (- arg))) + +(defun godot-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 + (godot-gdscript-rx line-start (* whitespace) block-start)) + (starting-pos (point))) + (while (> arg 0) + (godot-gdscript-nav-end-of-statement) + (while (and + (re-search-forward block-start-regexp nil t) + (godot-gdscript-syntax-context-type))) + (setq arg (1- arg))) + (while (< arg 0) + (godot-gdscript-nav-beginning-of-statement) + (while (and + (re-search-backward block-start-regexp nil t) + (godot-gdscript-syntax-context-type))) + (setq arg (1+ arg))) + (godot-gdscript-nav-beginning-of-statement) + (if (not (looking-at (godot-gdscript-rx block-start))) + (and (goto-char starting-pos) nil) + (and (not (= (point) starting-pos)) (point-marker))))) + +(defun godot-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 godot-gdscript-nav--lisp-forward-sexp-safe (&optional arg) + "Safe version of standard `forward-sexp'. +When at end of sexp (i.e. looking at a 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) (godot-gdscript-rx close-paren) (godot-gdscript-rx open-paren))) + (search-fn + (if (> arg 0) #'re-search-forward #'re-search-backward))) + (condition-case nil + (godot-gdscript-nav--lisp-forward-sexp arg) + (error + (while (and (funcall search-fn paren-regexp nil t) + (godot-gdscript-syntax-context 'paren))))))) + +(defun godot-gdscript-nav--forward-sexp (&optional dir safe) + "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." + (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 (godot-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))) + ((or (eq context-type 'paren) + (and forward-p (looking-at (godot-gdscript-rx open-paren))) + (and (not forward-p) + (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 + (godot-gdscript-nav--lisp-forward-sexp-safe dir) + (godot-gdscript-nav--lisp-forward-sexp dir))) + (t + ;; This part handles the lispy feel of + ;; `godot-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 + ((godot-gdscript-info-beginning-of-block-p) 'block-start) + ((godot-gdscript-info-end-of-block-p) 'block-end) + ((godot-gdscript-info-beginning-of-statement-p) 'statement-start) + ((godot-gdscript-info-end-of-statement-p) 'statement-end))) + (next-sexp-pos + (save-excursion + (if safe + (godot-gdscript-nav--lisp-forward-sexp-safe dir) + (godot-gdscript-nav--lisp-forward-sexp dir)) + (point))) + (next-sexp-context + (save-excursion + (goto-char next-sexp-pos) + (cond + ((godot-gdscript-info-beginning-of-block-p) 'block-start) + ((godot-gdscript-info-end-of-block-p) 'block-end) + ((godot-gdscript-info-beginning-of-statement-p) 'statement-start) + ((godot-gdscript-info-end-of-statement-p) 'statement-end) + ((godot-gdscript-info-statement-starts-block-p) 'starts-block) + ((godot-gdscript-info-statement-ends-block-p) 'ends-block))))) + (if forward-p + (cond ((and (not (eobp)) + (godot-gdscript-info-current-line-empty-p)) + (godot-gdscript-util-forward-comment dir) + (godot-gdscript-nav--forward-sexp dir)) + ((eq context 'block-start) + (godot-gdscript-nav-end-of-block)) + ((eq context 'statement-start) + (godot-gdscript-nav-end-of-statement)) + ((and (memq context '(statement-end block-end)) + (eq next-sexp-context 'ends-block)) + (goto-char next-sexp-pos) + (godot-gdscript-nav-end-of-block)) + ((and (memq context '(statement-end block-end)) + (eq next-sexp-context 'starts-block)) + (goto-char next-sexp-pos) + (godot-gdscript-nav-end-of-block)) + ((memq context '(statement-end block-end)) + (goto-char next-sexp-pos) + (godot-gdscript-nav-end-of-statement)) + (t (goto-char next-sexp-pos))) + (cond ((and (not (bobp)) + (godot-gdscript-info-current-line-empty-p)) + (godot-gdscript-util-forward-comment dir) + (godot-gdscript-nav--forward-sexp dir)) + ((eq context 'block-end) + (godot-gdscript-nav-beginning-of-block)) + ((eq context 'statement-end) + (godot-gdscript-nav-beginning-of-statement)) + ((and (memq context '(statement-start block-start)) + (eq next-sexp-context 'starts-block)) + (goto-char next-sexp-pos) + (godot-gdscript-nav-beginning-of-block)) + ((and (memq context '(statement-start block-start)) + (eq next-sexp-context 'ends-block)) + (goto-char next-sexp-pos) + (godot-gdscript-nav-beginning-of-block)) + ((memq context '(statement-start block-start)) + (goto-char next-sexp-pos) + (godot-gdscript-nav-beginning-of-statement)) + (t (goto-char next-sexp-pos)))))))))) + +(defun godot-gdscript-nav-forward-sexp (&optional arg) + "Move forward across expressions. +With ARG, do it that many times. Negative arg -N means move +backward N times." + (interactive "^p") + (or arg (setq arg 1)) + (while (> arg 0) + (godot-gdscript-nav--forward-sexp 1) + (setq arg (1- arg))) + (while (< arg 0) + (godot-gdscript-nav--forward-sexp -1) + (setq arg (1+ arg)))) + +(defun godot-gdscript-nav-backward-sexp (&optional arg) + "Move backward across expressions. +With ARG, do it that many times. Negative arg -N means move +forward N times." + (interactive "^p") + (or arg (setq arg 1)) + (godot-gdscript-nav-forward-sexp (- arg))) + +(defun godot-gdscript-nav-forward-sexp-safe (&optional arg) + "Move forward safely across expressions. +With ARG, do it that many times. Negative arg -N means move +backward N times." + (interactive "^p") + (or arg (setq arg 1)) + (while (> arg 0) + (godot-gdscript-nav--forward-sexp 1 t) + (setq arg (1- arg))) + (while (< arg 0) + (godot-gdscript-nav--forward-sexp -1 t) + (setq arg (1+ arg)))) + +(defun godot-gdscript-nav-backward-sexp-safe (&optional arg) + "Move backward safely across expressions. +With ARG, do it that many times. Negative arg -N means move +forward N times." + (interactive "^p") + (or arg (setq arg 1)) + (godot-gdscript-nav-forward-sexp-safe (- arg))) + +(defun godot-gdscript-nav--up-list (&optional dir) + "Internal implementation of `godot-gdscript-nav-up-list'. +DIR is always 1 or -1 and comes sanitized from +`godot-gdscript-nav-up-list' calls." + (let ((context (godot-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 (godot-gdscript-info-end-of-block-p)) + (let ((parent-end-pos + (save-excursion + (let ((indentation (and + (godot-gdscript-nav-beginning-of-block) + (current-indentation)))) + (while (and indentation + (> indentation 0) + (>= (current-indentation) indentation) + (godot-gdscript-nav-backward-block))) + (godot-gdscript-nav-end-of-block))))) + (and (> (or parent-end-pos (point)) (point)) + (goto-char parent-end-pos)))) + (forward-p (godot-gdscript-nav-end-of-block)) + ((and (not forward-p) + (> (current-indentation) 0) + (godot-gdscript-info-beginning-of-block-p)) + (let ((prev-block-pos + (save-excursion + (let ((indentation (current-indentation))) + (while (and (godot-gdscript-nav-backward-block) + (>= (current-indentation) indentation)))) + (point)))) + (and (> (point) prev-block-pos) + (goto-char prev-block-pos)))) + ((not forward-p) (godot-gdscript-nav-beginning-of-block))))) + +(defun godot-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) + (godot-gdscript-nav--up-list 1) + (setq arg (1- arg))) + (while (< arg 0) + (godot-gdscript-nav--up-list -1) + (setq arg (1+ arg)))) + +(defun godot-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)) + (godot-gdscript-nav-up-list (- arg))) + +(defun godot-gdscript-nav-if-name-main () + "Move point at the beginning the __main__ block. +When \"if __name__ == '__main__':\" is found returns its +position, else returns nil." + (interactive) + (let ((point (point)) + (found (catch 'found + (goto-char (point-min)) + (while (re-search-forward + (godot-gdscript-rx line-start + "if" (+ space) + "__name__" (+ space) + "==" (+ space) + (group-n 1 (or ?\" ?\')) + "__main__" (backref 1) (* space) ":") + nil t) + (when (not (godot-gdscript-syntax-context-type)) + (beginning-of-line) + (throw 'found t)))))) + (if found + (point) + (ignore (goto-char point))))) + +;;; Shell integration + +(defcustom godot-gdscript-shell-buffer-name "Godot-GDScript" + "Default buffer name for Godot-GDScript interpreter." + :type 'string + :group 'godot-gdscript + :safe 'stringp) + +(defcustom godot-gdscript-shell-interpreter "godot-gdscript" + "Default Godot-GDScript interpreter for shell." + :type 'string + :group 'godot-gdscript) + +(defcustom godot-gdscript-shell-internal-buffer-name "godot-gdscript internal" + "Default buffer name for the Internal Godot-GDScript interpreter." + :type 'string + :group 'godot-gdscript + :safe 'stringp) + +(defcustom godot-gdscript-shell-interpreter-args "-i" + "Default arguments for the Godot-GDScript interpreter." + :type 'string + :group 'godot-gdscript) + +(defcustom godot-gdscript-shell-interpreter-interactive-arg "-i" + "Interpreter argument to force it to run interactively." + :type 'string + :version "24.4") + +(defcustom godot-gdscript-shell-prompt-detect-enabled t + "Non-nil enables autodetection of interpreter prompts." + :type 'boolean + :safe 'booleanp + :version "24.4") + +(defcustom godot-gdscript-shell-prompt-detect-failure-warning t + "Non-nil enables warnings when detection of prompts fail." + :type 'boolean + :safe 'booleanp + :version "24.4") + +(defcustom godot-gdscript-shell-prompt-input-regexps + '(">>> " "\\.\\.\\. " ; Godot-GDScript + "In \\[[0-9]+\\]: " ; IGodot-GDScript + ;; Using ipdb outside IGodot-GDScript may fail to cleanup and leave static + ;; IGodot-GDScript prompts activated, this adds some safeguard for that. + "In : " "\\.\\.\\.: ") + "List of regular expressions matching input prompts." + :type '(repeat string) + :version "24.4") + +(defcustom godot-gdscript-shell-prompt-output-regexps + '("" ; Godot-GDScript + "Out\\[[0-9]+\\]: " ; IGodot-GDScript + "Out :") ; ipdb safeguard + "List of regular expressions matching output prompts." + :type '(repeat string) + :version "24.4") + +(defcustom godot-gdscript-shell-prompt-regexp ">>> " + "Regular expression matching top level input prompt of Godot-GDScript shell. +It should not contain a caret (^) at the beginning." + :type 'string) + +(defcustom godot-gdscript-shell-prompt-block-regexp "\\.\\.\\. " + "Regular expression matching block input prompt of Godot-GDScript shell. +It should not contain a caret (^) at the beginning." + :type 'string) + +(defcustom godot-gdscript-shell-prompt-output-regexp "" + "Regular expression matching output prompt of Godot-GDScript shell. +It should not contain a caret (^) at the beginning." + :type 'string) + +(defcustom godot-gdscript-shell-prompt-pdb-regexp "[(<]*[Ii]?[Pp]db[>)]+ " + "Regular expression matching pdb input prompt of Godot-GDScript shell. +It should not contain a caret (^) at the beginning." + :type 'string) + +(defcustom godot-gdscript-shell-enable-font-lock t + "Should syntax highlighting be enabled in the Godot-GDScript shell buffer? +Restart the Godot-GDScript shell after changing this variable for +it to take effect." + :type 'boolean + :group 'godot-gdscript + :safe 'booleanp) + +(defcustom godot-gdscript-shell-unbuffered t + "Should shell output be unbuffered?. +When non-nil, this may prevent delayed and missing output in the +Godot-GDScript shell. See commentary for details." + :type 'boolean + :group 'godot-gdscript + :safe 'booleanp) + +(defcustom godot-gdscript-shell-process-environment nil + "List of environment variables for Godot-GDScript shell. +This variable follows the same rules as `process-environment' +since it merges with it before the process creation routines are +called. When this variable is nil, the Godot-GDScript shell is run with +the default `process-environment'." + :type '(repeat string) + :group 'godot-gdscript + :safe 'listp) + +(defcustom godot-gdscript-shell-extra-godot-gdscriptpaths nil + "List of extra Godot-GDScriptpaths for Godot-GDScript shell. +The values of this variable are added to the existing value of +GODOT-GDSCRIPTPATH in the `process-environment' variable." + :type '(repeat string) + :group 'godot-gdscript + :safe 'listp) + +(defcustom godot-gdscript-shell-exec-path nil + "List of path to search for binaries. +This variable follows the same rules as `exec-path' since it +merges with it before the process creation routines are called. +When this variable is nil, the Godot-GDScript shell is run with the +default `exec-path'." + :type '(repeat string) + :group 'godot-gdscript + :safe 'listp) + +(defcustom godot-gdscript-shell-virtualenv-path nil + "Path to virtualenv root. +This variable, when set to a string, makes the values stored in +`godot-gdscript-shell-process-environment' and +`godot-gdscript-shell-exec-path' to be modified properly so +shells are started with the specified virtualenv." + :type '(choice (const nil) string) + :group 'godot-gdscript + :safe 'stringp) + +(defcustom godot-gdscript-shell-setup-codes '(godot-gdscript-shell-completion-setup-code + godot-gdscript-ffap-setup-code) + "List of code run by `godot-gdscript-shell-send-setup-codes'." + :type '(repeat symbol) + :group 'godot-gdscript + :safe 'listp) + +(defcustom godot-gdscript-shell-compilation-regexp-alist + `((,(rx line-start (1+ (any " \t")) "File \"" + (group (1+ (not (any "\"<")))) ; avoid `' &c + "\", line " (group (1+ digit))) + 1 2) + (,(rx " in file " (group (1+ not-newline)) " on line " + (group (1+ digit))) + 1 2) + (,(rx line-start "> " (group (1+ (not (any "(\"<")))) + "(" (group (1+ digit)) ")" (1+ (not (any "("))) "()") + 1 2)) + "`compilation-error-regexp-alist' for inferior Godot-GDScript." + :type '(alist string) + :group 'godot-gdscript) + +(defvar godot-gdscript-shell--prompt-calculated-input-regexp nil + "Calculated input prompt regexp for inferior Godot-GDScript shell. +Do not set this variable directly, instead use +`godot-gdscript-shell-prompt-set-calculated-regexps'.") + +(defvar godot-gdscript-shell--prompt-calculated-output-regexp nil + "Calculated output prompt regexp for inferior Godot-GDScript shell. +Do not set this variable directly, instead use +`godot-gdscript-shell-set-prompt-regexp'.") + +(defun godot-gdscript-shell-prompt-detect () + "Detect prompts for the current `godot-gdscript-shell-interpreter'. +When prompts can be retrieved successfully from the +`godot-gdscript-shell-interpreter' run with +`godot-gdscript-shell-interpreter-interactive-arg', returns a list of +three elements, where the first two are input prompts and the +last one is an output prompt. When no prompts can be detected +and `godot-gdscript-shell-prompt-detect-failure-warning' is non-nil, +shows a warning with instructions to avoid hangs and returns nil. +When `godot-gdscript-shell-prompt-detect-enabled' is nil avoids any +detection and just returns nil." + (when godot-gdscript-shell-prompt-detect-enabled + ;; FIXME This check causes Godot to run again, opening a new window. + (let* ((process-environment (godot-gdscript-shell-calculate-process-environment)) + (exec-path (godot-gdscript-shell-calculate-exec-path)) + (code (concat + "import sys\n" + "ps = [getattr(sys, 'ps%s' % i, '') for i in range(1,4)]\n" + ;; JSON is built manually for compatibility + "ps_json = '\\n[\"%s\", \"%s\", \"%s\"]\\n' % tuple(ps)\n" + "print (ps_json)\n" + "sys.exit(0)\n")) + (output + (with-temp-buffer + ;; TODO: improve error handling by using + ;; `condition-case' and displaying the error message to + ;; the user in the no-prompts warning. + (ignore-errors + (let ((code-file (godot-gdscript-shell--save-temp-file code))) + ;; Use `process-file' as it is remote-host friendly. + (process-file + godot-gdscript-shell-interpreter + code-file + '(t nil) + nil + godot-gdscript-shell-interpreter-interactive-arg) + ;; Try to cleanup + (delete-file code-file))) + (buffer-string))) + (prompts + (catch 'prompts + (dolist (line (split-string output "\n" t)) + (let ((res + ;; Check if current line is a valid JSON array + (and (string= (substring line 0 2) "[\"") + (ignore-errors + ;; Return prompts as a list, not vector + (append (json-read-from-string line) nil))))) + ;; The list must contain 3 strings, where the first + ;; is the input prompt, the second is the block + ;; prompt and the last one is the output prompt. The + ;; input prompt is the only one that can't be empty. + (when (and (= (length res) 3) + (cl-every #'stringp res) + (not (string= (car res) ""))) + (throw 'prompts res)))) + nil))) + (when (and (not prompts) + godot-gdscript-shell-prompt-detect-failure-warning) + (warn + (concat + "Godot-GDScript shell prompts cannot be detected.\n" + "If your emacs session hangs when starting Godot-GDScript shells\n" + "recover with `keyboard-quit' and then try fixing the\n" + "interactive flag for your interpreter by adjusting the\n" + "`godot-gdscript-shell-interpreter-interactive-arg' or add regexps\n" + "matching shell prompts in the directory-local friendly vars:\n" + " + `godot-gdscript-shell-prompt-regexp'\n" + " + `godot-gdscript-shell-prompt-block-regexp'\n" + " + `godot-gdscript-shell-prompt-output-regexp'\n" + "Or alternatively in:\n" + " + `godot-gdscript-shell-prompt-input-regexps'\n" + " + `godot-gdscript-shell-prompt-output-regexps'"))) + prompts))) + +(defun godot-gdscript-shell-prompt-validate-regexps () + "Validate all user provided regexps for prompts. +Signals `user-error' if any of these vars contain invalid +regexps: `godot-gdscript-shell-prompt-regexp', +`godot-gdscript-shell-prompt-block-regexp', +`godot-gdscript-shell-prompt-pdb-regexp', +`godot-gdscript-shell-prompt-output-regexp', +`godot-gdscript-shell-prompt-input-regexps', +`godot-gdscript-shell-prompt-output-regexps'." + (dolist (symbol (list 'godot-gdscript-shell-prompt-input-regexps + 'godot-gdscript-shell-prompt-output-regexps + 'godot-gdscript-shell-prompt-regexp + 'godot-gdscript-shell-prompt-block-regexp + 'godot-gdscript-shell-prompt-pdb-regexp + 'godot-gdscript-shell-prompt-output-regexp)) + (dolist (regexp (let ((regexps (symbol-value symbol))) + (if (listp regexps) + regexps + (list regexps)))) + (when (not (godot-gdscript-util-valid-regexp-p regexp)) + (user-error "Invalid regexp %s in `%s'" + regexp symbol))))) + +(defun godot-gdscript-shell-prompt-set-calculated-regexps () + "Detect and set input and output prompt regexps. +Build and set the values for +`godot-gdscript-shell-input-prompt-regexp' and +`godot-gdscript-shell-output-prompt-regexp' using the values from +`godot-gdscript-shell-prompt-regexp', +`godot-gdscript-shell-prompt-block-regexp', +`godot-gdscript-shell-prompt-pdb-regexp', +`godot-gdscript-shell-prompt-output-regexp', +`godot-gdscript-shell-prompt-input-regexps', +`godot-gdscript-shell-prompt-output-regexps' and detected prompts +from `godot-gdscript-shell-prompt-detect'." + (when (not (and godot-gdscript-shell--prompt-calculated-input-regexp + godot-gdscript-shell--prompt-calculated-output-regexp)) + (let* ((detected-prompts (godot-gdscript-shell-prompt-detect)) + (input-prompts nil) + (output-prompts nil) + (build-regexp + (lambda (prompts) + (concat "^\\(" + (mapconcat #'identity + (sort prompts + (lambda (a b) + (let ((length-a (length a)) + (length-b (length b))) + (if (= length-a length-b) + (string< a b) + (> (length a) (length b)))))) + "\\|") + "\\)")))) + ;; Validate ALL regexps + (godot-gdscript-shell-prompt-validate-regexps) + ;; Collect all user defined input prompts + (dolist (prompt (append godot-gdscript-shell-prompt-input-regexps + (list godot-gdscript-shell-prompt-regexp + godot-gdscript-shell-prompt-block-regexp + godot-gdscript-shell-prompt-pdb-regexp))) + (cl-pushnew prompt input-prompts :test #'string=)) + ;; Collect all user defined output prompts + (dolist (prompt (cons godot-gdscript-shell-prompt-output-regexp + godot-gdscript-shell-prompt-output-regexps)) + (cl-pushnew prompt output-prompts :test #'string=)) + ;; Collect detected prompts if any + (when detected-prompts + (dolist (prompt (butlast detected-prompts)) + (setq prompt (regexp-quote prompt)) + (cl-pushnew prompt input-prompts :test #'string=)) + (cl-pushnew (regexp-quote + (car (last detected-prompts))) + output-prompts :test #'string=)) + ;; Set input and output prompt regexps from collected prompts + (setq godot-gdscript-shell--prompt-calculated-input-regexp + (funcall build-regexp input-prompts) + godot-gdscript-shell--prompt-calculated-output-regexp + (funcall build-regexp output-prompts))))) + +(defun godot-gdscript-shell-get-process-name (dedicated) + "Calculate the appropriate process name for inferior Godot-GDScript process. +If DEDICATED is t and the variable `buffer-file-name' is non-nil +returns a string with the form +`godot-gdscript-shell-buffer-name'[variable `buffer-file-name'] +else returns the value of `godot-gdscript-shell-buffer-name'." + (let ((process-name + (if (and dedicated + buffer-file-name) + (format "%s[%s]" godot-gdscript-shell-buffer-name buffer-file-name) + (format "%s" godot-gdscript-shell-buffer-name)))) + process-name)) + +(defun godot-gdscript-shell-internal-get-process-name () + "Calculate the appropriate process name for internal Godot-GDScript process. +The name is calculated from +`godot-gdscript-shell-global-buffer-name' and a hash of all +relevant global shell settings in order to ensure uniqueness for +different types of configurations." + (format "%s [%s]" + godot-gdscript-shell-internal-buffer-name + (md5 + (concat + godot-gdscript-shell-interpreter + godot-gdscript-shell-interpreter-args + godot-gdscript-shell--prompt-calculated-input-regexp + godot-gdscript-shell--prompt-calculated-output-regexp + (mapconcat #'symbol-value godot-gdscript-shell-setup-codes "") + (mapconcat #'identity godot-gdscript-shell-process-environment "") + (mapconcat #'identity godot-gdscript-shell-extra-godot-gdscriptpaths "") + (mapconcat #'identity godot-gdscript-shell-exec-path "") + (or godot-gdscript-shell-virtualenv-path "") + (mapconcat #'identity godot-gdscript-shell-exec-path ""))))) + +(defun godot-gdscript-shell-parse-command () ;FIXME: why name it "parse"? + "Calculate the string used to execute the inferior Godot-GDScript process." + ;; FIXME: process-environment doesn't seem to be used anywhere within + ;; this let. + (let ((process-environment (godot-gdscript-shell-calculate-process-environment)) + (exec-path (godot-gdscript-shell-calculate-exec-path))) + (format "%s %s" + ;; FIXME: Why executable-find? + (shell-quote-argument + (executable-find godot-gdscript-shell-interpreter)) + godot-gdscript-shell-interpreter-args))) + +(defun godot-gdscript-shell-calculate-process-environment () + "Calculate process environment given `godot-gdscript-shell-virtualenv-path'." + (let ((process-environment (append + godot-gdscript-shell-process-environment + process-environment nil)) + (virtualenv (if godot-gdscript-shell-virtualenv-path + (directory-file-name godot-gdscript-shell-virtualenv-path) + nil))) + (when godot-gdscript-shell-unbuffered + (setenv "GODOT-GDSCRIPTUNBUFFERED" "1")) + (when godot-gdscript-shell-extra-godot-gdscriptpaths + (setenv "GODOT-GDSCRIPTPATH" + (format "%s%s%s" + (mapconcat 'identity + godot-gdscript-shell-extra-godot-gdscriptpaths + path-separator) + path-separator + (or (getenv "GODOT-GDSCRIPTPATH") "")))) + (if (not virtualenv) + process-environment + (setenv "GODOT-GDSCRIPTHOME" nil) + (setenv "PATH" (format "%s/bin%s%s" + virtualenv path-separator + (or (getenv "PATH") ""))) + (setenv "VIRTUAL_ENV" virtualenv)) + process-environment)) + +(defun godot-gdscript-shell-calculate-exec-path () + "Calculate exec path given `godot-gdscript-shell-virtualenv-path'." + (let ((path (append godot-gdscript-shell-exec-path + exec-path nil))) ;FIXME: Why nil? + (if (not godot-gdscript-shell-virtualenv-path) + path + (cons (expand-file-name "bin" godot-gdscript-shell-virtualenv-path) + path)))) + +(defun godot-gdscript-comint-output-filter-function (output) + "Hook run after content is put into comint buffer. +OUTPUT is a string with the contents of the buffer." + (ansi-color-filter-apply output)) + +(defvar godot-gdscript-shell--parent-buffer nil) + +(defvar godot-gdscript-shell-output-syntax-table + (let ((table (make-syntax-table godot-gdscript-dotty-syntax-table))) + (modify-syntax-entry ?\' "." table) + (modify-syntax-entry ?\" "." table) + (modify-syntax-entry ?\( "." table) + (modify-syntax-entry ?\[ "." table) + (modify-syntax-entry ?\{ "." table) + (modify-syntax-entry ?\) "." table) + (modify-syntax-entry ?\] "." table) + (modify-syntax-entry ?\} "." table) + table) + "Syntax table for shell output. +It makes parens and quotes be treated as punctuation chars.") + +(define-derived-mode inferior-godot-gdscript-mode comint-mode "Inferior Godot-GDScript" + "Major mode for Godot-GDScript inferior process. +Runs a Godot-GDScript interpreter as a subprocess of Emacs, with +Godot-GDScript I/O through an Emacs buffer. Variables +`godot-gdscript-shell-interpreter' and +`godot-gdscript-shell-interpreter-args' control which +Godot-GDScript interpreter is run. Variables +`godot-gdscript-shell-prompt-regexp', +`godot-gdscript-shell-prompt-output-regexp', +`godot-gdscript-shell-prompt-block-regexp', +`godot-gdscript-shell-enable-font-lock', +`godot-gdscript-shell-completion-setup-code', +`godot-gdscript-shell-completion-string-code', +`godot-gdscript-ffap-setup-code' and +`godot-gdscript-ffap-string-code' can customize this mode for +different Godot-GDScript interpreters. + +You can also add additional setup code to be run at +initialization of the interpreter via `godot-gdscript-shell-setup-codes' +variable. + +\(Type \\[describe-mode] in the process buffer for a list of commands.)" + (let ((interpreter godot-gdscript-shell-interpreter) + (args godot-gdscript-shell-interpreter-args)) + (when godot-gdscript-shell--parent-buffer + (godot-gdscript-util-clone-local-variables godot-gdscript-shell--parent-buffer)) + ;; Users can override default values for these vars when calling + ;; `godot-gdscript-run-script'. This ensures new values let-bound in + ;; `godot-gdscript-shell-make-comint' are locally set. + (set (make-local-variable 'godot-gdscript-shell-interpreter) interpreter) + (set (make-local-variable 'godot-gdscript-shell-interpreter-args) args)) + (set (make-local-variable 'godot-gdscript-shell--prompt-calculated-input-regexp) nil) + (set (make-local-variable 'godot-gdscript-shell--prompt-calculated-output-regexp) nil) + ;; FIXME Causes window to be duplicated in `godot-gdscript-shell-prompt-detect`. + ;; (godot-gdscript-shell-prompt-set-calculated-regexps) + ;; As Godot's terminal does not accept input, we avoid font-locking for now. + ;; (setq comint-prompt-regexp godot-gdscript-shell--prompt-calculated-input-regexp) + ;; (setq mode-line-process '(":%s")) + ;; (make-local-variable 'comint-output-filter-functions) + ;; (add-hook 'comint-output-filter-functions + ;; 'godot-gdscript-comint-output-filter-function) + ;; (add-hook 'comint-output-filter-functions + ;; 'godot-gdscript-pdbtrack-comint-output-filter-function) + ;; (set (make-local-variable 'compilation-error-regexp-alist) + ;; godot-gdscript-shell-compilation-regexp-alist) + ;; (define-key inferior-godot-gdscript-mode-map [remap complete-symbol] + ;; 'completion-at-point) + ;; (add-hook 'completion-at-point-functions + ;; #'godot-gdscript-shell-completion-complete-at-point nil 'local) + ;; (add-hook 'comint-dynamic-complete-functions ;FIXME: really? + ;; #'godot-gdscript-shell-completion-complete-at-point nil 'local) + ;; (define-key inferior-godot-gdscript-mode-map "\t" + ;; 'godot-gdscript-shell-completion-complete-or-indent) + ;; (make-local-variable 'godot-gdscript-pdbtrack-buffers-to-kill) + ;; (make-local-variable 'godot-gdscript-pdbtrack-tracked-buffer) + ;; (make-local-variable 'godot-gdscript-shell-internal-last-output) + ;; (when godot-gdscript-shell-enable-font-lock + ;; (set-syntax-table godot-gdscript-mode-syntax-table) + ;; (set (make-local-variable 'font-lock-defaults) + ;; '(godot-gdscript-font-lock-keywords nil nil nil nil)) + ;; (set (make-local-variable 'syntax-propertize-function) + ;; (eval + ;; ;; XXX: Unfortunately eval is needed here to make use of the + ;; ;; dynamic value of `comint-prompt-regexp'. + ;; `(syntax-propertize-rules + ;; (,comint-prompt-regexp + ;; (0 (ignore + ;; (put-text-property + ;; comint-last-input-start end 'syntax-table + ;; godot-gdscript-shell-output-syntax-table) + ;; ;; XXX: This might look weird, but it is the easiest + ;; ;; way to ensure font lock gets cleaned up before the + ;; ;; current prompt, which is needed for unclosed + ;; ;; strings to not mess up with current input. + ;; (font-lock-unfontify-region comint-last-input-start end)))) + ;; (,(godot-gdscript-rx string-delimiter) + ;; (0 (ignore + ;; (and (not (eq (get-text-property start 'field) 'output)) + ;; (godot-gdscript-syntax-stringify))))))))) + ;; (compilation-shell-minor-mode 1) +) + +(defun godot-gdscript-shell-make-comint (cmd proc-name &optional pop internal) + "Create a Godot-GDScript shell comint buffer. +CMD is the Godot-GDScript command to be executed and PROC-NAME is the +process name the comint buffer will get. After the comint buffer +is created the `inferior-godot-gdscript-mode' is activated. When +optional argument POP is non-nil the buffer is shown. When +optional argument INTERNAL is non-nil this process is run on a +buffer with a name that starts with a space, following the Emacs +convention for temporary/internal buffers, and also makes sure +the user is not queried for confirmation when the process is +killed." + (save-excursion + (let* ((proc-buffer-name + (format (if (not internal) "*%s*" " *%s*") proc-name)) + (process-environment (godot-gdscript-shell-calculate-process-environment)) + (exec-path (godot-gdscript-shell-calculate-exec-path))) + (when (not (comint-check-proc proc-buffer-name)) + (let* ((cmdlist (split-string-and-unquote cmd)) + (interpreter (car cmdlist)) + (args (cdr cmdlist)) + (buffer (apply #'make-comint-in-buffer proc-name proc-buffer-name + interpreter nil args)) + (godot-gdscript-shell--parent-buffer (current-buffer)) + (process (get-buffer-process buffer)) + ;; As the user may have overridden default values for + ;; these vars on `godot-gdscript-run-script', let-binding them allows + ;; to have the new right values in all setup code + ;; that's is done in `inferior-godot-gdscript-mode', which is + ;; important, especially for prompt detection. + (godot-gdscript-shell-interpreter interpreter) + (godot-gdscript-shell-interpreter-args + (mapconcat #'identity args " "))) + (with-current-buffer buffer + (inferior-godot-gdscript-mode)) + (accept-process-output process) + (and pop (pop-to-buffer buffer t)) + (and internal (set-process-query-on-exit-flag process nil)))) + proc-buffer-name))) + +;;;###autoload +(defun godot-gdscript-run-script (cmd &optional dedicated show) + "Run an inferior Godot-GDScript process. +Input and output via buffer named after +`godot-gdscript-shell-buffer-name'. If there is a process already +running in that buffer, just switch to it. + +With argument, allows you to define CMD so you can edit the +command used to call the interpreter and define DEDICATED, so a +dedicated process for the current buffer is open. When numeric +prefix arg is other than 0 or 4 do not SHOW. + +Runs the hook `inferior-godot-gdscript-mode-hook' after +`comint-mode-hook' is run. (Type \\[describe-mode] in the +process buffer for a list of commands.)" + (interactive + (if current-prefix-arg + (list + (read-string "Run Godot-GDScript: " (godot-gdscript-shell-parse-command)) + (y-or-n-p "Make dedicated process? ") + (= (prefix-numeric-value current-prefix-arg) 4)) + (list (godot-gdscript-shell-parse-command) nil t))) + (godot-gdscript-shell-make-comint + cmd (godot-gdscript-shell-get-process-name dedicated) show) + dedicated) + +(defun godot-gdscript-run-script-internal () + "Run an inferior Internal Godot-GDScript process. +Input and output via buffer named after +`godot-gdscript-shell-internal-buffer-name' and what +`godot-gdscript-shell-internal-get-process-name' returns. + +This new kind of shell is intended to be used for generic +communication related to defined configurations; the main +difference with global or dedicated shells is that these ones are +attached to a configuration, not a buffer. This means that can be +used for example to retrieve the sys.path and other stuff, +without messing with user shells. Note that +`godot-gdscript-shell-enable-font-lock' and +`inferior-godot-gdscript-mode-hook' are set to nil for these +shells, so setup codes are not sent at startup." + (let ((godot-gdscript-shell-enable-font-lock nil) + (inferior-godot-gdscript-mode-hook nil)) + (get-buffer-process + (godot-gdscript-shell-make-comint + (godot-gdscript-shell-parse-command) + (godot-gdscript-shell-internal-get-process-name) nil t)))) + +(defun godot-gdscript-shell-get-buffer () + "Return inferior Godot-GDScript buffer for current buffer." + (let* ((dedicated-proc-name (godot-gdscript-shell-get-process-name t)) + (dedicated-proc-buffer-name (format "*%s*" dedicated-proc-name)) + (global-proc-name (godot-gdscript-shell-get-process-name nil)) + (global-proc-buffer-name (format "*%s*" global-proc-name)) + (dedicated-running (comint-check-proc dedicated-proc-buffer-name)) + (global-running (comint-check-proc global-proc-buffer-name))) + ;; Always prefer dedicated + (or (and dedicated-running dedicated-proc-buffer-name) + (and global-running global-proc-buffer-name)))) + +(defun godot-gdscript-shell-get-process () + "Return inferior godot-gdscript process for current buffer." + (get-buffer-process (godot-gdscript-shell-get-buffer))) + +(defun godot-gdscript-shell-get-or-create-process (&optional cmd dedicated show) + "Get or create an inferior GDScript process for current buffer and return it. +Arguments CMD, DEDICATED and SHOW are those of +`godot-gdscript-run-script' and are used to start the shell. If those +arguments are not provided, `godot-gdscript-run-script' is called +interactively and the user will be asked for their values." + (let* ((dedicated-proc-name (godot-gdscript-shell-get-process-name t)) + (dedicated-proc-buffer-name (format "*%s*" dedicated-proc-name)) + (global-proc-name (godot-gdscript-shell-get-process-name nil)) + (global-proc-buffer-name (format "*%s*" global-proc-name)) + (dedicated-running (comint-check-proc dedicated-proc-buffer-name)) + (global-running (comint-check-proc global-proc-buffer-name)) + (current-prefix-arg 16)) + (when (and (not dedicated-running) (not global-running)) + (if (if (not cmd) + ;; XXX: Refactor code such that calling `godot-gdscript-run-script' + ;; interactively is not needed anymore. + (call-interactively 'godot-gdscript-run-script) + (godot-gdscript-run-script cmd dedicated show)) + (setq dedicated-running t) + (setq global-running t))) + ;; Always prefer dedicated + (get-buffer-process (if dedicated-running + dedicated-proc-buffer-name + global-proc-buffer-name)))) + +(defvar godot-gdscript-shell-internal-buffer nil + "Current internal shell buffer for the current buffer. +This is really not necessary at all for the code to work but it's +there for compatibility with CEDET.") + +(defvar godot-gdscript-shell-internal-last-output nil + "Last output captured by the internal shell. +This is really not necessary at all for the code to work but it's +there for compatibility with CEDET.") + +(defun godot-gdscript-shell-internal-get-or-create-process () + "Get or create an inferior internal Godot-GDScript process." + (let* ((proc-name (godot-gdscript-shell-internal-get-process-name)) + (proc-buffer-name (format " *%s*" proc-name))) + (when (not (process-live-p proc-name)) + (godot-gdscript-run-script-internal) + (setq godot-gdscript-shell-internal-buffer proc-buffer-name) + ;; XXX: Why is this `sit-for' needed? + ;; `godot-gdscript-shell-make-comint' calls `accept-process-output' + ;; already but it is not helping to get proper output on + ;; 'gnu/linux when the internal shell process is not running and + ;; a call to `godot-gdscript-shell-internal-send-string' is issued. + (sit-for 0.1 t)) + (get-buffer-process proc-buffer-name))) + +(define-obsolete-function-alias + 'godot-gdscript-proc 'godot-gdscript-shell-internal-get-or-create-process "24.3") + +(define-obsolete-variable-alias + 'godot-gdscript-buffer 'godot-gdscript-shell-internal-buffer "24.3") + +(define-obsolete-variable-alias + 'godot-gdscript-preoutput-result 'godot-gdscript-shell-internal-last-output "24.3") + +(defun godot-gdscript-shell--save-temp-file (string) + "Save a temporary file with contents defined in STRING." + (let* ((temporary-file-directory + (if (file-remote-p default-directory) + (concat (file-remote-p default-directory) "/tmp") + temporary-file-directory)) + (temp-file-name (make-temp-file "py")) + (coding-system-for-write (godot-gdscript-info-encoding))) + (with-temp-file temp-file-name + (insert string) + (delete-trailing-whitespace)) + temp-file-name)) + +(defun godot-gdscript-shell-send-string (string &optional process) + "Send STRING to inferior Godot-GDScript PROCESS." + (interactive "sGodot-GDScript command: ") + (let ((process (or process (godot-gdscript-shell-get-or-create-process)))) + (if (string-match ".\n+." string) ;Multiline. + (let* ((temp-file-name (godot-gdscript-shell--save-temp-file string)) + (file-name (or (buffer-file-name) temp-file-name))) + (godot-gdscript-shell-send-file file-name process temp-file-name t)) + (comint-send-string process string) + (when (or (not (string-match "\n\\'" string)) + (string-match "\n[ \t].*\n?\\'" string)) + (comint-send-string process "\n"))))) + +(defvar godot-gdscript-shell-output-filter-in-progress nil) +(defvar godot-gdscript-shell-output-filter-buffer nil) + +(defun godot-gdscript-shell-output-filter (string) + "Filter used in `godot-gdscript-shell-send-string-no-output' to grab output. +STRING is the output received to this point from the process. +This filter saves received output from the process in +`godot-gdscript-shell-output-filter-buffer' and stops receiving +it after detecting a prompt at the end of the buffer." + (setq + string (ansi-color-filter-apply string) + godot-gdscript-shell-output-filter-buffer + (concat godot-gdscript-shell-output-filter-buffer string)) + (when (string-match + ;; XXX: It seems on OSX an extra carriage return is attached + ;; at the end of output, this handles that too. + (concat + "\r?\n" + ;; Remove initial caret from calculated regexp + (replace-regexp-in-string + (rx string-start ?^) "" + godot-gdscript-shell--prompt-calculated-input-regexp) + "$") + godot-gdscript-shell-output-filter-buffer) + ;; Output ends when `godot-gdscript-shell-output-filter-buffer' contains + ;; the prompt attached at the end of it. + (setq godot-gdscript-shell-output-filter-in-progress nil + godot-gdscript-shell-output-filter-buffer + (substring godot-gdscript-shell-output-filter-buffer + 0 (match-beginning 0))) + (when (string-match + godot-gdscript-shell--prompt-calculated-output-regexp + godot-gdscript-shell-output-filter-buffer) + ;; Some shells, like IGodot-GDScript might append a prompt before the + ;; output, clean that. + (setq godot-gdscript-shell-output-filter-buffer + (substring godot-gdscript-shell-output-filter-buffer (match-end 0))))) + "") + +(defun godot-gdscript-shell-send-string-no-output (string &optional process) + "Send STRING to PROCESS and inhibit output. +Return the output." + (let ((process (or process (godot-gdscript-shell-get-or-create-process))) + (comint-preoutput-filter-functions + '(godot-gdscript-shell-output-filter)) + (godot-gdscript-shell-output-filter-in-progress t) + (inhibit-quit t)) + (or + (with-local-quit + (godot-gdscript-shell-send-string string process) + (while godot-gdscript-shell-output-filter-in-progress + ;; `godot-gdscript-shell-output-filter' takes care of setting + ;; `godot-gdscript-shell-output-filter-in-progress' to NIL after it + ;; detects end of output. + (accept-process-output process)) + (prog1 + godot-gdscript-shell-output-filter-buffer + (setq godot-gdscript-shell-output-filter-buffer nil))) + (with-current-buffer (process-buffer process) + (comint-interrupt-subjob))))) + +(defun godot-gdscript-shell-internal-send-string (string) + "Send STRING to the Internal Godot-GDScript interpreter. +Returns the output. See `godot-gdscript-shell-send-string-no-output'." + ;; XXX Remove `godot-gdscript-shell-internal-last-output' once CEDET is + ;; updated to support this new mode. + (setq godot-gdscript-shell-internal-last-output + (godot-gdscript-shell-send-string-no-output + ;; Makes this function compatible with the old + ;; godot-gdscript-send-receive. (At least for CEDET). + (replace-regexp-in-string "_emacs_out +" "" string) + (godot-gdscript-shell-internal-get-or-create-process)))) + +(define-obsolete-function-alias + 'godot-gdscript-send-receive 'godot-gdscript-shell-internal-send-string "24.3") + +(define-obsolete-function-alias + 'godot-gdscript-send-string 'godot-gdscript-shell-internal-send-string "24.3") + +(defun godot-gdscript-shell-buffer-substring (start end &optional nomain) + "Send buffer substring from START to END formatted for shell. +This is a wrapper over `buffer-substring' that takes care of +different transformations for the code sent to be evaluated in +the godot-gdscript shell: + 1. When optional argument NOMAIN is non-nil everything under an + \"if __name__ == '__main__'\" block will be removed. + 2. When a subregion of the buffer is sent, it takes care of + appending extra empty lines so tracebacks are correct. + 3. When the region sent is a substring of the current buffer, a + coding cookie is added. + 4. Wraps indented regions under an \"if true:\" block so the + interpreter evaluates them correctly." + (let* ((substring (buffer-substring-no-properties start end)) + (starts-at-point-min-p (save-restriction + (widen) + (= (point-min) start))) + (encoding (godot-gdscript-info-encoding)) + (fillstr (when (not starts-at-point-min-p) + (concat + (format "# -*- coding: %s -*-\n" encoding) + (make-string + ;; Subtract 2 because of the coding cookie. + (- (line-number-at-pos start) 2) ?\n)))) + (toplevel-block-p (save-excursion + (goto-char start) + (or (zerop (line-number-at-pos start)) + (progn + (godot-gdscript-util-forward-comment 1) + (zerop (current-indentation))))))) + (with-temp-buffer + (godot-gdscript-mode) + (if fillstr (insert fillstr)) + (insert substring) + (goto-char (point-min)) + (when (not toplevel-block-p) + (insert "if true:") + (delete-region (point) (line-end-position))) + (when nomain + (let* ((if-name-main-start-end + (and nomain + (save-excursion + (when (godot-gdscript-nav-if-name-main) + (cons (point) + (progn (godot-gdscript-nav-forward-sexp-safe) + ;; Include ending newline + (forward-line 1) + (point))))))) + ;; Oh destructuring bind, how I miss you. + (if-name-main-start (car if-name-main-start-end)) + (if-name-main-end (cdr if-name-main-start-end)) + (fillstr (make-string + (- (line-number-at-pos if-name-main-end) + (line-number-at-pos if-name-main-start)) ?\n))) + (when if-name-main-start-end + (goto-char if-name-main-start) + (delete-region if-name-main-start if-name-main-end) + (insert fillstr)))) + ;; Ensure there's only one coding cookie in the generated string. + (goto-char (point-min)) + (when (looking-at-p (godot-gdscript-rx coding-cookie)) + (forward-line 1) + (when (looking-at-p (godot-gdscript-rx coding-cookie)) + (delete-region + (line-beginning-position) (line-end-position)))) + (buffer-substring-no-properties (point-min) (point-max))))) + +(defun godot-gdscript-shell-send-region (start end &optional send-main) + "Send the region delimited by START and END to inferior GDScript process. +When optional argument SEND-MAIN is non-nil, allow execution of +code inside blocks delimited by \"if __name__== '__main__':\". +When called interactively SEND-MAIN defaults to nil, unless it's +called with prefix argument." + (interactive "r\nP") + (let* ((string (godot-gdscript-shell-buffer-substring start end (not send-main))) + (process (godot-gdscript-shell-get-or-create-process)) + (original-string (buffer-substring-no-properties start end)) + (_ (string-match "\\`\n*\\(.*\\)" original-string))) + (message "Sent: %s..." (match-string 1 original-string)) + (godot-gdscript-shell-send-string string process))) + +(defun godot-gdscript-shell-send-buffer (&optional send-main) + "Send the entire buffer to inferior Godot-GDScript process. +When optional argument SEND-MAIN is non-nil, allow execution of +code inside blocks delimited by \"if __name__== '__main__':\". +When called interactively SEND-MAIN defaults to nil, unless it's +called with prefix argument." + (interactive "P") + (save-restriction + (widen) + (godot-gdscript-shell-send-region (point-min) (point-max) send-main))) + +(defun godot-gdscript-shell-send-defun (arg) + "Send the current defun to inferior Godot-GDScript process. +When argument ARG is non-nil do not include decorators." + (interactive "P") + (save-excursion + (godot-gdscript-shell-send-region + (progn + (end-of-line 1) + (while (and (or (godot-gdscript-nav-beginning-of-defun) + (beginning-of-line 1)) + (> (current-indentation) 0))) + (when (not arg) + (while (and (forward-line -1) + (looking-at (godot-gdscript-rx decorator)))) + (forward-line 1)) + (point-marker)) + (progn + (or (godot-gdscript-nav-end-of-defun) + (end-of-line 1)) + (point-marker))))) + +(defun godot-gdscript-shell-send-file (file-name &optional process temp-file-name + delete) + "Send FILE-NAME to inferior Godot-GDScript PROCESS. +If TEMP-FILE-NAME is passed then that file is used for processing +instead, while internally the shell will continue to use +FILE-NAME. If TEMP-FILE-NAME and DELETE are non-nil, then +TEMP-FILE-NAME is deleted after evaluation is performed." + (interactive "fFile to send: ") + (let* ((process (or process (godot-gdscript-shell-get-or-create-process))) + (encoding (with-temp-buffer + (insert-file-contents + (or temp-file-name file-name)) + (godot-gdscript-info-encoding))) + (file-name (expand-file-name + (or (file-remote-p file-name 'localname) + file-name))) + (temp-file-name (when temp-file-name + (expand-file-name + (or (file-remote-p temp-file-name 'localname) + temp-file-name))))) + (godot-gdscript-shell-send-string + (format + (concat + "import codecs, os;" + "__pyfile = codecs.open('''%s''', encoding='''%s''');" + "__code = __pyfile.read().encode('''%s''');" + "__pyfile.close();" + (when (and delete temp-file-name) + (format "os.remove('''%s''');" temp-file-name)) + "exec(compile(__code, '''%s''', 'exec'));") + (or temp-file-name file-name) encoding encoding file-name) + process))) + +(defun godot-gdscript-shell-switch-to-shell () + "Switch to inferior Godot-GDScript process buffer." + (interactive) + (pop-to-buffer (process-buffer (godot-gdscript-shell-get-or-create-process)) t)) + +(defun godot-gdscript-shell-send-setup-code () + "Send all setup code for shell. +This function takes the list of setup code to send from the +`godot-gdscript-shell-setup-codes' list." + (let ((process (get-buffer-process (current-buffer)))) + (dolist (code godot-gdscript-shell-setup-codes) + (when code + (message "Sent %s" code) + (godot-gdscript-shell-send-string + (symbol-value code) process))))) + +(add-hook 'inferior-godot-gdscript-mode-hook + #'godot-gdscript-shell-send-setup-code) + +;;; Shell completion + +(defcustom godot-gdscript-shell-completion-setup-code + "try: + import __builtin__ +except ImportError: + # Godot-Gdscript 3 + import builtins as __builtin__ +try: + import readline, rlcompleter +except: + def __GODOT-GDSCRIPT_EL_get_completions(text): + return [] +else: + def __GODOT-GDSCRIPT_EL_get_completions(text): + builtins = dir(__builtin__) + completions = [] + try: + splits = text.split() + is_module = splits and splits[0] in ('from', 'import') + is_igodot-gdscript = ('__IGODOT-GDSCRIPT__' in builtins or + '__IGODOT-GDSCRIPT__active' in builtins) + if is_module: + from IGodot-Gdscript.core.completerlib import module_completion + completions = module_completion(text.strip()) + elif is_igodot-gdscript and '__IP' in builtins: + completions = __IP.complete(text) + elif is_igodot-gdscript and 'get_igodot-gdscript' in builtins: + completions = get_igodot-gdscript().Completer.all_completions(text) + else: + i = 0 + while true: + res = readline.get_completer()(text, i) + if not res: + break + i += 1 + completions.append(res) + except: + pass + return completions" + "Code used to setup completion in inferior Godot-Gdscript processes." + :type 'string + :group 'godot-gdscript) + +(defcustom godot-gdscript-shell-completion-string-code + "';'.join(__GODOT-GDSCRIPT_EL_get_completions('''%s'''))\n" + "Gdscript code used to get a string of completions separated by semicolons. +The string passed to the function is the current Godot-GDScript name or +the full statement in the case of imports." + :type 'string + :group 'godot-gdscript) + +(define-obsolete-variable-alias + 'godot-gdscript-shell-completion-module-string-code + 'godot-gdscript-shell-completion-string-code + "24.4" + "Completion string code must also autocomplete modules.") + +(defcustom godot-gdscript-shell-completion-pdb-string-code + "';'.join(globals().keys() + locals().keys())" + "Gdscript code used to get completions separated by semicolons for [i]pdb." + :type 'string + :group 'godot-gdscript) + +(defun godot-gdscript-shell-completion-get-completions (process line input) + "Do completion at point for PROCESS. +LINE is used to detect the context on how to complete given INPUT." + (with-current-buffer (process-buffer process) + (let* ((prompt + ;; Get last prompt of the inferior process buffer (this + ;; intentionally avoids using `comint-last-prompt' because + ;; of incompatibilities with Emacs 24.x). + (save-excursion + (buffer-substring-no-properties + (line-beginning-position) ;End of prompt. + (re-search-backward "^")))) + (completion-code + ;; Check whether a prompt matches a pdb string, an import + ;; statement or just the standard prompt and use the + ;; correct godot-gdscript-shell-completion-*-code string + (cond ((and (> (length godot-gdscript-shell-completion-pdb-string-code) 0) + (string-match + (concat "^" godot-gdscript-shell-prompt-pdb-regexp) prompt)) + godot-gdscript-shell-completion-pdb-string-code) + ((string-match + godot-gdscript-shell--prompt-calculated-input-regexp prompt) + godot-gdscript-shell-completion-string-code) + (t nil))) + (input + (if (string-match + (godot-gdscript-rx line-start (* space) (or "from" "import") space) + line) + line + input))) + (and completion-code + (> (length input) 0) + (let ((completions + (godot-gdscript-util-strip-string + (godot-gdscript-shell-send-string-no-output + (format completion-code input) process)))) + (and (> (length completions) 2) + (split-string completions + "^'\\|^\"\\|;\\|'$\\|\"$" t))))))) + +(defun godot-gdscript-shell-completion-complete-at-point (&optional process) + "Perform completion at point in inferior Godot-Gdscript. +Optional argument PROCESS forces completions to be retrieved +using that one instead of current buffer's process." + (setq process (or process (get-buffer-process (current-buffer)))) + (let* ((start + (save-excursion + (with-syntax-table godot-gdscript-dotty-syntax-table + (let* ((paren-depth (car (syntax-ppss))) + (syntax-string "w_") + (syntax-list (string-to-syntax syntax-string))) + ;; Stop scanning for the beginning of the completion + ;; subject after the char before point matches a + ;; delimiter + (while (member + (car (syntax-after (1- (point)))) syntax-list) + (skip-syntax-backward syntax-string) + (when (or (equal (char-before) ?\)) + (equal (char-before) ?\")) + (forward-char -1)) + (while (or + ;; honor initial paren depth + (> (car (syntax-ppss)) paren-depth) + (godot-gdscript-syntax-context 'string)) + (forward-char -1))) + (point))))) + (end (point))) + (list start end + (completion-table-dynamic + (apply-partially + #'godot-gdscript-shell-completion-get-completions + process (buffer-substring-no-properties + (line-beginning-position) end)))))) + +(defun godot-gdscript-shell-completion-complete-or-indent () + "Complete or indent depending on the context. +If content before pointer is all whitespace, indent. +If not try to complete." + (interactive) + (if (string-match "^[[:space:]]*$" + (buffer-substring (comint-line-beginning-position) + (point-marker))) + (indent-for-tab-command) + (completion-at-point))) + +;;; PDB Track integration + +(defcustom godot-gdscript-pdbtrack-activate t + "Non-nil makes Godot-Gdscript shell enable pdbtracking." + :type 'boolean + :group 'godot-gdscript + :safe 'booleanp) + +(defcustom godot-gdscript-pdbtrack-stacktrace-info-regexp + "> \\([^\"(<]+\\)(\\([0-9]+\\))\\([?a-zA-Z0-9_<>]+\\)()" + "Regular expression matching stacktrace information. +Used to extract the current line and module being inspected." + :type 'string + :group 'godot-gdscript + :safe 'stringp) + +(defvar godot-gdscript-pdbtrack-tracked-buffer nil + "Variable containing the value of the current tracked buffer. +Never set this variable directly, use +`godot-gdscript-pdbtrack-set-tracked-buffer' instead.") + +(defvar godot-gdscript-pdbtrack-buffers-to-kill nil + "List of buffers to be deleted after tracking finishes.") + +(defun godot-gdscript-pdbtrack-set-tracked-buffer (file-name) + "Set the buffer for FILE-NAME as the tracked buffer. +Internally it uses the `godot-gdscript-pdbtrack-tracked-buffer' variable. +Returns the tracked buffer." + (let ((file-buffer (get-file-buffer + (concat (file-remote-p default-directory) + file-name)))) + (if file-buffer + (setq godot-gdscript-pdbtrack-tracked-buffer file-buffer) + (setq file-buffer (find-file-noselect file-name)) + (when (not (member file-buffer godot-gdscript-pdbtrack-buffers-to-kill)) + (add-to-list 'godot-gdscript-pdbtrack-buffers-to-kill file-buffer))) + file-buffer)) + +(defun godot-gdscript-pdbtrack-comint-output-filter-function (output) + "Move overlay arrow to current pdb line in tracked buffer. +Argument OUTPUT is a string with the output from the comint process." + (when (and godot-gdscript-pdbtrack-activate (not (string= output ""))) + (let* ((full-output (ansi-color-filter-apply + (buffer-substring comint-last-input-end (point-max)))) + (line-number) + (file-name + (with-temp-buffer + (insert full-output) + ;; When the debugger encounters a pdb.set_trace() + ;; command, it prints a single stack frame. Sometimes + ;; it prints a bit of extra information about the + ;; arguments of the present function. When ipdb + ;; encounters an exception, it prints the _entire_ stack + ;; trace. To handle all of these cases, we want to find + ;; the _last_ stack frame printed in the most recent + ;; batch of output, then jump to the corresponding + ;; file/line number. + (goto-char (point-max)) + (when (re-search-backward godot-gdscript-pdbtrack-stacktrace-info-regexp nil t) + (setq line-number (string-to-number + (match-string-no-properties 2))) + (match-string-no-properties 1))))) + (if (and file-name line-number) + (let* ((tracked-buffer + (godot-gdscript-pdbtrack-set-tracked-buffer file-name)) + (shell-buffer (current-buffer)) + (tracked-buffer-window (get-buffer-window tracked-buffer)) + (tracked-buffer-line-pos)) + (with-current-buffer tracked-buffer + (set (make-local-variable 'overlay-arrow-string) "=>") + (set (make-local-variable 'overlay-arrow-position) (make-marker)) + (setq tracked-buffer-line-pos (progn + (goto-char (point-min)) + (forward-line (1- line-number)) + (point-marker))) + (when tracked-buffer-window + (set-window-point + tracked-buffer-window tracked-buffer-line-pos)) + (set-marker overlay-arrow-position tracked-buffer-line-pos)) + (pop-to-buffer tracked-buffer) + (switch-to-buffer-other-window shell-buffer)) + (when godot-gdscript-pdbtrack-tracked-buffer + (with-current-buffer godot-gdscript-pdbtrack-tracked-buffer + (set-marker overlay-arrow-position nil)) + (mapc #'(lambda (buffer) + (ignore-errors (kill-buffer buffer))) + godot-gdscript-pdbtrack-buffers-to-kill) + (setq godot-gdscript-pdbtrack-tracked-buffer nil + godot-gdscript-pdbtrack-buffers-to-kill nil))))) + output) + +;;; Symbol completion + +(defun godot-gdscript-completion-complete-at-point () + "Complete current symbol at point. +For this to work as best as possible you should call +`godot-gdscript-shell-send-buffer' from time to time so context in +inferior Godot-Gdscript process is updated properly." + (when (require 'company nil 'noerror) + (company-complete))) + +;;; Fill paragraph + +(defcustom godot-gdscript-fill-comment-function 'godot-gdscript-fill-comment + "Function to fill comments. +This is the function used by `godot-gdscript-fill-paragraph' to +fill comments." + :type 'symbol + :group 'godot-gdscript) + +(defcustom godot-gdscript-fill-string-function 'godot-gdscript-fill-string + "Function to fill strings. +This is the function used by `godot-gdscript-fill-paragraph' to +fill strings." + :type 'symbol + :group 'godot-gdscript) + +(defcustom godot-gdscript-fill-decorator-function 'godot-gdscript-fill-decorator + "Function to fill decorators. +This is the function used by `godot-gdscript-fill-paragraph' to +fill decorators." + :type 'symbol + :group 'godot-gdscript) + +(defcustom godot-gdscript-fill-paren-function 'godot-gdscript-fill-paren + "Function to fill parens. +This is the function used by `godot-gdscript-fill-paragraph' to +fill parens." + :type 'symbol + :group 'godot-gdscript) + +(defcustom godot-gdscript-fill-docstring-style 'pep-257 + "Style used to fill docstrings. +This affects `godot-gdscript-fill-string' behavior with regards to +triple quotes positioning. + +Possible values are `django', `onetwo', `pep-257', `pep-257-nn', +`symmetric', and nil. A value of nil won't care about quotes +position and will treat docstrings a normal string, any other +value may result in one of the following docstring styles: + +`django': + + \"\"\" + Process foo, return bar. + \"\"\" + + \"\"\" + Process foo, return bar. + + If processing fails throw ProcessingError. + \"\"\" + +`onetwo': + + \"\"\"Process foo, return bar.\"\"\" + + \"\"\" + Process foo, return bar. + + If processing fails throw ProcessingError. + + \"\"\" + +`pep-257': + + \"\"\"Process foo, return bar.\"\"\" + + \"\"\"Process foo, return bar. + + If processing fails throw ProcessingError. + + \"\"\" + +`pep-257-nn': + + \"\"\"Process foo, return bar.\"\"\" + + \"\"\"Process foo, return bar. + + If processing fails throw ProcessingError. + \"\"\" + +`symmetric': + + \"\"\"Process foo, return bar.\"\"\" + + \"\"\" + Process foo, return bar. + + If processing fails throw ProcessingError. + \"\"\"" + :type '(choice + (const :tag "Don't format docstrings" nil) + (const :tag "Django's coding standards style." django) + (const :tag "One newline and start and Two at end style." onetwo) + (const :tag "PEP-257 with 2 newlines at end of string." pep-257) + (const :tag "PEP-257 with 1 newline at end of string." pep-257-nn) + (const :tag "Symmetric style." symmetric)) + :group 'godot-gdscript + :safe (lambda (val) + (memq val '(django onetwo pep-257 pep-257-nn symmetric nil)))) + +(defun godot-gdscript-fill-paragraph (&optional justify) + "`fill-paragraph-function' handling multi-line strings and possibly comments. +If any of the current line is in or at the end of a multi-line string, +fill the string or the paragraph of it that point is in, preserving +the string's indentation. +Optional argument JUSTIFY defines if the paragraph should be justified." + (interactive "P") + (save-excursion + (cond + ;; Comments + ((godot-gdscript-syntax-context 'comment) + (funcall godot-gdscript-fill-comment-function justify)) + ;; Strings/Docstrings + ((save-excursion (or (godot-gdscript-syntax-context 'string) + (equal (string-to-syntax "|") + (syntax-after (point))))) + (funcall godot-gdscript-fill-string-function justify)) + ;; Decorators + ((equal (char-after (save-excursion + (godot-gdscript-nav-beginning-of-statement))) ?@) + (funcall godot-gdscript-fill-decorator-function justify)) + ;; Parens + ((or (godot-gdscript-syntax-context 'paren) + (looking-at (godot-gdscript-rx open-paren)) + (save-excursion + (skip-syntax-forward "^(" (line-end-position)) + (looking-at (godot-gdscript-rx open-paren)))) + (funcall godot-gdscript-fill-paren-function justify)) + (t t)))) + +(defun godot-gdscript-fill-comment (&optional justify) + "Comment fill function for `godot-gdscript-fill-paragraph'. +JUSTIFY should be used (if applicable) as in `fill-paragraph'." + (fill-comment-paragraph justify)) + +(defun godot-gdscript-fill-string (&optional justify) + "String fill function for `godot-gdscript-fill-paragraph'. +JUSTIFY should be used (if applicable) as in `fill-paragraph'." + (let* ((str-start-pos + (set-marker + (make-marker) + (or (godot-gdscript-syntax-context 'string) + (and (equal (string-to-syntax "|") + (syntax-after (point))) + (point))))) + (num-quotes (godot-gdscript-syntax-count-quotes + (char-after str-start-pos) str-start-pos)) + (str-end-pos + (save-excursion + (goto-char (+ str-start-pos num-quotes)) + (or (re-search-forward (rx (syntax string-delimiter)) nil t) + (goto-char (point-max))) + (point-marker))) + (multi-line-p + ;; Docstring styles may vary for oneliners and multi-liners. + (> (count-matches "\n" str-start-pos str-end-pos) 0)) + (delimiters-style + (pcase godot-gdscript-fill-docstring-style + ;; delimiters-style is a cons cell with the form + ;; (START-NEWLINES . END-NEWLINES). When any of the sexps + ;; is NIL means to not add any newlines for start or end + ;; of docstring. See `godot-gdscript-fill-docstring-style' for a + ;; graphic idea of each style. + (`django (cons 1 1)) + (`onetwo (and multi-line-p (cons 1 2))) + (`pep-257 (and multi-line-p (cons nil 2))) + (`pep-257-nn (and multi-line-p (cons nil 1))) + (`symmetric (and multi-line-p (cons 1 1))))) + (docstring-p (save-excursion + ;; Consider docstrings those strings which + ;; start on a line by themselves. + (godot-gdscript-nav-beginning-of-statement) + (and (= (point) str-start-pos)))) + (fill-paragraph-function)) + (save-restriction + (narrow-to-region str-start-pos str-end-pos) + (fill-paragraph justify)) + (save-excursion + (when (and docstring-p godot-gdscript-fill-docstring-style) + ;; Add the number of newlines indicated by the selected style + ;; at the start of the docstring. + (goto-char (+ str-start-pos num-quotes)) + (delete-region (point) (progn + (skip-syntax-forward "> ") + (point))) + (and (car delimiters-style) + (or (newline (car delimiters-style)) t) + ;; Indent only if a newline is added. + (indent-according-to-mode)) + ;; Add the number of newlines indicated by the selected style + ;; at the end of the docstring. + (goto-char (if (not (= str-end-pos (point-max))) + (- str-end-pos num-quotes) + str-end-pos)) + (delete-region (point) (progn + (skip-syntax-backward "> ") + (point))) + (and (cdr delimiters-style) + ;; Add newlines only if string ends. + (not (= str-end-pos (point-max))) + (or (newline (cdr delimiters-style)) t) + ;; Again indent only if a newline is added. + (indent-according-to-mode))))) t) + +(defun godot-gdscript-fill-decorator (&optional _justify) + "Decorator fill function for `godot-gdscript-fill-paragraph'. +JUSTIFY should be used (if applicable) as in `fill-paragraph'." + t) + +(defun godot-gdscript-fill-paren (&optional justify) + "Paren fill function for `godot-gdscript-fill-paragraph'. +JUSTIFY should be used (if applicable) as in `fill-paragraph'." + (save-restriction + (narrow-to-region (progn + (while (godot-gdscript-syntax-context 'paren) + (goto-char (1- (point-marker)))) + (point-marker) + (line-beginning-position)) + (progn + (when (not (godot-gdscript-syntax-context 'paren)) + (end-of-line) + (when (not (godot-gdscript-syntax-context 'paren)) + (skip-syntax-backward "^)"))) + (while (and (godot-gdscript-syntax-context 'paren) + (not (eobp))) + (goto-char (1+ (point-marker)))) + (point-marker))) + (let ((paragraph-start "\f\\|[ \t]*$") + (paragraph-separate ",") + (fill-paragraph-function)) + (goto-char (point-min)) + (fill-paragraph justify)) + (while (not (eobp)) + (forward-line 1) + (godot-gdscript-indent-line) + (goto-char (line-end-position)))) + t) + +;;; Skeletons + +(defcustom godot-gdscript-skeleton-autoinsert nil + "Non-nil means template skeletons will be automagically inserted. +This happens when pressing \"if\", for example, to prompt for +the if condition." + :type 'boolean + :group 'godot-gdscript + :safe 'booleanp) + +(define-obsolete-variable-alias + 'godot-gdscript-use-skeletons 'godot-gdscript-skeleton-autoinsert "24.3") + +(defvar godot-gdscript-skeleton-available '() + "Internal list of available skeletons.") + +(define-abbrev-table 'godot-gdscript-mode-skeleton-abbrev-table () + "Abbrev table for Godot-Gdscript mode skeletons." + :case-fixed t + ;; Allow / inside abbrevs. + :regexp "\\(?:^\\|[^/]\\)\\<\\([[:word:]/]+\\)\\W*" + ;; Only expand in code. + :enable-function (lambda () + (and + (not (godot-gdscript-syntax-comment-or-string-p)) + godot-gdscript-skeleton-autoinsert))) + +(defmacro godot-gdscript-skeleton-define (name doc &rest skel) + "Define a skeleton using NAME DOC and SKEL. +The skeleton will be bound to godot-gdscript-skeleton-NAME and will +be added to `godot-gdscript-mode-skeleton-abbrev-table'." + (declare (indent 2)) + (let* ((name (symbol-name name)) + (function-name (intern (concat "godot-gdscript-skeleton-" name)))) + `(progn + (define-abbrev godot-gdscript-mode-skeleton-abbrev-table + ,name "" ',function-name :system t) + (setq godot-gdscript-skeleton-available + (cons ',function-name godot-gdscript-skeleton-available)) + (define-skeleton ,function-name + ,(or doc + (format "Insert %s statement." name)) + ,@skel)))) + +(define-abbrev-table 'godot-gdscript-mode-abbrev-table () + "Abbrev table for Godot-Gdscript mode." + :parents (list godot-gdscript-mode-skeleton-abbrev-table)) + +(defmacro godot-gdscript-define-auxiliary-skeleton (name &optional doc &rest skel) + "Define a auxiliary skeleton using NAME DOC and SKEL. +The skeleton will be bound to godot-gdscript-skeleton-NAME." + (declare (indent 2)) + (let* ((name (symbol-name name)) + (function-name (intern (concat "godot-gdscript-skeleton--" name))) + (msg (funcall (if (fboundp 'format-message) #'format-message #'format) + "Add `%s' clause? " name))) + (when (not skel) + (setq skel + `(< ,(format "%s:" name) \n \n + > _ \n))) + `(define-skeleton ,function-name + ,(or doc + (format "Auxiliary skeleton for %s statement." name)) + nil + (unless (y-or-n-p ,msg) + (signal 'quit t)) + ,@skel))) + +(godot-gdscript-define-auxiliary-skeleton else) + +(godot-gdscript-define-auxiliary-skeleton except) + +(godot-gdscript-define-auxiliary-skeleton finally) + +(godot-gdscript-skeleton-define if nil + "Condition: " + "if " str ":" \n + _ \n + ("other condition, %s: " + < + "elif " str ":" \n + > _ \n nil) + '(godot-gdscript-skeleton--else) | ^) + +(godot-gdscript-skeleton-define while nil + "Condition: " + "while " str ":" \n + > _ \n + '(godot-gdscript-skeleton--else) | ^) + +(godot-gdscript-skeleton-define for nil + "Iteration spec: " + "for " str ":" \n + > _ \n + '(godot-gdscript-skeleton--else) | ^) + +(godot-gdscript-skeleton-define try nil + nil + "try:" \n + > _ \n + ("Exception, %s: " + < + "except " str ":" \n + > _ \n nil) + resume: + '(godot-gdscript-skeleton--except) + '(godot-gdscript-skeleton--else) + '(godot-gdscript-skeleton--finally) | ^) + +(godot-gdscript-skeleton-define def nil + "Function name: " + "def " str "(" ("Parameter, %s: " + (unless (equal ?\( (char-before)) ", ") + str) "):" \n + "\"\"\"" - "\"\"\"" \n + > _ \n) + +(godot-gdscript-skeleton-define class nil + "Class name: " + "class " str "(" ("Inheritance, %s: " + (unless (equal ?\( (char-before)) ", ") + str) + & ")" | -2 + ":" \n + "\"\"\"" - "\"\"\"" \n + > _ \n) + +(defun godot-gdscript-skeleton-add-menu-items () + "Add menu items to Godot-Gdscript->Skeletons menu." + (let ((skeletons (sort godot-gdscript-skeleton-available 'string<))) + (dolist (skeleton skeletons) + (easy-menu-add-item + nil '("Godot-Gdscript" "Skeletons") + `[,(format + "Insert %s" (nth 2 (split-string (symbol-name skeleton) "-"))) + ,skeleton t])))) + +;;; FFAP + +(defcustom godot-gdscript-ffap-setup-code + "def __FFAP_get_module_path(module): + try: + import os + path = __import__(module).__file__ + if path[-4:] == '.pyc' and os.path.exists(path[0:-1]): + path = path[:-1] + return path + except: + return ''" + "Godot-Gdscript code to get a module path." + :type 'string + :group 'godot-gdscript) + +(defcustom godot-gdscript-ffap-string-code + "__FFAP_get_module_path('''%s''')\n" + "Godot-Gdscript code used to get a string with the path of a module." + :type 'string + :group 'godot-gdscript) + +(defun godot-gdscript-ffap-module-path (module) + "Function for `ffap-alist' to return path for MODULE." + (let ((process (or + (and (derived-mode-p 'inferior-godot-gdscript-mode) + (get-buffer-process (current-buffer))) + (godot-gdscript-shell-get-process)))) + (if (not process) + nil + (let ((module-file + (godot-gdscript-shell-send-string-no-output + (format godot-gdscript-ffap-string-code module) process))) + (when module-file + (substring-no-properties module-file 1 -1)))))) + +(eval-after-load "ffap" + '(progn + (push '(godot-gdscript-mode . godot-gdscript-ffap-module-path) ffap-alist) + (push '(inferior-godot-gdscript-mode . godot-gdscript-ffap-module-path) ffap-alist))) + +(defun godot-gdscript-build-shell-command (&optional path) + "Build base shell command to run Godot Engine with the +project's base PATH. If PATH is not provided, try to find it +using the current file's directory as starting point." + (let* ((project-path (or path (godot-gdscript-find-project-configuration)))) + (concat godot-gdscript-shell-interpreter " --path " project-path))) + +(defun godot-gdscript-run-godot-editor () + "Run Godot Engine Editor." + (interactive) + (godot-gdscript-run-script + (concat (godot-gdscript-build-shell-command) " -e"))) + +(defun godot-gdscript-run-project-in-godot () + "Run the current project in Godot Engine." + (interactive) + (let* ((project-path (godot-gdscript-find-project-configuration))) + (godot-gdscript-run-script + (godot-gdscript-build-shell-command)))) + +(defun godot-gdscript-run-project-in-godot-debug-mode () + "Run the current project in Godot Engine." + (interactive) + (let* ((project-path (godot-gdscript-find-project-configuration))) + (godot-gdscript-run-script + (concat (godot-gdscript-build-shell-command) " -d")))) + +(defun godot-gdscript-run-current-scene-in-godot () + "Run the current script file in Godot Engine." + (interactive) + (godot-gdscript-run-script + (concat (godot-gdscript-build-shell-command) " " (file-name-sans-extension (file-relative-name buffer-file-name)) ".tscn"))) + +(defun godot-gdscript-run-current-scene-in-godot-debug-mode () + "Run the current script file in Godot Engine." + (interactive) + (godot-gdscript-run-script + (concat (godot-gdscript-build-shell-command) " -d " (file-name-sans-extension (file-relative-name buffer-file-name)) ".tscn"))) + +(defun godot-gdscript-edit-current-scene-in-godot () + "Run the current script file in Godot Engine." + (interactive) + (godot-gdscript-run-script + (concat (godot-gdscript-build-shell-command) " -e " (file-name-sans-extension (file-relative-name buffer-file-name)) ".tscn"))) + +(defun godot-gdscript-run-current-script-in-godot () + "Run the current script file in Godot Engine. + +For this to work, the script must inherit either from +\"SceneTree\" or \"MainLoop\"." + (interactive) + (godot-gdscript-run-script + (concat (godot-gdscript-build-shell-command) " -s " (file-relative-name buffer-file-name)))) + +(defun godot-gdscript-find-project-configuration (&optional path) + "Return the path where Godot's configuration File (\"project.godot\") is stored. + +If PATH is given, starts searching by it. Otherwise, the search +starts by the current buffer path." + ;; TODO: Handle error when project file does not exist. + ;; TODO: This is now duplicated in Company-Godot-GDScript. + (let ((base-path (or path default-directory))) + (locate-dominating-file base-path + (lambda (parent) + (directory-files parent t "project.godot"))))) + +(defun godot-gdscript-get-project-name () + "Retrieves the project name from Godot's configuration file." + (with-temp-buffer + (insert-file-contents (concat (godot-gdscript-find-project-configuration) "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")))) + +;;; Code check + +(defcustom godot-gdscript-check-command + "pyflakes" + "Command used to check a Godot-Gdscript file." + :type 'string + :group 'godot-gdscript) + +(defcustom godot-gdscript-check-buffer-name + "*Godot-Gdscript check: %s*" + "Buffer name used for check commands." + :type 'string + :group 'godot-gdscript) + +(defvar godot-gdscript-check-custom-command nil + "Internal use.") + +(defun godot-gdscript-check (command) + "Check a Godot-Gdscript file (default current buffer's file). +Runs COMMAND, a shell command, as if by `compile'. +See `godot-gdscript-check-command' for the default." + (interactive + (list (read-string "Check command: " + (or godot-gdscript-check-custom-command + (concat godot-gdscript-check-command " " + (shell-quote-argument + (or + (let ((name (buffer-file-name))) + (and name + (file-name-nondirectory name))) + ""))))))) + (setq godot-gdscript-check-custom-command command) + (save-some-buffers (not compilation-ask-about-save) nil) + (let ((process-environment (godot-gdscript-shell-calculate-process-environment)) + (exec-path (godot-gdscript-shell-calculate-exec-path))) + (compilation-start command nil + (lambda (_modename) + (format godot-gdscript-check-buffer-name command))))) + +;;; Imenu + +(defvar godot-gdscript-imenu-format-item-label-function + 'godot-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 godot-gdscript-imenu-format-parent-item-label-function + 'godot-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 godot-gdscript-imenu-format-parent-item-jump-label-function + 'godot-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 godot-gdscript-imenu-format-item-label (type name) + "Return Imenu label for single node using TYPE and NAME." + (format "%s (%s)" name type)) + +(defun godot-gdscript-imenu-format-parent-item-label (type name) + "Return Imenu label for parent node using TYPE and NAME." + (format "%s..." (godot-gdscript-imenu-format-item-label type name))) + +(defun godot-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 godot-gdscript-imenu--put-parent (type name pos tree) + "Add the parent with TYPE, NAME and POS to TREE." + (let ((label + (funcall godot-gdscript-imenu-format-item-label-function type name)) + (jump-label + (funcall godot-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 godot-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 godot-gdscript-indent-offset)) + (let* ((pos (godot-gdscript-nav-backward-defun)) + (type) + (name (when (and pos (looking-at godot-gdscript-nav-beginning-of-defun-regexp)) + (let ((split (split-string (match-string-no-properties 0)))) + (setq type (car split)) + (cadr split)))) + (label (when name + (funcall godot-gdscript-imenu-format-item-label-function type name))) + (indent (current-indentation)) + (children-indent-limit (+ godot-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. + (godot-gdscript-imenu--put-parent type name pos tree)) + (t + (godot-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 + (godot-gdscript-imenu--build-tree + prev-indent indent (list (cons label pos))) + tree))))))) + +(defun godot-gdscript-imenu-create-index () + "Return tree Imenu alist for the current Godot-Gdscript buffer. +Change `godot-gdscript-imenu-format-item-label-function', +`godot-gdscript-imenu-format-parent-item-label-function', +`godot-gdscript-imenu-format-parent-item-jump-label-function' to +customize how labels are formatted." + (goto-char (point-max)) + (let ((index) + (tree)) + (while (setq tree (godot-gdscript-imenu--build-tree)) + (setq index (cons tree index))) + index)) + +(defun godot-gdscript-imenu-create-flat-index (&optional alist prefix) + "Return flat outline of the current Godot-Gdscript buffer for Imenu. +Optional argument ALIST is the tree to be flattened; when nil +`godot-gdscript-imenu-build-index' is used with +`godot-gdscript-imenu-format-parent-item-jump-label-function' +`godot-gdscript-imenu-format-parent-item-label-function' +`godot-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)) + (godot-gdscript-imenu-create-flat-index (cddr item) name)))))) + (or alist + (let* ((fn (lambda (_type name) name)) + (godot-gdscript-imenu-format-item-label-function fn) + (godot-gdscript-imenu-format-parent-item-label-function fn) + (godot-gdscript-imenu-format-parent-item-jump-label-function fn)) + (godot-gdscript-imenu-create-index)))))) + +;;; Misc helpers + +(defun godot-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 (godot-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) + (godot-gdscript-nav-beginning-of-statement) + (beginning-of-line 1) + (looking-at-p + godot-gdscript-nav-beginning-of-defun-regexp))) + (< starting-pos + (save-excursion + (let ((min-indent + (+ (current-indentation) + godot-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. + (godot-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 godot-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 (godot-gdscript-syntax-comment-or-string-p)) + (with-syntax-table godot-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 (godot-gdscript-info-current-defun))) + (if (not current-defun) + name + (replace-regexp-in-string + (godot-gdscript-rx line-start word-start "self" word-end ?.) + (concat + (mapconcat 'identity + (butlast (split-string current-defun "\\.")) + ".") ".") + name))))))) + +(defun godot-gdscript-info-statement-starts-block-p () + "Return non-nil if current statement opens a block." + (save-excursion + (godot-gdscript-nav-beginning-of-statement) + (looking-at (godot-gdscript-rx block-start)))) + +(defun godot-gdscript-info-statement-ends-block-p () + "Return non-nil if point is at end of block." + (let ((end-of-block-pos (save-excursion + (godot-gdscript-nav-end-of-block))) + (end-of-statement-pos (save-excursion + (godot-gdscript-nav-end-of-statement)))) + (and end-of-block-pos end-of-statement-pos + (= end-of-block-pos end-of-statement-pos)))) + +(defun godot-gdscript-info-beginning-of-statement-p () + "Return non-nil if point is at beginning of statement." + (= (point) (save-excursion + (godot-gdscript-nav-beginning-of-statement) + (point)))) + +(defun godot-gdscript-info-end-of-statement-p () + "Return non-nil if point is at end of statement." + (= (point) (save-excursion + (godot-gdscript-nav-end-of-statement) + (point)))) + +(defun godot-gdscript-info-beginning-of-block-p () + "Return non-nil if point is at beginning of block." + (and (godot-gdscript-info-beginning-of-statement-p) + (godot-gdscript-info-statement-starts-block-p))) + +(defun godot-gdscript-info-end-of-block-p () + "Return non-nil if point is at end of block." + (and (godot-gdscript-info-end-of-statement-p) + (godot-gdscript-info-statement-ends-block-p))) + +(define-obsolete-function-alias + 'godot-gdscript-info-closing-block + 'godot-gdscript-info-dedenter-opening-block-position "24.4") + +(defun godot-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 godot-gdscript file." + (let ((positions (godot-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 godot-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 godot-gdscript file." + (save-excursion + (let ((dedenter-pos (godot-gdscript-info-dedenter-statement-p))) + (when dedenter-pos + (goto-char dedenter-pos) + (let* ((pairs '(("elif" "elif" "if") + ("else" "if" "elif" "except" "for" "while") + ("except" "except" "try") + ("finally" "else" "except" "try"))) + (dedenter (match-string-no-properties 0)) + (possible-opening-blocks (cdr (assoc-string dedenter pairs))) + (collected-indentations) + (opening-blocks)) + (catch 'exit + (while (godot-gdscript-nav--syntactically + (lambda () + (re-search-backward (godot-gdscript-rx block-start) nil t)) + #'<) + (let ((indentation (current-indentation))) + (when (and (not (memq indentation collected-indentations)) + (or (not collected-indentations) + (< indentation (apply #'min collected-indentations)))) + (setq collected-indentations + (cons indentation collected-indentations)) + (when (member (match-string-no-properties 0) + possible-opening-blocks) + (setq opening-blocks (cons (point) opening-blocks)))) + (when (zerop indentation) + (throw 'exit nil))))) + ;; sort by closer + (nreverse opening-blocks)))))) + +(define-obsolete-function-alias + 'godot-gdscript-info-closing-block-message + 'godot-gdscript-info-dedenter-opening-block-message "24.4") + +(defun godot-gdscript-info-dedenter-opening-block-message () + "Message the first line of the block the current statement closes." + (let ((point (godot-gdscript-info-dedenter-opening-block-position))) + (when point + (save-restriction + (widen) + (message "Closes %s" (save-excursion + (goto-char point) + (buffer-substring + (point) (line-end-position)))))))) + +(defun godot-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 + (godot-gdscript-nav-beginning-of-statement) + (when (and (not (godot-gdscript-syntax-context-type)) + (looking-at (godot-gdscript-rx dedenter))) + (point)))) + +(defun godot-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 + (save-restriction + (widen) + (when line-number + (godot-gdscript-util-goto-line line-number)) + (while (and (not (eobp)) + (goto-char (line-end-position)) + (godot-gdscript-syntax-context 'paren) + (not (equal (char-before (point)) ?\\))) + (forward-line 1)) + (when (equal (char-before) ?\\) + (point-marker))))) + +(defun godot-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 + (save-restriction + (widen) + (when line-number + (godot-gdscript-util-goto-line line-number)) + (when (godot-gdscript-info-line-ends-backslash-p) + (while (save-excursion + (goto-char (line-beginning-position)) + (godot-gdscript-syntax-context 'paren)) + (forward-line -1)) + (back-to-indentation) + (point-marker))))) + +(defun godot-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 + (save-restriction + (widen) + (let* ((context-type (progn + (back-to-indentation) + (godot-gdscript-syntax-context-type))) + (line-start (line-number-at-pos)) + (context-start (when context-type + (godot-gdscript-syntax-context context-type)))) + (cond ((equal context-type 'paren) + ;; Lines inside a paren are always a continuation line + ;; (except the first one). + (godot-gdscript-util-forward-comment -1) + (point-marker)) + ((member context-type '(string comment)) + ;; move forward an roll again + (goto-char context-start) + (godot-gdscript-util-forward-comment) + (godot-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) + (godot-gdscript-util-forward-comment -1) + (when (and (equal (1- line-start) (line-number-at-pos)) + (godot-gdscript-info-line-ends-backslash-p)) + (point-marker)))))))) + +(defun godot-gdscript-info-block-continuation-line-p () + "Return non-nil if current line is a continuation of a block." + (save-excursion + (when (godot-gdscript-info-continuation-line-p) + (forward-line -1) + (back-to-indentation) + (when (looking-at (godot-gdscript-rx block-start)) + (point-marker))))) + +(defun godot-gdscript-info-assignment-continuation-line-p () + "Check if current line is a 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 (godot-gdscript-info-continuation-line-p) + (forward-line -1) + (back-to-indentation) + (when (and (not (looking-at (godot-gdscript-rx block-start))) + (and (re-search-forward (godot-gdscript-rx not-simple-operator + assignment-operator + not-simple-operator) + (line-end-position) t) + (not (godot-gdscript-syntax-context-type)))) + (skip-syntax-forward "\s") + (point-marker))))) + +(defun godot-gdscript-info-looking-at-beginning-of-defun (&optional syntax-ppss) + "Check if point is at `beginning-of-defun' using SYNTAX-PPSS." + (and (not (godot-gdscript-syntax-context-type (or syntax-ppss (syntax-ppss)))) + (save-excursion + (beginning-of-line 1) + (looking-at godot-gdscript-nav-beginning-of-defun-regexp)))) + +(defun godot-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))) ?_) + ?#)) + +(defun godot-gdscript-info-current-line-empty-p () + "Return non-nil if current line is empty, ignoring whitespace." + (save-excursion + (beginning-of-line 1) + (looking-at + (godot-gdscript-rx line-start (* whitespace) + (group (* not-newline)) + (* whitespace) line-end)) + (string-equal "" (match-string-no-properties 1)))) + +(defun godot-gdscript-info-encoding-from-cookie () + "Detect current buffer's encoding from its coding cookie. +Returns the encoding as a symbol." + (let ((first-two-lines + (save-excursion + (save-restriction + (widen) + (goto-char (point-min)) + (forward-line 2) + (buffer-substring-no-properties + (point) + (point-min)))))) + (when (string-match (godot-gdscript-rx coding-cookie) first-two-lines) + (intern (match-string-no-properties 1 first-two-lines))))) + +(defun godot-gdscript-info-encoding () + "Return encoding for file. +Try `godot-gdscript-info-encoding-from-cookie', if none is found then +default to utf-8." + ;; If no encoding is defined, then it's safe to use UTF-8: Godot-Gdscript 2 + ;; uses ASCII as default while Godot-Gdscript 3 uses UTF-8. This means that + ;; in the worst case scenario godot-gdscript.el will make things work for + ;; Godot-Gdscript 2 files with unicode data and no encoding defined. + (or (godot-gdscript-info-encoding-from-cookie) + 'utf-8)) + + +;;; Utility functions + +(defun godot-gdscript-util-goto-line (line-number) + "Move point to LINE-NUMBER." + (goto-char (point-min)) + (forward-line (1- line-number))) + +;; Stolen from org-mode +(defun godot-gdscript-util-clone-local-variables (from-buffer &optional regexp) + "Clone local variables from FROM-BUFFER. +Optional argument REGEXP selects variables to clone and defaults +to \"^godot-gdscript-\"." + (mapc + (lambda (pair) + (and (symbolp (car pair)) + (string-match (or regexp "^godot-gdscript-") + (symbol-name (car pair))) + (set (make-local-variable (car pair)) + (cdr pair)))) + (buffer-local-variables from-buffer))) + +(defun godot-gdscript-util-forward-comment (&optional direction) + "Godot-Gdscript mode specific version of `forward-comment'. +Optional argument DIRECTION defines the direction to move to." + (let ((comment-start (godot-gdscript-syntax-context 'comment)) + (factor (if (< (or direction 0) 0) + -99999 + 99999))) + (when comment-start + (goto-char comment-start)) + (forward-comment factor))) + +(defun godot-gdscript-util-popn (lst n) + "Return LST first N elements. +N should be an integer, when negative its opposite is used. +When N is bigger than the length of LST, the list is +returned as is." + (let* ((n (min (abs n))) + (len (length lst)) + (acc)) + (if (> n len) + lst + (while (< 0 n) + (setq acc (cons (car lst) acc) + lst (cdr lst) + n (1- n))) + (reverse acc)))) + +(defun godot-gdscript-util-strip-string (string) + "Strip STRING whitespace and newlines from end and beginning." + (replace-regexp-in-string + (rx (or (: string-start (* (any whitespace ?\r ?\n))) + (: (* (any whitespace ?\r ?\n)) string-end))) + "" + string)) + +(defun godot-gdscript-util-valid-regexp-p (regexp) + "Return non-nil if REGEXP is valid." + (ignore-errors (string-match regexp "") t)) + + +(defun godot-gdscript-electric-pair-string-delimiter () + "Identify the delimiter for a string." + (when (and electric-pair-mode + (memq last-command-event '(?\" ?\')) + (let ((count 0)) + (while (eq (char-before (- (point) count)) last-command-event) + (cl-incf count)) + (= count 3)) + (eq (char-after) last-command-event)) + (save-excursion (insert (make-string 2 last-command-event))))) + +;;;###autoload +(define-derived-mode godot-gdscript-mode prog-mode "Godot-Gdscript" + "Major mode for editing Godot Engine GDScript files. + +\\{godot-gdscript-mode-map}" + (set (make-local-variable 'tab-width) 4) + (set (make-local-variable 'indent-tabs-mode) t) + + (set (make-local-variable 'comment-start) "# ") + (set (make-local-variable 'comment-start-skip) "#+\\s-*") + + (set (make-local-variable 'parse-sexp-lookup-properties) t) + (set (make-local-variable 'parse-sexp-ignore-comments) t) + + (set (make-local-variable 'forward-sexp-function) + 'godot-gdscript-nav-forward-sexp) + + (set (make-local-variable 'font-lock-defaults) + '(godot-gdscript-font-lock-keywords nil nil nil nil)) + + (set (make-local-variable 'syntax-propertize-function) + godot-gdscript-syntax-propertize-function) + + (set (make-local-variable 'indent-line-function) + #'godot-gdscript-indent-line-function) + (set (make-local-variable 'indent-region-function) #'godot-gdscript-indent-region) + ;; Because indentation is not redundant, we cannot safely reindent code. + (setq-local electric-indent-inhibit t) + (setq-local electric-indent-chars (cons ?: electric-indent-chars)) + + ;; Add """ ... """ pairing to electric-pair-mode. + (add-hook 'post-self-insert-hook + #'godot-gdscript-electric-pair-string-delimiter 'append t) + + (set (make-local-variable 'paragraph-start) "\\s-*$") + (set (make-local-variable 'fill-paragraph-function) + #'godot-gdscript-fill-paragraph) + + (set (make-local-variable 'beginning-of-defun-function) + #'godot-gdscript-nav-beginning-of-defun) + (set (make-local-variable 'end-of-defun-function) + #'godot-gdscript-nav-end-of-defun) + + (add-hook 'completion-at-point-functions + #'godot-gdscript-completion-complete-at-point nil 'local) + + (add-hook 'post-self-insert-hook + #'godot-gdscript-indent-post-self-insert-function 'append 'local) + + (set (make-local-variable 'imenu-create-index-function) + #'godot-gdscript-imenu-create-index) + + (set (make-local-variable 'add-log-current-defun-function) + #'godot-gdscript-info-current-defun) + + (add-hook 'which-func-functions #'godot-gdscript-info-current-defun nil t) + + (set (make-local-variable 'skeleton-further-elements) + '((abbrev-mode nil) + (< '(backward-delete-char-untabify (min godot-gdscript-indent-offset + (current-column)))) + (^ '(- (1+ (current-indentation)))))) + + (add-to-list 'hs-special-modes-alist + `(godot-gdscript-mode "^\\s-*\\(?:def\\|class\\)\\>" nil "#" + ,(lambda (_arg) + (godot-gdscript-nav-end-of-defun)) nil)) + + (set (make-local-variable 'outline-regexp) + (godot-gdscript-rx (* space) block-start)) + (set (make-local-variable 'outline-heading-end-regexp) ":[^\n]*\n") + (set (make-local-variable 'outline-level) + #'(lambda () + "`outline-level' function for Godot-Gdscript mode." + (1+ (/ (current-indentation) godot-gdscript-indent-offset)))) + + (godot-gdscript-skeleton-add-menu-items) + + (make-local-variable 'godot-gdscript-shell-internal-buffer) + + (when godot-gdscript-indent-guess-indent-offset + (godot-gdscript-indent-guess-indent-offset))) + + + +(provide 'extensions/godot/godot-gdscript) + +;; Local Variables: +;; coding: utf-8 +;; indent-tabs-mode: nil +;; End: + +;;; godot-gdscript.el ends here diff --git a/lib/extensions/godot/init.el b/lib/extensions/godot/init.el index 0b5584d..eb7d495 100644 --- a/lib/extensions/godot/init.el +++ b/lib/extensions/godot/init.el @@ -10,7 +10,8 @@ (defun setup-gdscript() (interactive) - (setq tab-width 2)) + (setq tab-width 2) + (setq indent-tab-mode nil)) ;;;###autoload (defun extensions/godot-initialize ()