From d19416d15c29368112fba9a7437930abcec9af3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=ADn?= Date: Sun, 12 Mar 2023 13:38:34 +0100 Subject: [PATCH 1/6] Fix pluralization in shortdoc-help-fns-examples-function * lisp/emacs-lisp/shortdoc.el (shortdoc-help-fns-examples-function): Implement a better logic to pluralize "Example", by counting the number of arrow characters in the example string. (Bug#61877) * test/lisp/emacs-lisp/shortdoc-tests.el (shortdoc-help-fns-examples-function-test): Add a test. --- lisp/emacs-lisp/shortdoc.el | 35 ++++++++++++++++++++++---- test/lisp/emacs-lisp/shortdoc-tests.el | 15 +++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lisp/emacs-lisp/shortdoc.el b/lisp/emacs-lisp/shortdoc.el index 6e3ebc7c6a2..9a6f5dd12ce 100644 --- a/lisp/emacs-lisp/shortdoc.el +++ b/lisp/emacs-lisp/shortdoc.el @@ -1621,13 +1621,38 @@ doesn't has any shortdoc information." You can add this function to the `help-fns-describe-function-functions' hook to show examples of using FUNCTION in *Help* buffers produced by \\[describe-function]." - (let ((examples (shortdoc-function-examples function)) - (times 0)) + (let* ((examples (shortdoc-function-examples function)) + (num-examples (length examples)) + (times 0)) (dolist (example examples) (when (zerop times) - (if (eq (length examples) 1) - (insert "\n Example:\n\n") - (insert "\n Examples:\n\n"))) + (if (> num-examples 1) + (insert "\n Examples:\n\n") + ;; Some functions have more than one example per group. + ;; Count the number of arrows to know if we need to + ;; pluralize "Example". + (let* ((text (cdr example)) + (count 0) + (pos 0) + (end (length text)) + (double-arrow (if (char-displayable-p ?⇒) + " ⇒" + " =>")) + (double-arrow-example (if (char-displayable-p ?⇒) + " e.g. ⇒" + " e.g. =>")) + (single-arrow (if (char-displayable-p ?→) + " →" + " ->"))) + (while (and (< pos end) + (or (string-match double-arrow text pos) + (string-match double-arrow-example text pos) + (string-match single-arrow text pos))) + (setq count (1+ count) + pos (match-end 0))) + (if (> count 1) + (insert "\n Examples:\n\n") + (insert "\n Example:\n\n"))))) (setq times (1+ times)) (insert " ") (insert (cdr example)) diff --git a/test/lisp/emacs-lisp/shortdoc-tests.el b/test/lisp/emacs-lisp/shortdoc-tests.el index a65a4a5ddc3..d2dfbc66864 100644 --- a/test/lisp/emacs-lisp/shortdoc-tests.el +++ b/test/lisp/emacs-lisp/shortdoc-tests.el @@ -75,6 +75,21 @@ (should (equal '((regexp . "(string-match-p \"^[fo]+\" \"foobar\")\n => 0")) (shortdoc-function-examples 'string-match-p)))) +(ert-deftest shortdoc-help-fns-examples-function-test () + "Test that `shortdoc-help-fns-examples-function' correctly prints ELisp function examples." + (with-temp-buffer + (shortdoc-help-fns-examples-function 'string-fill) + (should (equal "\n Examples:\n\n (string-fill \"Three short words\" 12)\n => \"Three short\\nwords\"\n (string-fill \"Long-word\" 3)\n => \"Long-word\"\n\n" + (buffer-substring-no-properties (point-min) (point-max)))) + (erase-buffer) + (shortdoc-help-fns-examples-function 'assq) + (should (equal "\n Examples:\n\n (assq 'foo '((foo . bar) (zot . baz)))\n => (foo . bar)\n\n (assq 'b '((a . 1) (b . 2)))\n => (b . 2)\n\n" + (buffer-substring-no-properties (point-min) (point-max)))) + (erase-buffer) + (shortdoc-help-fns-examples-function 'string-trim) + (should (equal "\n Example:\n\n (string-trim \" foo \")\n => \"foo\"\n\n" + (buffer-substring-no-properties (point-min) (point-max)))))) + (provide 'shortdoc-tests) ;;; shortdoc-tests.el ends here From 802e64922bcee40c8362b9627aa33a0de0c068d7 Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 17:08:50 +0200 Subject: [PATCH 2/6] Add heex-ts-mode (Bug#61996) * etc/NEWS: Mention the new mode. * lisp/progmodes/heex-ts-mode.el: New file. * test/lisp/progmodes/heex-ts-mode-tests.el: New file. * test/lisp/progmodes/heex-ts-mode-resources/indent.erts: New file. * admin/notes/tree-sitter/build-module/batch.sh: * admin/notes/tree-sitter/build-module/build.sh: Add HEEx support. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 3 + lisp/progmodes/heex-ts-mode.el | 185 ++++++++++++++++++ .../heex-ts-mode-resources/indent.erts | 47 +++++ test/lisp/progmodes/heex-ts-mode-tests.el | 9 + 6 files changed, 248 insertions(+) create mode 100644 lisp/progmodes/heex-ts-mode.el create mode 100644 test/lisp/progmodes/heex-ts-mode-resources/indent.erts create mode 100644 test/lisp/progmodes/heex-ts-mode-tests.el diff --git a/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh index 58272c74549..8b0072782e8 100755 --- a/admin/notes/tree-sitter/build-module/batch.sh +++ b/admin/notes/tree-sitter/build-module/batch.sh @@ -10,6 +10,7 @@ languages=( 'dockerfile' 'go' 'go-mod' + 'heex' 'html' 'javascript' 'json' diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh index 9dc674237ca..78ecfb5bc82 100755 --- a/admin/notes/tree-sitter/build-module/build.sh +++ b/admin/notes/tree-sitter/build-module/build.sh @@ -36,6 +36,9 @@ case "${lang}" in lang="gomod" org="camdencheek" ;; + "heex") + org="phoenixframework" + ;; "typescript") sourcedir="tree-sitter-typescript/typescript/src" grammardir="tree-sitter-typescript/typescript" diff --git a/etc/NEWS b/etc/NEWS index e43aac614c9..682928afa8e 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -248,6 +248,9 @@ following to you init file: An optional major mode based on the tree-sitter library for editing HTML files. +*** New major mode heex-ts-mode'. +A major mode based on the tree-sitter library for editing HEEx files. + --- ** The highly accessible Modus themes collection has six items. The 'modus-operandi' and 'modus-vivendi' are the main themes that have diff --git a/lisp/progmodes/heex-ts-mode.el b/lisp/progmodes/heex-ts-mode.el new file mode 100644 index 00000000000..68a537b9229 --- /dev/null +++ b/lisp/progmodes/heex-ts-mode.el @@ -0,0 +1,185 @@ +;;; heex-ts-mode.el --- Major mode for Heex with tree-sitter support -*- lexical-binding: t; -*- + +;; Copyright (C) 2022-2023 Free Software Foundation, Inc. + +;; Author: Wilhelm H Kirschbaum +;; Created: November 2022 +;; Keywords: elixir languages tree-sitter + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: +;; +;; This package provides `heex-ts-mode' which is a major mode for editing +;; HEEx files that uses Tree Sitter to parse the language. +;; +;; This package is compatible with and was tested against the tree-sitter grammar +;; for HEEx found at https://github.com/phoenixframework/tree-sitter-heex. + +;;; Code: + +(require 'treesit) +(eval-when-compile (require 'rx)) + +(declare-function treesit-parser-create "treesit.c") +(declare-function treesit-node-child "treesit.c") +(declare-function treesit-node-type "treesit.c") +(declare-function treesit-node-start "treesit.c") + +(defgroup heex-ts nil + "Major mode for editing HEEx code." + :prefix "heex-ts-" + :group 'langauges) + +(defcustom heex-ts-indent-offset 2 + "Indentation of HEEx statements." + :version "30.1" + :type 'integer + :safe 'integerp + :group 'heex-ts) + +(defconst heex-ts--sexp-regexp + (rx bol + (or "directive" "tag" "component" "slot" + "attribute" "attribute_value" "quoted_attribute_value") + eol)) + +;; There seems to be no parent directive block for tree-sitter-heex, +;; so we ignore them for now until we learn how to query them. +;; https://github.com/phoenixframework/tree-sitter-heex/issues/28 +(defvar heex-ts--indent-rules + (let ((offset heex-ts-indent-offset)) + `((heex + ((parent-is "fragment") + (lambda (node parent &rest _) + ;; If HEEx is embedded indent to parent + ;; otherwise indent to the bol. + (if (eq (treesit-language-at (point-min)) 'heex) + (point-min) + (save-excursion + (goto-char (treesit-node-start parent)) + (back-to-indentation) + (point)) + )) 0) + ((node-is "end_tag") parent-bol 0) + ((node-is "end_component") parent-bol 0) + ((node-is "end_slot") parent-bol 0) + ((node-is "/>") parent-bol 0) + ((node-is ">") parent-bol 0) + ((parent-is "comment") prev-adaptive-prefix 0) + ((parent-is "component") parent-bol ,offset) + ((parent-is "tag") parent-bol ,offset) + ((parent-is "start_tag") parent-bol ,offset) + ((parent-is "component") parent-bol ,offset) + ((parent-is "start_component") parent-bol ,offset) + ((parent-is "slot") parent-bol ,offset) + ((parent-is "start_slot") parent-bol ,offset) + ((parent-is "self_closing_tag") parent-bol ,offset) + (no-node parent-bol ,offset))))) + +(defvar heex-ts--font-lock-settings + (when (treesit-available-p) + (treesit-font-lock-rules + :language 'heex + :feature 'heex-comment + '((comment) @font-lock-comment-face) + :language 'heex + :feature 'heex-doctype + '((doctype) @font-lock-doc-face) + :language 'heex + :feature 'heex-tag + `([(tag_name) (slot_name)] @font-lock-function-name-face) + :language 'heex + :feature 'heex-attribute + `((attribute_name) @font-lock-variable-name-face) + :language 'heex + :feature 'heex-keyword + `((special_attribute_name) @font-lock-keyword-face) + :language 'heex + :feature 'heex-string + `([(attribute_value) (quoted_attribute_value)] @font-lock-constant-face) + :language 'heex + :feature 'heex-component + `([ + (component_name) @font-lock-function-name-face + (module) @font-lock-keyword-face + (function) @font-lock-keyword-face + "." @font-lock-keyword-face + ]))) + "Tree-sitter font-lock settings.") + +(defun heex-ts--defun-name (node) + "Return the name of the defun NODE. +Return nil if NODE is not a defun node or doesn't have a name." + (pcase (treesit-node-type node) + ((or "component" "slot" "tag") + (string-trim + (treesit-node-text + (treesit-node-child (treesit-node-child node 0) 1) nil))) + (_ nil))) + +(defun heex-ts--forward-sexp (&optional arg) + "Move forward across one balanced expression (sexp). +With ARG, do it many times. Negative ARG means move backward." + (or arg (setq arg 1)) + (funcall + (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing) + heex-ts--sexp-regexp + (abs arg))) + +;;;###autoload +(define-derived-mode heex-ts-mode html-mode "HEEx" + "Major mode for editing HEEx, powered by tree-sitter." + :group 'heex-ts + + (when (treesit-ready-p 'heex) + (treesit-parser-create 'heex) + + ;; Comments + (setq-local treesit-text-type-regexp + (regexp-opt '("comment" "text"))) + + (setq-local forward-sexp-function #'heex-ts--forward-sexp) + + ;; Navigation. + (setq-local treesit-defun-type-regexp + (rx bol (or "component" "tag" "slot") eol)) + (setq-local treesit-defun-name-function #'heex-ts--defun-name) + + ;; Imenu + (setq-local treesit-simple-imenu-settings + '(("Component" "\\`component\\'" nil nil) + ("Slot" "\\`slot\\'" nil nil) + ("Tag" "\\`tag\\'" nil nil))) + + (setq-local treesit-font-lock-settings heex-ts--font-lock-settings) + + (setq-local treesit-simple-indent-rules heex-ts--indent-rules) + + (setq-local treesit-font-lock-feature-list + '(( heex-comment heex-keyword heex-doctype ) + ( heex-component heex-tag heex-attribute heex-string ) + () ())) + + (treesit-major-mode-setup))) + +(if (treesit-ready-p 'heex) + ;; Both .heex and the deprecated .leex files should work + ;; with the tree-sitter-heex grammar. + (add-to-list 'auto-mode-alist '("\\.[hl]?eex\\'" . heex-ts-mode))) + +(provide 'heex-ts-mode) +;;; heex-ts-mode.el ends here diff --git a/test/lisp/progmodes/heex-ts-mode-resources/indent.erts b/test/lisp/progmodes/heex-ts-mode-resources/indent.erts new file mode 100644 index 00000000000..500ddb2b536 --- /dev/null +++ b/test/lisp/progmodes/heex-ts-mode-resources/indent.erts @@ -0,0 +1,47 @@ +Code: + (lambda () + (setq indent-tabs-mode nil) + (heex-ts-mode) + (indent-region (point-min) (point-max))) + +Point-Char: $ + +Name: Tag + +=-= +
+ div +
+=-= +
+ div +
+=-=-= + +Name: Component + +=-= + + foobar + +=-= + + foobar + +=-=-= + +Name: Slots + +=-= + + <:bar> + foobar + + +=-= + + <:bar> + foobar + + +=-=-= diff --git a/test/lisp/progmodes/heex-ts-mode-tests.el b/test/lisp/progmodes/heex-ts-mode-tests.el new file mode 100644 index 00000000000..b59126e136a --- /dev/null +++ b/test/lisp/progmodes/heex-ts-mode-tests.el @@ -0,0 +1,9 @@ +(require 'ert) +(require 'ert-x) +(require 'treesit) + +(ert-deftest heex-ts-mode-test-indentation () + (skip-unless (treesit-ready-p 'heex)) + (ert-test-erts-file (ert-resource-file "indent.erts"))) + +(provide 'heex-ts-mode-tests) From d965d030879d9ca4ef5098cb4e2e7c56128b904b Mon Sep 17 00:00:00 2001 From: Wilhelm H Kirschbaum Date: Sun, 12 Mar 2023 17:10:43 +0200 Subject: [PATCH 3/6] Add elixir-ts-mode (Bug#61996) * etc/NEWS: Mention the new mode. * lisp/progmodes/elixir-ts-mode.el: New file. * test/lisp/progmodes/elixir-ts-mode-tests.el: New file. * test/lisp/progmodes/elixir-ts-mode-resources/indent.erts: New file. * admin/notes/tree-sitter/build-module/batch.sh: * admin/notes/tree-sitter/build-module/build.sh: Add Elixir support. * lisp/progmodes/eglot.el (eglot-server-programs): Add elixir-ts-mode. --- admin/notes/tree-sitter/build-module/batch.sh | 1 + admin/notes/tree-sitter/build-module/build.sh | 3 + etc/NEWS | 4 + lisp/progmodes/eglot.el | 2 +- lisp/progmodes/elixir-ts-mode.el | 634 ++++++++++++++++++ .../elixir-ts-mode-resources/indent.erts | 308 +++++++++ test/lisp/progmodes/elixir-ts-mode-tests.el | 31 + 7 files changed, 982 insertions(+), 1 deletion(-) create mode 100644 lisp/progmodes/elixir-ts-mode.el create mode 100644 test/lisp/progmodes/elixir-ts-mode-resources/indent.erts create mode 100644 test/lisp/progmodes/elixir-ts-mode-tests.el diff --git a/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh index 8b0072782e8..1d4076564dc 100755 --- a/admin/notes/tree-sitter/build-module/batch.sh +++ b/admin/notes/tree-sitter/build-module/batch.sh @@ -8,6 +8,7 @@ languages=( 'css' 'c-sharp' 'dockerfile' + 'elixir' 'go' 'go-mod' 'heex' diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh index 78ecfb5bc82..0832875168b 100755 --- a/admin/notes/tree-sitter/build-module/build.sh +++ b/admin/notes/tree-sitter/build-module/build.sh @@ -31,6 +31,9 @@ case "${lang}" in "cmake") org="uyha" ;; + "elixir") + org="elixir-lang" + ;; "go-mod") # The parser is called "gomod". lang="gomod" diff --git a/etc/NEWS b/etc/NEWS index 682928afa8e..662d2ad52b7 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -251,6 +251,10 @@ HTML files. *** New major mode heex-ts-mode'. A major mode based on the tree-sitter library for editing HEEx files. +*** New major mode elixir-ts-mode'. +A major mode based on the tree-sitter library for editing Elixir +files. + --- ** The highly accessible Modus themes collection has six items. The 'modus-operandi' and 'modus-vivendi' are the main themes that have diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2f8d2002cd3..7b2341f3f49 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -221,7 +221,7 @@ chosen (interactively or automatically)." ((java-mode java-ts-mode) . ("jdtls")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) - (elixir-mode . ("language_server.sh")) + ((elixir-ts-mode elixir-mode) . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ,(eglot-alternatives '("metals" "metals-emacs"))) diff --git a/lisp/progmodes/elixir-ts-mode.el b/lisp/progmodes/elixir-ts-mode.el new file mode 100644 index 00000000000..8adf647b081 --- /dev/null +++ b/lisp/progmodes/elixir-ts-mode.el @@ -0,0 +1,634 @@ +;;; elixir-ts-mode.el --- Major mode for Elixir with tree-sitter support -*- lexical-binding: t; -*- + +;; Copyright (C) 2022-2023 Free Software Foundation, Inc. + +;; Author: Wilhelm H Kirschbaum +;; Created: November 2022 +;; Keywords: elixir languages tree-sitter + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: +;; +;; This package provides `elixir-ts-mode' which is a major mode for editing +;; Elixir files and embedded HEEx templates that uses Tree Sitter to parse +;; the language. +;; +;; This package is compatible with and was tested against the tree-sitter grammar +;; for Elixir found at https://github.com/elixir-lang/tree-sitter-elixir. +;; +;; Features +;; +;; * Indent +;; +;; `elixir-ts-mode' tries to replicate the indentation provided by +;; mix format, but will come with some minor differences. +;; +;; * IMenu +;; * Navigation +;; * Which-fun + +;;; Code: + +(require 'treesit) +(require 'heex-ts-mode) +(eval-when-compile (require 'rx)) + +(declare-function treesit-parser-create "treesit.c") +(declare-function treesit-node-child "treesit.c") +(declare-function treesit-node-type "treesit.c") +(declare-function treesit-node-child-by-field-name "treesit.c") +(declare-function treesit-parser-language "treesit.c") +(declare-function treesit-parser-included-ranges "treesit.c") +(declare-function treesit-parser-list "treesit.c") +(declare-function treesit-node-parent "treesit.c") +(declare-function treesit-node-start "treesit.c") +(declare-function treesit-query-compile "treesit.c") +(declare-function treesit-node-eq "treesit.c") +(declare-function treesit-node-prev-sibling "treesit.c") + +(defgroup elixir-ts nil + "Major mode for editing Elixir code." + :prefix "elixir-ts-" + :group 'languages) + +(defcustom elixir-ts-indent-offset 2 + "Indentation of Elixir statements." + :version "30.1" + :type 'integer + :safe 'integerp + :group 'elixir-ts) + +(defface elixir-ts-font-comment-doc-identifier-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc tags in Elixir files.") + +(defface elixir-ts-font-comment-doc-attribute-face + '((t (:inherit font-lock-doc-face))) + "Face used for @comment.doc.__attribute__ tags in Elixir files.") + +(defface elixir-ts-font-sigil-name-face + '((t (:inherit font-lock-string-face))) + "Face used for @__name__ tags in Elixir files.") + +(defconst elixir-ts--sexp-regexp + (rx bol + (or "call" "stab_clause" "binary_operator" "list" "tuple" "map" "pair" + "sigil" "string" "atom" "pair" "alias" "arguments" "atom" "identifier" + "boolean" "quoted_content") + eol)) + +(defconst elixir-ts--test-definition-keywords + '("describe" "test")) + +(defconst elixir-ts--definition-keywords + '("def" "defdelegate" "defexception" "defguard" "defguardp" + "defimpl" "defmacro" "defmacrop" "defmodule" "defn" "defnp" + "defoverridable" "defp" "defprotocol" "defstruct")) + +(defconst elixir-ts--definition-keywords-re + (concat "^" (regexp-opt elixir-ts--definition-keywords) "$")) + +(defconst elixir-ts--kernel-keywords + '("alias" "case" "cond" "else" "for" "if" "import" "quote" + "raise" "receive" "require" "reraise" "super" "throw" "try" + "unless" "unquote" "unquote_splicing" "use" "with")) + +(defconst elixir-ts--kernel-keywords-re + (concat "^" (regexp-opt elixir-ts--kernel-keywords) "$")) + +(defconst elixir-ts--builtin-keywords + '("__MODULE__" "__DIR__" "__ENV__" "__CALLER__" "__STACKTRACE__")) + +(defconst elixir-ts--builtin-keywords-re + (concat "^" (regexp-opt elixir-ts--builtin-keywords) "$")) + +(defconst elixir-ts--doc-keywords + '("moduledoc" "typedoc" "doc")) + +(defconst elixir-ts--doc-keywords-re + (concat "^" (regexp-opt elixir-ts--doc-keywords) "$")) + +(defconst elixir-ts--reserved-keywords + '("when" "and" "or" "not" "in" + "not in" "fn" "do" "end" "catch" "rescue" "after" "else")) + +(defconst elixir-ts--reserved-keywords-re + (concat "^" (regexp-opt elixir-ts--reserved-keywords) "$")) + +(defconst elixir-ts--reserved-keywords-vector + (apply #'vector elixir-ts--reserved-keywords)) + +(defvar elixir-ts--capture-anonymous-function-end + (when (treesit-available-p) + (treesit-query-compile 'elixir '((anonymous_function "end" @end))))) + +(defvar elixir-ts--capture-operator-parent + (when (treesit-available-p) + (treesit-query-compile 'elixir '((binary_operator operator: _ @val))))) + +(defvar elixir-ts--syntax-table + (let ((table (make-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) + (modify-syntax-entry ?? "w" table) + (modify-syntax-entry ?~ "w" table) + (modify-syntax-entry ?! "_" table) + (modify-syntax-entry ?' "\"" table) + (modify-syntax-entry ?\" "\"" table) + (modify-syntax-entry ?# "<" table) + (modify-syntax-entry ?\n ">" table) + (modify-syntax-entry ?\( "()" table) + (modify-syntax-entry ?\) ")(" table) + (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 `elixir-ts-mode'.") + +(defun elixir-ts--argument-indent-offset (node _parent &rest _) + "Return the argument offset position for NODE." + (if (treesit-node-prev-sibling node t) 0 elixir-ts-indent-offset)) + +(defun elixir-ts--argument-indent-anchor (node parent &rest _) + "Return the argument anchor position for NODE and PARENT." + (let ((first-sibling (treesit-node-child parent 0 t))) + (if (and first-sibling (not (treesit-node-eq first-sibling node))) + (treesit-node-start first-sibling) + (elixir-ts--parent-expression-start node parent)))) + +(defun elixir-ts--parent-expression-start (_node parent &rest _) + "Return the indentation expression start for NODE and PARENT." + ;; If the parent is the first expression on the line return the + ;; parent start of node position, otherwise use the parent call + ;; start if available. + (if (eq (treesit-node-start parent) + (save-excursion + (goto-char (treesit-node-start parent)) + (back-to-indentation) + (point))) + (treesit-node-start parent) + (let ((expr-parent + (treesit-parent-until + parent + (lambda (n) + (member (treesit-node-type n) + '("call" "binary_operator" "keywords" "list")))))) + (save-excursion + (goto-char (treesit-node-start expr-parent)) + (back-to-indentation) + (if (looking-at "|>") + (point) + (treesit-node-start expr-parent)))))) + +(defvar elixir-ts--indent-rules + (let ((offset elixir-ts-indent-offset)) + `((elixir + ((parent-is "^source$") column-0 0) + ((parent-is "^string$") parent-bol 0) + ((parent-is "^quoted_content$") + (lambda (_n parent bol &rest _) + (save-excursion + (back-to-indentation) + (if (bolp) + (progn + (goto-char (treesit-node-start parent)) + (back-to-indentation) + (point)) + (point)))) + 0) + ((node-is "^|>$") parent-bol 0) + ((node-is "^|$") parent-bol 0) + ((node-is "^]$") ,'elixir-ts--parent-expression-start 0) + ((node-is "^}$") ,'elixir-ts--parent-expression-start 0) + ((node-is "^)$") ,'elixir-ts--parent-expression-start 0) + ((node-is "^else_block$") grand-parent 0) + ((node-is "^catch_block$") grand-parent 0) + ((node-is "^rescue_block$") grand-parent 0) + ((node-is "^after_block$") grand-parent 0) + ((parent-is "^else_block$") parent ,offset) + ((parent-is "^catch_block$") parent ,offset) + ((parent-is "^rescue_block$") parent ,offset) + ((parent-is "^rescue_block$") parent ,offset) + ((parent-is "^after_block$") parent ,offset) + ((parent-is "^access_call$") + ,'elixir-ts--argument-indent-anchor + ,'elixir-ts--argument-indent-offset) + ((parent-is "^tuple$") + ,'elixir-ts--argument-indent-anchor + ,'elixir-ts--argument-indent-offset) + ((parent-is "^list$") + ,'elixir-ts--argument-indent-anchor + ,'elixir-ts--argument-indent-offset) + ((parent-is "^pair$") parent ,offset) + ((parent-is "^map_content$") parent-bol 0) + ((parent-is "^map$") ,'elixir-ts--parent-expression-start ,offset) + ((node-is "^stab_clause$") parent-bol ,offset) + ((query ,elixir-ts--capture-operator-parent) grand-parent 0) + ((node-is "^when$") parent 0) + ((node-is "^keywords$") parent-bol ,offset) + ((parent-is "^body$") + (lambda (node parent _) + (save-excursion + ;; The grammar adds a comment outside of the body, so we have to indent + ;; to the grand-parent if it is available. + (goto-char (treesit-node-start + (or (treesit-node-parent parent) (parent)))) + (back-to-indentation) + (point))) + ,offset) + ((parent-is "^arguments$") + ,'elixir-ts--argument-indent-anchor + ,'elixir-ts--argument-indent-offset) + ;; Handle incomplete maps when parent is ERROR. + ((n-p-gp "^binary_operator$" "ERROR" nil) parent-bol 0) + ;; When there is an ERROR, just indent to prev-line. + ((parent-is "ERROR") prev-line 0) + ((node-is "^binary_operator$") + (lambda (node parent &rest _) + (let ((top-level + (treesit-parent-while + node + (lambda (node) + (equal (treesit-node-type node) + "binary_operator"))))) + (if (treesit-node-eq top-level node) + (elixir-ts--parent-expression-start node parent) + (treesit-node-start top-level)))) + (lambda (node parent _) + (cond + ((equal (treesit-node-type parent) "do_block") + ,offset) + ((equal (treesit-node-type parent) "binary_operator") + ,offset) + (t 0)))) + ((parent-is "^binary_operator$") + (lambda (node parent bol &rest _) + (treesit-node-start + (treesit-parent-while + parent + (lambda (node) + (equal (treesit-node-type node) "binary_operator"))))) + ,offset) + ((node-is "^pair$") first-sibling 0) + ((query ,elixir-ts--capture-anonymous-function-end) parent-bol 0) + ((node-is "^end$") standalone-parent 0) + ((parent-is "^do_block$") grand-parent ,offset) + ((parent-is "^anonymous_function$") + elixir-ts--treesit-anchor-grand-parent-bol ,offset) + ((parent-is "^else_block$") parent ,offset) + ((parent-is "^rescue_block$") parent ,offset) + ((parent-is "^catch_block$") parent ,offset) + ((parent-is "^keywords$") parent-bol 0) + ((node-is "^call$") parent-bol ,offset) + ((node-is "^comment$") parent-bol ,offset))))) + +(defvar elixir-ts--font-lock-settings + (treesit-font-lock-rules + :language 'elixir + :feature 'elixir-comment + '((comment) @font-lock-comment-face) + + :language 'elixir + :feature 'elixir-string + :override t + '([(string) (charlist)] @font-lock-string-face) + + :language 'elixir + :feature 'elixir-string-interpolation + :override t + '((string + [ + quoted_end: _ @font-lock-string-face + quoted_start: _ @font-lock-string-face + (quoted_content) @font-lock-string-face + (interpolation + "#{" @font-lock-regexp-grouping-backslash "}" + @font-lock-regexp-grouping-backslash) + ]) + (charlist + [ + quoted_end: _ @font-lock-string-face + quoted_start: _ @font-lock-string-face + (quoted_content) @font-lock-string-face + (interpolation + "#{" @font-lock-regexp-grouping-backslash "}" + @font-lock-regexp-grouping-backslash) + ])) + + :language 'elixir + :feature 'elixir-keyword + `(,elixir-ts--reserved-keywords-vector + @font-lock-keyword-face + (binary_operator + operator: _ @font-lock-keyword-face + (:match ,elixir-ts--reserved-keywords-re @font-lock-keyword-face))) + + :language 'elixir + :feature 'elixir-doc + :override t + `((unary_operator + operator: "@" @elixir-ts-font-comment-doc-attribute-face + operand: (call + target: (identifier) @elixir-ts-font-comment-doc-identifier-face + ;; Arguments can be optional, so adding another + ;; entry without arguments. + ;; If we don't handle then we don't apply font + ;; and the non doc fortification query will take specify + ;; a more specific font which takes precedence. + (arguments + [ + (string) @font-lock-doc-face + (charlist) @font-lock-doc-face + (sigil) @font-lock-doc-face + (boolean) @font-lock-doc-face + ])) + (:match ,elixir-ts--doc-keywords-re + @elixir-ts-font-comment-doc-identifier-face)) + (unary_operator + operator: "@" @elixir-ts-font-comment-doc-attribute-face + operand: (call + target: (identifier) @elixir-ts-font-comment-doc-identifier-face) + (:match ,elixir-ts--doc-keywords-re + @elixir-ts-font-comment-doc-identifier-face))) + + :language 'elixir + :feature 'elixir-unary-operator + `((unary_operator operator: "@" @font-lock-preprocessor-face + operand: [ + (identifier) @font-lock-preprocessor-face + (call target: (identifier) + @font-lock-preprocessor-face) + (boolean) @font-lock-preprocessor-face + (nil) @font-lock-preprocessor-face + ]) + + (unary_operator operator: "&") @font-lock-function-name-face + (operator_identifier) @font-lock-operator-face) + + :language 'elixir + :feature 'elixir-operator + '((binary_operator operator: _ @font-lock-operator-face) + (dot operator: _ @font-lock-operator-face) + (stab_clause operator: _ @font-lock-operator-face) + + [(boolean) (nil)] @font-lock-constant-face + [(integer) (float)] @font-lock-number-face + (alias) @font-lock-type-face + (call target: (dot left: (atom) @font-lock-type-face)) + (char) @font-lock-constant-face + [(atom) (quoted_atom)] @font-lock-type-face + [(keyword) (quoted_keyword)] @font-lock-builtin-face) + + :language 'elixir + :feature 'elixir-call + `((call + target: (identifier) @font-lock-keyword-face + (:match ,elixir-ts--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (:match ,elixir-ts--kernel-keywords-re @font-lock-keyword-face)) + (call + target: [(identifier) @font-lock-function-name-face + (dot right: (identifier) @font-lock-keyword-face)]) + (call + target: (identifier) @font-lock-keyword-face + (arguments + [ + (identifier) @font-lock-keyword-face + (binary_operator + left: (identifier) @font-lock-keyword-face + operator: "when") + ]) + (:match ,elixir-ts--definition-keywords-re @font-lock-keyword-face)) + (call + target: (identifier) @font-lock-keyword-face + (arguments + (binary_operator + operator: "|>" + right: (identifier))) + (:match ,elixir-ts--definition-keywords-re @font-lock-keyword-face))) + + :language 'elixir + :feature 'elixir-constant + `((binary_operator operator: "|>" right: (identifier) + @font-lock-function-name-face) + ((identifier) @font-lock-keyword-face + (:match ,elixir-ts--builtin-keywords-re + @font-lock-keyword-face)) + ((identifier) @font-lock-comment-face + (:match "^_" @font-lock-comment-face)) + (identifier) @font-lock-function-name-face + ["%"] @font-lock-keyward-face + ["," ";"] @font-lock-keyword-face + ["(" ")" "[" "]" "{" "}" "<<" ">>"] @font-lock-keyword-face) + + :language 'elixir + :feature 'elixir-sigil + :override t + `((sigil + (sigil_name) @elixir-ts-font-sigil-name-face + quoted_start: _ @font-lock-string-face + quoted_end: _ @font-lock-string-face + (:match "^[sSwWpP]$" @elixir-ts-font-sigil-name-face)) + @font-lock-string-face + (sigil + (sigil_name) @elixir-ts-font-sigil-name-face + quoted_start: _ @font-lock-regex-face + quoted_end: _ @font-lock-regex-face + (:match "^[rR]$" @elixir-ts-font-sigil-name-face)) + @font-lock-regex-face + (sigil + "~" @font-lock-string-face + (sigil_name) @elixir-ts-font-sigil-name-face + quoted_start: _ @font-lock-string-face + quoted_end: _ @font-lock-string-face + (:match "^[HF]$" @elixir-ts-font-sigil-name-face))) + + :language 'elixir + :feature 'elixir-string-escape + :override t + `((escape_sequence) @font-lock-regexp-grouping-backslash)) + "Tree-sitter font-lock settings.") + +(defvar elixir-ts--treesit-range-rules + (when (treesit-available-p) + (treesit-range-rules + :embed 'heex + :host 'elixir + '((sigil (sigil_name) @name (:match "^[HF]$" @name) (quoted_content) @heex))))) + +(defun elixir-ts--forward-sexp (&optional arg) + "Move forward across one balanced expression (sexp). +With ARG, do it many times. Negative ARG means move backward." + (or arg (setq arg 1)) + (funcall + (if (> arg 0) #'treesit-end-of-thing #'treesit-beginning-of-thing) + (if (eq (treesit-language-at (point)) 'heex) + heex-ts--sexp-regexp + elixir-ts--sexp-regexp) + (abs arg))) + +(defun elixir-ts--treesit-anchor-grand-parent-bol (_n parent &rest _) + "Return the beginning of non-space characters for the parent node of PARENT." + (save-excursion + (goto-char (treesit-node-start (treesit-node-parent parent))) + (back-to-indentation) + (point))) + +(defun elixir-ts--treesit-language-at-point (point) + "Return the language at POINT." + (let* ((range nil) + (language-in-range + (cl-loop + for parser in (treesit-parser-list) + do (setq range + (cl-loop + for range in (treesit-parser-included-ranges parser) + if (and (>= point (car range)) (<= point (cdr range))) + return parser)) + if range + return (treesit-parser-language parser)))) + (if (null language-in-range) + (when-let ((parser (car (treesit-parser-list)))) + (treesit-parser-language parser)) + language-in-range))) + +(defun elixir-ts--defun-p (node) + "Return non-nil when NODE is a defun." + (member (treesit-node-text + (treesit-node-child-by-field-name node "target")) + (append + elixir-ts--definition-keywords + elixir-ts--test-definition-keywords))) + +(defun elixir-ts--defun-name (node) + "Return the name of the defun NODE. +Return nil if NODE is not a defun node or doesn't have a name." + (pcase (treesit-node-type node) + ("call" (let ((node-child + (treesit-node-child (treesit-node-child node 1) 0))) + (pcase (treesit-node-type node-child) + ("alias" (treesit-node-text node-child t)) + ("call" (treesit-node-text + (treesit-node-child-by-field-name node-child "target") t)) + ("binary_operator" + (treesit-node-text + (treesit-node-child-by-field-name + (treesit-node-child-by-field-name node-child "left") "target") + t)) + ("identifier" + (treesit-node-text node-child t)) + (_ nil)))) + (_ nil))) + +;;;###autoload +(define-derived-mode elixir-ts-mode prog-mode "Elixir" + "Major mode for editing Elixir, powered by tree-sitter." + :group 'elixir-ts + :syntax-table elixir-ts--syntax-table + + ;; Comments + (setq-local comment-start "# ") + (setq-local comment-start-skip + (rx "#" (* (syntax whitespace)))) + + (setq-local comment-end "") + (setq-local comment-end-skip + (rx (* (syntax whitespace)) + (group (or (syntax comment-end) "\n")))) + + ;; Compile + (setq-local compile-command "mix") + + (when (treesit-ready-p 'elixir) + ;; The HEEx parser has to be created first for elixir to ensure elixir + ;; is the first language when looking for treesit ranges. + (if (treesit-ready-p 'heex) + (treesit-parser-create 'heex)) + + (treesit-parser-create 'elixir) + + (setq-local treesit-language-at-point-function + 'elixir-ts--treesit-language-at-point) + + ;; Font-lock. + (setq-local treesit-font-lock-settings elixir-ts--font-lock-settings) + (setq-local treesit-font-lock-feature-list + '(( elixir-comment elixir-constant elixir-doc ) + ( elixir-string elixir-keyword elixir-unary-operator + elixir-call elixir-operator ) + ( elixir-sigil elixir-string-escape elixir-string-interpolation))) + + ;; Imenu. + (setq-local treesit-simple-imenu-settings + '((nil "\\`call\\'" elixir-ts--defun-p nil))) + + ;; Indent. + (setq-local treesit-simple-indent-rules elixir-ts--indent-rules) + + ;; Navigation + (setq-local forward-sexp-function #'elixir-ts--forward-sexp) + (setq-local treesit-defun-type-regexp + '("call" . elixir-ts--defun-p)) + + (setq-local treesit-defun-name-function #'elixir-ts--defun-name) + + ;; Embedded Heex + (when (treesit-ready-p 'heex) + (setq-local treesit-range-settings elixir-ts--treesit-range-rules) + + (setq-local treesit-simple-indent-rules + (append treesit-simple-indent-rules heex-ts--indent-rules)) + + (setq-local treesit-font-lock-settings + (append treesit-font-lock-settings + heex-ts--font-lock-settings)) + + (setq-local treesit-simple-indent-rules + (append treesit-simple-indent-rules + heex-ts--indent-rules)) + + (setq-local treesit-font-lock-feature-list + '(( elixir-comment elixir-constant elixir-doc + heex-comment heex-keyword heex-doctype ) + ( elixir-string elixir-keyword elixir-unary-operator + elixir-call elixir-operator + heex-component heex-tag heex-attribute heex-string) + ( elixir-sigil elixir-string-escape + elixir-string-interpolation )))) + + (treesit-major-mode-setup))) + +(if (treesit-ready-p 'elixir) + (progn + (add-to-list 'auto-mode-alist '("\\.elixir\\'" . elixir-ts-mode)) + (add-to-list 'auto-mode-alist '("\\.ex\\'" . elixir-ts-mode)) + (add-to-list 'auto-mode-alist '("\\.exs\\'" . elixir-ts-mode)) + (add-to-list 'auto-mode-alist '("mix\\.lock" . elixir-ts-mode)))) + +(provide 'elixir-ts-mode) + +;;; elixir-ts-mode.el ends here diff --git a/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts new file mode 100644 index 00000000000..748455cc3f2 --- /dev/null +++ b/test/lisp/progmodes/elixir-ts-mode-resources/indent.erts @@ -0,0 +1,308 @@ +Code: + (lambda () + (setq indent-tabs-mode nil) + (elixir-ts-mode) + (indent-region (point-min) (point-max))) + +Point-Char: $ + +Name: Basic modules + +=-= + defmodule Foobar do +def bar() do +"one" + end + end +=-= +defmodule Foobar do + def bar() do + "one" + end +end +=-=-= + +Name: Map + +=-= +map = %{ + "a" => 1, + "b" => 2 +} +=-=-= + +Name: Map in function def + +=-= +def foobar() do + %{ + one: "one", + two: "two", + three: "three", + four: "four" + } +end +=-=-= + +Name: Map in tuple + +=-= +def foo() do + {:ok, + %{ + state + | extra_arguments: extra_arguments, + max_children: max_children, + max_restarts: max_restarts, + max_seconds: max_seconds, + strategy: strategy + }} +end +=-=-= + +Name: Nested maps + +=-= +%{ + foo: "bar", + bar: %{ + foo: "bar" + } +} + +def foo() do + %{ + foo: "bar", + bar: %{ + foo: "bar" + } + } +end +=-=-= + +Name: Block assignments + +=-= +foo = + if true do + "yes" + else + "no" + end +=-=-= + +Name: Function rescue + +=-= +def foo do + "bar" +rescue + e -> + "bar" +end +=-=-= + +Name: With statement +=-= +with one <- one(), + two <- two(), + {:ok, value} <- get_value(one, two) do + {:ok, value} +else + {:error, %{"Message" => message}} -> + {:error, message} +end +=-=-= + +Name: Pipe statements with fn + +=-= +[1, 2] +|> Enum.map(fn num -> + num + 1 +end) +=-=-= + +Name: Pipe statements stab clases + +=-= +[1, 2] +|> Enum.map(fn + x when x < 10 -> x * 2 + x -> x * 3 +end) +=-=-= + +Name: Pipe statements params + +=-= +[1, 2] +|> foobar( + :one, + :two, + :three, + :four +) +=-=-= + +Name: Parameter maps + +=-= +def something(%{ + one: :one, + two: :two + }) do + {:ok, "done"} +end +=-=-= + +Name: Binary operator in else block + +=-= +defp foobar() do + if false do + :foo + else + :bar |> foo + end +end +=-=-= + +Name: Tuple indentation + +=-= +tuple = { + :one, + :two +} + +{ + :one, + :two +} +=-=-= + +Name: Spec and method + +=-= +@spec foobar( + t, + acc, + (one, something -> :bar | far), + (two -> :bar | far) + ) :: any() + when chunk: any +def foobar(enumerable, acc, chunk_fun, after_fun) do + {_, {res, acc}} = + case after_fun.(acc) do + {:one, "one"} -> + "one" + + {:two, "two"} -> + "two" + end +end +=-=-= + +Name: Spec with multi-line result + +=-= +@type result :: + {:done, term} + | {:two} + | {:one} + +@type result :: + { + :done, + term + } + | {:two} + | {:one} + +@type boo_bar :: + (foo :: pos_integer, bar :: pos_integer -> any()) + +@spec foo_bar( + t, + (foo -> any), + (() -> any) | (foo, foo -> boolean) | module() + ) :: any + when foo: any +def foo(one, fun, other) +=-=-= + +Name: String concatenation in call + +=-= +IO.warn( + "one" <> + "two" <> + "bar" +) + +IO.warn( + "foo" <> + "bar" +) +=-=-= + +Name: Incomplete tuple + +=-= +map = { +:foo + +=-= +map = { + :foo + +=-=-= + +Name: Incomplete map + +=-= +map = %{ + "a" => "a", +=-=-= + +Name: Incomplete list + +=-= +map = [ +:foo + +=-= +map = [ + :foo + +=-=-= + +Name: String concatenation + +=-= +"one" <> + "two" <> + "three" <> + "four" +=-=-= + +Name: Tuple with same line first node + +=-= +{:one, + :two} + +{:ok, + fn one -> + one + |> String.upcase(one) + end} +=-=-= + +Name: Long tuple + +=-= +{"January", "February", "March", "April", "May", "June", "July", "August", "September", + "October", "November", "December"} +=-=-= diff --git a/test/lisp/progmodes/elixir-ts-mode-tests.el b/test/lisp/progmodes/elixir-ts-mode-tests.el new file mode 100644 index 00000000000..8e546ad5cc6 --- /dev/null +++ b/test/lisp/progmodes/elixir-ts-mode-tests.el @@ -0,0 +1,31 @@ +;;; c-ts-mode-tests.el --- Tests for Tree-sitter-based C mode -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Code: + +(require 'ert) +(require 'ert-x) +(require 'treesit) + +(ert-deftest elixir-ts-mode-test-indentation () + (skip-unless (and (treesit-ready-p 'elixir) (treesit-ready-p 'heex))) + (ert-test-erts-file (ert-resource-file "indent.erts"))) + +(provide 'elixir-ts-mode-tests) +;;; elixir-ts-mode-tests.el ends here From e87431eda0a73c15865deb554cdb3ba13b7767f6 Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Sun, 12 Mar 2023 17:43:40 +0200 Subject: [PATCH 4/6] ; NEWS markings. --- etc/NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/etc/NEWS b/etc/NEWS index 662d2ad52b7..e31203689e3 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -248,9 +248,11 @@ following to you init file: An optional major mode based on the tree-sitter library for editing HTML files. +--- *** New major mode heex-ts-mode'. A major mode based on the tree-sitter library for editing HEEx files. +--- *** New major mode elixir-ts-mode'. A major mode based on the tree-sitter library for editing Elixir files. From f5f13495f5dac4148c1da8b0ba18c22daf77bb04 Mon Sep 17 00:00:00 2001 From: Michael Albinus Date: Sun, 12 Mar 2023 17:21:57 +0100 Subject: [PATCH 5/6] Make Tramp file name completion more quiet for all backends * lisp/net/tramp-adb.el (tramp-adb-handle-file-name-all-completions): * lisp/net/tramp-archive.el (tramp-archive-handle-file-name-all-completions): * lisp/net/tramp-crypt.el (tramp-crypt-handle-file-name-all-completions): * lisp/net/tramp-fuse.el (tramp-fuse-handle-file-name-all-completions): * lisp/net/tramp-gvfs.el (tramp-gvfs-handle-file-name-all-completions): * lisp/net/tramp-sh.el (tramp-sh-handle-file-name-all-completions): * lisp/net/tramp-smb.el (tramp-smb-handle-file-name-all-completions): * lisp/net/tramp-sudoedit.el (tramp-sudoedit-handle-file-name-all-completions): Return nil when DIRECTORY is missing. (Bug#61890) --- lisp/net/tramp-adb.el | 51 ++++++++++++++-------------- lisp/net/tramp-archive.el | 4 ++- lisp/net/tramp-crypt.el | 25 +++++++------- lisp/net/tramp-fuse.el | 29 ++++++++-------- lisp/net/tramp-gvfs.el | 23 +++++++------ lisp/net/tramp-sh.el | 68 ++++++++++++++++++++------------------ lisp/net/tramp-smb.el | 26 ++++++++------- lisp/net/tramp-sudoedit.el | 39 +++++++++++----------- 8 files changed, 139 insertions(+), 126 deletions(-) diff --git a/lisp/net/tramp-adb.el b/lisp/net/tramp-adb.el index f8c38859477..64f45e7958d 100644 --- a/lisp/net/tramp-adb.el +++ b/lisp/net/tramp-adb.el @@ -432,31 +432,32 @@ Emacs dired can't find files." (defun tramp-adb-handle-file-name-all-completions (filename directory) "Like `file-name-all-completions' for Tramp files." - (all-completions - filename - (with-parsed-tramp-file-name (expand-file-name directory) nil - (with-tramp-file-property v localname "file-name-all-completions" - (tramp-adb-send-command - v (format "%s -a %s | cat" - (tramp-adb-get-ls-command v) - (tramp-shell-quote-argument localname))) - (mapcar - (lambda (f) - (if (file-directory-p (expand-file-name f directory)) - (file-name-as-directory f) - f)) - (with-current-buffer (tramp-get-buffer v) - (delete-dups - (append - ;; On some file systems like "sdcard", "." and ".." are - ;; not included. We fix this by `delete-dups'. - '("." "..") - (delq - nil - (mapcar - (lambda (l) - (and (not (string-match-p (rx bol (* blank) eol) l)) l)) - (split-string (buffer-string) "\n"))))))))))) + (ignore-error file-missing + (all-completions + filename + (with-parsed-tramp-file-name (expand-file-name directory) nil + (with-tramp-file-property v localname "file-name-all-completions" + (tramp-adb-send-command + v (format "%s -a %s | cat" + (tramp-adb-get-ls-command v) + (tramp-shell-quote-argument localname))) + (mapcar + (lambda (f) + (if (file-directory-p (expand-file-name f directory)) + (file-name-as-directory f) + f)) + (with-current-buffer (tramp-get-buffer v) + (delete-dups + (append + ;; On some file systems like "sdcard", "." and ".." are + ;; not included. We fix this by `delete-dups'. + '("." "..") + (delq + nil + (mapcar + (lambda (l) + (and (not (string-match-p (rx bol (* blank) eol) l)) l)) + (split-string (buffer-string) "\n")))))))))))) (defun tramp-adb-handle-file-local-copy (filename) "Like `file-local-copy' for Tramp files." diff --git a/lisp/net/tramp-archive.el b/lisp/net/tramp-archive.el index 97adb36c4af..c2175612fa8 100644 --- a/lisp/net/tramp-archive.el +++ b/lisp/net/tramp-archive.el @@ -650,7 +650,9 @@ offered." (defun tramp-archive-handle-file-name-all-completions (filename directory) "Like `file-name-all-completions' for file archives." - (file-name-all-completions filename (tramp-archive-gvfs-file-name directory))) + (ignore-error file-missing + (file-name-all-completions + filename (tramp-archive-gvfs-file-name directory)))) (defun tramp-archive-handle-file-readable-p (filename) "Like `file-readable-p' for file archives." diff --git a/lisp/net/tramp-crypt.el b/lisp/net/tramp-crypt.el index afd3166d161..d0f1f1b8184 100644 --- a/lisp/net/tramp-crypt.el +++ b/lisp/net/tramp-crypt.el @@ -730,18 +730,19 @@ absolute file names." (defun tramp-crypt-handle-file-name-all-completions (filename directory) "Like `file-name-all-completions' for Tramp files." - (all-completions - filename - (let* (completion-regexp-list - tramp-crypt-enabled - (directory (file-name-as-directory directory)) - (enc-dir (tramp-crypt-encrypt-file-name directory))) - (mapcar - (lambda (x) - (substring - (tramp-crypt-decrypt-file-name (concat enc-dir x)) - (length directory))) - (file-name-all-completions "" enc-dir))))) + (ignore-error file-missing + (all-completions + filename + (let* (completion-regexp-list + tramp-crypt-enabled + (directory (file-name-as-directory directory)) + (enc-dir (tramp-crypt-encrypt-file-name directory))) + (mapcar + (lambda (x) + (substring + (tramp-crypt-decrypt-file-name (concat enc-dir x)) + (length directory))) + (file-name-all-completions "" enc-dir)))))) (defun tramp-crypt-handle-file-readable-p (filename) "Like `file-readable-p' for Tramp files." diff --git a/lisp/net/tramp-fuse.el b/lisp/net/tramp-fuse.el index b846caadc18..8112e564a2c 100644 --- a/lisp/net/tramp-fuse.el +++ b/lisp/net/tramp-fuse.el @@ -98,20 +98,21 @@ (defun tramp-fuse-handle-file-name-all-completions (filename directory) "Like `file-name-all-completions' for Tramp files." (tramp-fuse-remove-hidden-files - (all-completions - filename - (delete-dups - (append - (file-name-all-completions - filename (tramp-fuse-local-file-name directory)) - ;; Some storage systems do not return "." and "..". - (let (result) - (dolist (item '(".." ".") result) - (when (string-prefix-p filename item) - (catch 'match - (dolist (elt completion-regexp-list) - (unless (string-match-p elt item) (throw 'match nil))) - (setq result (cons (concat item "/") result))))))))))) + (ignore-error file-missing + (all-completions + filename + (delete-dups + (append + (file-name-all-completions + filename (tramp-fuse-local-file-name directory)) + ;; Some storage systems do not return "." and "..". + (let (result) + (dolist (item '(".." ".") result) + (when (string-prefix-p filename item) + (catch 'match + (dolist (elt completion-regexp-list) + (unless (string-match-p elt item) (throw 'match nil))) + (setq result (cons (concat item "/") result)))))))))))) ;; This function isn't used. (defun tramp-fuse-handle-insert-directory diff --git a/lisp/net/tramp-gvfs.el b/lisp/net/tramp-gvfs.el index b9639c1e7f7..266724c587f 100644 --- a/lisp/net/tramp-gvfs.el +++ b/lisp/net/tramp-gvfs.el @@ -1418,16 +1418,19 @@ If FILE-SYSTEM is non-nil, return file system attributes." (defun tramp-gvfs-handle-file-name-all-completions (filename directory) "Like `file-name-all-completions' for Tramp files." (unless (tramp-compat-string-search "/" filename) - (all-completions - filename - (with-parsed-tramp-file-name (expand-file-name directory) nil - (with-tramp-file-property v localname "file-name-all-completions" - (let ((result '("./" "../"))) - ;; Get a list of directories and files. - (dolist (item (tramp-gvfs-get-directory-attributes directory) result) - (if (string-equal (cdr (assoc "type" item)) "directory") - (push (file-name-as-directory (car item)) result) - (push (car item) result))))))))) + (ignore-error file-missing + (all-completions + filename + (with-parsed-tramp-file-name (expand-file-name directory) nil + (with-tramp-file-property v localname "file-name-all-completions" + (let ((result '("./" "../"))) + ;; Get a list of directories and files. + (dolist (item + (tramp-gvfs-get-directory-attributes directory) + result) + (if (string-equal (cdr (assoc "type" item)) "directory") + (push (file-name-as-directory (car item)) result) + (push (car item) result)))))))))) (defun tramp-gvfs-handle-file-notify-add-watch (file-name flags _callback) "Like `file-notify-add-watch' for Tramp files." diff --git a/lisp/net/tramp-sh.el b/lisp/net/tramp-sh.el index 3ae5208154a..a854ff42b0d 100644 --- a/lisp/net/tramp-sh.el +++ b/lisp/net/tramp-sh.el @@ -1767,41 +1767,43 @@ ID-FORMAT valid values are `string' and `integer'." (with-parsed-tramp-file-name (expand-file-name directory) nil (when (and (not (tramp-compat-string-search "/" filename)) (tramp-connectable-p v)) - (all-completions - filename - (with-tramp-file-property v localname "file-name-all-completions" - (let (result) - ;; Get a list of directories and files, including reliably - ;; tagging the directories with a trailing "/". Because I - ;; rock. --daniel@danann.net - (when (tramp-send-command-and-check - v - (if (tramp-get-remote-perl v) - (progn - (tramp-maybe-send-script - v tramp-perl-file-name-all-completions - "tramp_perl_file_name_all_completions") - (format "tramp_perl_file_name_all_completions %s" - (tramp-shell-quote-argument localname))) + (unless (tramp-compat-string-search "/" filename) + (ignore-error file-missing + (all-completions + filename + (with-tramp-file-property v localname "file-name-all-completions" + (let (result) + ;; Get a list of directories and files, including + ;; reliably tagging the directories with a trailing "/". + ;; Because I rock. --daniel@danann.net + (when (tramp-send-command-and-check + v + (if (tramp-get-remote-perl v) + (progn + (tramp-maybe-send-script + v tramp-perl-file-name-all-completions + "tramp_perl_file_name_all_completions") + (format "tramp_perl_file_name_all_completions %s" + (tramp-shell-quote-argument localname))) - (format (concat - "cd %s 2>&1 && %s -a 2>%s" - " | while IFS= read f; do" - " if %s -d \"$f\" 2>%s;" - " then \\echo \"$f/\"; else \\echo \"$f\"; fi;" - " done") - (tramp-shell-quote-argument localname) - (tramp-get-ls-command v) - (tramp-get-remote-null-device v) - (tramp-get-test-command v) - (tramp-get-remote-null-device v)))) + (format (concat + "cd %s 2>&1 && %s -a 2>%s" + " | while IFS= read f; do" + " if %s -d \"$f\" 2>%s;" + " then \\echo \"$f/\"; else \\echo \"$f\"; fi;" + " done") + (tramp-shell-quote-argument localname) + (tramp-get-ls-command v) + (tramp-get-remote-null-device v) + (tramp-get-test-command v) + (tramp-get-remote-null-device v)))) - ;; Now grab the output. - (with-current-buffer (tramp-get-buffer v) - (goto-char (point-max)) - (while (zerop (forward-line -1)) - (push (buffer-substring (point) (line-end-position)) result))) - result))))))) + ;; Now grab the output. + (with-current-buffer (tramp-get-buffer v) + (goto-char (point-max)) + (while (zerop (forward-line -1)) + (push (buffer-substring (point) (line-end-position)) result))) + result))))))))) ;; cp, mv and ln diff --git a/lisp/net/tramp-smb.el b/lisp/net/tramp-smb.el index 2a69465224f..1aa4520eeb6 100644 --- a/lisp/net/tramp-smb.el +++ b/lisp/net/tramp-smb.el @@ -976,18 +976,20 @@ PRESERVE-UID-GID and PRESERVE-EXTENDED-ATTRIBUTES are completely ignored." ;; files. (defun tramp-smb-handle-file-name-all-completions (filename directory) "Like `file-name-all-completions' for Tramp files." - (all-completions - filename - (with-parsed-tramp-file-name (expand-file-name directory) nil - (with-tramp-file-property v localname "file-name-all-completions" - (delete-dups - (mapcar - (lambda (x) - (list - (if (tramp-compat-string-search "d" (nth 1 x)) - (file-name-as-directory (nth 0 x)) - (nth 0 x)))) - (tramp-smb-get-file-entries directory))))))) + (ignore-error file-missing + (all-completions + filename + (when (file-directory-p directory) + (with-parsed-tramp-file-name (expand-file-name directory) nil + (with-tramp-file-property v localname "file-name-all-completions" + (delete-dups + (mapcar + (lambda (x) + (list + (if (tramp-compat-string-search "d" (nth 1 x)) + (file-name-as-directory (nth 0 x)) + (nth 0 x)))) + (tramp-smb-get-file-entries directory))))))))) (defun tramp-smb-handle-file-system-info (filename) "Like `file-system-info' for Tramp files." diff --git a/lisp/net/tramp-sudoedit.el b/lisp/net/tramp-sudoedit.el index fa1689d6851..abb9afc570b 100644 --- a/lisp/net/tramp-sudoedit.el +++ b/lisp/net/tramp-sudoedit.el @@ -460,26 +460,27 @@ the result will be a local, non-Tramp, file name." (defun tramp-sudoedit-handle-file-name-all-completions (filename directory) "Like `file-name-all-completions' for Tramp files." - (all-completions - filename - (with-parsed-tramp-file-name (expand-file-name directory) nil - (with-tramp-file-property v localname "file-name-all-completions" - (tramp-sudoedit-send-command - v "ls" "-a1" "--quoting-style=literal" "--show-control-chars" - (if (tramp-string-empty-or-nil-p localname) - "" (file-name-unquote localname))) - (mapcar - (lambda (f) - (if (ignore-errors (file-directory-p (expand-file-name f directory))) - (file-name-as-directory f) - f)) - (delq - nil + (ignore-error file-missing + (all-completions + filename + (with-parsed-tramp-file-name (expand-file-name directory) nil + (with-tramp-file-property v localname "file-name-all-completions" + (tramp-sudoedit-send-command + v "ls" "-a1" "--quoting-style=literal" "--show-control-chars" + (if (tramp-string-empty-or-nil-p localname) + "" (file-name-unquote localname))) (mapcar - (lambda (l) (and (not (string-match-p (rx bol (* blank) eol) l)) l)) - (split-string - (tramp-get-buffer-string (tramp-get-connection-buffer v)) - "\n" 'omit)))))))) + (lambda (f) + (if (ignore-errors (file-directory-p (expand-file-name f directory))) + (file-name-as-directory f) + f)) + (delq + nil + (mapcar + (lambda (l) (and (not (string-match-p (rx bol (* blank) eol) l)) l)) + (split-string + (tramp-get-buffer-string (tramp-get-connection-buffer v)) + "\n" 'omit))))))))) (defun tramp-sudoedit-handle-file-readable-p (filename) "Like `file-readable-p' for Tramp files." From 75f04848a653e70f12f0e5a62b756c5bba0dd204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Engdeg=C3=A5rd?= Date: Sun, 12 Mar 2023 17:00:25 +0100 Subject: [PATCH 6/6] Repair and speed up safe-copy-tree and make it internal (bug#61962) There is no particular requirement for safe-copy-tree so let's make it internal for now. The new implementation is faster and more correct. * doc/lispref/lists.texi (Building Lists): * etc/NEWS: Remove doc and announcement. * lisp/subr.el (safe-copy-tree--seen, safe-copy-tree--1) (safe-copy-tree): Remove old version. * lisp/emacs-lisp/bytecomp.el (bytecomp--copy-tree-seen) (bytecomp--copy-tree-1, bytecomp--copy-tree): Add new version. (byte-compile-initial-macro-environment): Use it. * test/lisp/subr-tests.el (subr--safe-copy-tree): * test/lisp/emacs-lisp/bytecomp-tests.el (bytecomp--copy-tree): Move and improve tests. --- doc/lispref/lists.texi | 13 ------ etc/NEWS | 5 --- lisp/emacs-lisp/bytecomp.el | 38 +++++++++++++++++- lisp/subr.el | 55 -------------------------- test/lisp/emacs-lisp/bytecomp-tests.el | 28 +++++++++++++ test/lisp/subr-tests.el | 26 ------------ 6 files changed, 65 insertions(+), 100 deletions(-) diff --git a/doc/lispref/lists.texi b/doc/lispref/lists.texi index 3478049c84f..a509325854f 100644 --- a/doc/lispref/lists.texi +++ b/doc/lispref/lists.texi @@ -708,19 +708,6 @@ non-@code{nil}, it copies vectors too (and operates recursively on their elements). This function cannot cope with circular lists. @end defun -@defun safe-copy-tree tree &optional vecp -This function returns a copy of the tree @var{tree}. If @var{tree} is -a cons cell, this make a new cons cell with the same @sc{car} and -@sc{cdr}, then recursively copies the @sc{car} and @sc{cdr} in the -same way. - -Normally, when @var{tree} is anything other than a cons cell, -@code{copy-tree} simply returns @var{tree}. However, if @var{vecp} is -non-@code{nil}, it copies vectors and records too (and operates -recursively on their elements). This function handles circular lists -and vectors, and is thus slower than @code{copy-tree} for typical cases. -@end defun - @defun flatten-tree tree This function returns a ``flattened'' copy of @var{tree}, that is, a list containing all the non-@code{nil} terminal nodes, or leaves, of diff --git a/etc/NEWS b/etc/NEWS index e31203689e3..3b02e85b691 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -391,11 +391,6 @@ was to catch all errors, add an explicit handler for 'error', or use This warning can be suppressed using 'with-suppressed-warnings' with the warning name 'suspicious'. -+++ -** New function 'safe-copy-tree' -This function is a version of copy-tree which handles circular lists -and circular vectors/records. - +++ ** New function 'file-user-uid'. This function is like 'user-uid', but is aware of file name handlers, diff --git a/lisp/emacs-lisp/bytecomp.el b/lisp/emacs-lisp/bytecomp.el index 12850c27b88..a122e81ba3c 100644 --- a/lisp/emacs-lisp/bytecomp.el +++ b/lisp/emacs-lisp/bytecomp.el @@ -495,6 +495,42 @@ Return the compile-time value of FORM." (cdr form))) (funcall non-toplevel-case form))) + +(defvar bytecomp--copy-tree-seen) + +(defun bytecomp--copy-tree-1 (tree) + ;; TREE must be a cons. + (or (gethash tree bytecomp--copy-tree-seen) + (let* ((next (cdr tree)) + (result (cons nil next)) + (copy result)) + (while (progn + (puthash tree copy bytecomp--copy-tree-seen) + (let ((a (car tree))) + (setcar copy (if (consp a) + (bytecomp--copy-tree-1 a) + a))) + (and (consp next) + (let ((tail (gethash next bytecomp--copy-tree-seen))) + (if tail + (progn (setcdr copy tail) + nil) + (setq tree next) + (setq next (cdr next)) + (let ((prev copy)) + (setq copy (cons nil next)) + (setcdr prev copy) + t)))))) + result))) + +(defun bytecomp--copy-tree (tree) + "Make a copy of TREE, preserving any circular structure therein. +Only conses are traversed and duplicated, not arrays or any other structure." + (if (consp tree) + (let ((bytecomp--copy-tree-seen (make-hash-table :test #'eq))) + (bytecomp--copy-tree-1 tree)) + tree)) + (defconst byte-compile-initial-macro-environment `( ;; (byte-compiler-options . (lambda (&rest forms) @@ -534,7 +570,7 @@ Return the compile-time value of FORM." form macroexpand-all-environment))) (eval (byte-run-strip-symbol-positions - (safe-copy-tree expanded)) + (bytecomp--copy-tree expanded)) lexical-binding) expanded))))) (with-suppressed-warnings diff --git a/lisp/subr.el b/lisp/subr.el index 40bec544b73..8aedce934d1 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -846,61 +846,6 @@ argument VECP, this copies vectors as well as conses." tree) tree))) -(defvar safe-copy-tree--seen nil - "A hash table for conses/vectors/records already seen by safe-copy-tree-1. -Its key is a cons or vector/record seen by the algorithm, and its -value is the corresponding cons/vector/record in the copy.") - -(defun safe-copy-tree--1 (tree &optional vecp) - "Make a copy of TREE, taking circular structure into account. -If TREE is a cons cell, this recursively copies both its car and its cdr. -Contrast to `copy-sequence', which copies only along the cdrs. With second -argument VECP, this copies vectors and records as well as conses." - (cond - ((gethash tree safe-copy-tree--seen)) - ((consp tree) - (let* ((result (cons (car tree) (cdr tree))) - (newcons result) - hash) - (while (and (not hash) (consp tree)) - (if (setq hash (gethash tree safe-copy-tree--seen)) - (setq newcons hash) - (puthash tree newcons safe-copy-tree--seen)) - (setq tree newcons) - (unless hash - (if (or (consp (car tree)) - (and vecp (or (vectorp (car tree)) (recordp (car tree))))) - (let ((newcar (safe-copy-tree--1 (car tree) vecp))) - (setcar tree newcar))) - (setq newcons (if (consp (cdr tree)) - (cons (cadr tree) (cddr tree)) - (cdr tree))) - (setcdr tree newcons) - (setq tree (cdr tree)))) - (nconc result - (if (and vecp (or (vectorp tree) (recordp tree))) - (safe-copy-tree--1 tree vecp) tree)))) - ((and vecp (or (vectorp tree) (recordp tree))) - (let* ((newvec (copy-sequence tree)) - (i (length newvec))) - (puthash tree newvec safe-copy-tree--seen) - (setq tree newvec) - (while (>= (setq i (1- i)) 0) - (aset tree i (safe-copy-tree--1 (aref tree i) vecp))) - tree)) - (t tree))) - -(defun safe-copy-tree (tree &optional vecp) - "Make a copy of TREE, taking circular structure into account. -If TREE is a cons cell, this recursively copies both its car and its cdr. -Contrast to `copy-sequence', which copies only along the cdrs. With second -argument VECP, this copies vectors and records as well as conses." - (setq safe-copy-tree--seen (make-hash-table :test #'eq)) - (unwind-protect - (safe-copy-tree--1 tree vecp) - (clrhash safe-copy-tree--seen) - (setq safe-copy-tree--seen nil))) - ;;;; Various list-search functions. diff --git a/test/lisp/emacs-lisp/bytecomp-tests.el b/test/lisp/emacs-lisp/bytecomp-tests.el index 10b009a261c..2cd4dd75742 100644 --- a/test/lisp/emacs-lisp/bytecomp-tests.el +++ b/test/lisp/emacs-lisp/bytecomp-tests.el @@ -1850,6 +1850,34 @@ EXPECTED-POINT BINDINGS (MODES \\='\\='(ruby-mode js-mode python-mode)) \ (should (eq (byte-compile-file src-file) 'no-byte-compile)) (should-not (file-exists-p dest-file)))) +(ert-deftest bytecomp--copy-tree () + (should (null (bytecomp--copy-tree nil))) + (let ((print-circle t)) + (let* ((x '(1 2 (3 4))) + (y (bytecomp--copy-tree x))) + (should (equal (prin1-to-string (list x y)) + "((1 2 (3 4)) (1 2 (3 4)))"))) + (let* ((x '#1=(a #1#)) + (y (bytecomp--copy-tree x))) + (should (equal (prin1-to-string (list x y)) + "(#1=(a #1#) #2=(a #2#))"))) + (let* ((x '#1=(#1# a)) + (y (bytecomp--copy-tree x))) + (should (equal (prin1-to-string (list x y)) + "(#1=(#1# a) #2=(#2# a))"))) + (let* ((x '((a . #1=(b)) #1#)) + (y (bytecomp--copy-tree x))) + (should (equal (prin1-to-string (list x y)) + "(((a . #1=(b)) #1#) ((a . #2=(b)) #2#))"))) + (let* ((x '#1=(a #2=(#1# b . #3=(#2# c . #1#)) (#3# d))) + (y (bytecomp--copy-tree x))) + (should (equal (prin1-to-string (list x y)) + (concat + "(" + "#1=(a #2=(#1# b . #3=(#2# c . #1#)) (#3# d))" + " " + "#4=(a #5=(#4# b . #6=(#5# c . #4#)) (#6# d))" + ")")))))) ;; Local Variables: ;; no-byte-compile: t diff --git a/test/lisp/subr-tests.el b/test/lisp/subr-tests.el index 37fe09c1716..050ee22ac18 100644 --- a/test/lisp/subr-tests.el +++ b/test/lisp/subr-tests.el @@ -1205,31 +1205,5 @@ final or penultimate step during initialization.")) (should (equal a-dedup '("a" "b" "a" "b" "c"))) (should (eq a a-dedup)))) -(ert-deftest subr--safe-copy-tree () - (should (null (safe-copy-tree nil))) - (let* ((foo '(1 2 (3 4))) (bar (safe-copy-tree foo))) - (should (equal bar foo)) - (should-not (eq bar foo)) - (should-not (eq (caddr bar) (caddr foo)))) - (let* ((foo '#1=(a #1#)) (bar (safe-copy-tree foo))) - (should (eq (car bar) (car foo))) -; (should-not (proper-list-p bar)) - (should (eq (caadr bar) (caadr foo))) - (should (eq (caadr bar) 'a))) - (let* ((foo [1 2 3 4]) (bar (safe-copy-tree foo))) - (should (eq bar foo))) - (let* ((foo [1 (2 3) 4]) (bar (safe-copy-tree foo t))) - (should-not (eq bar foo)) - (should (equal bar foo)) - (should-not (eq (aref bar 1) (aref foo 1)))) - (let* ((foo [1 [2 3] 4]) (bar (safe-copy-tree foo t))) - (should (equal bar foo)) - (should-not (eq bar foo)) - (should-not (eq (aref bar 1) (aref foo 1)))) - (let* ((foo (record 'foo 1 "two" 3)) (bar (safe-copy-tree foo t))) - (should (equal bar foo)) - (should-not (eq bar foo)) - (should (eq (aref bar 2) (aref foo 2))))) - (provide 'subr-tests) ;;; subr-tests.el ends here