From 2d85f51cd4919f560badda618fff2becc33d3ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Mo=CC=88ding?= Date: Sun, 7 Mar 2021 19:41:13 +0100 Subject: [PATCH] Add skeletons for types & statements (closes #31) These skeletons insert common types and statements using mnemonic keybinding. The "C-c C-t" prefix is used for types and the "C-c C-k" prefix is used for keywords. Some of the statement skeletons ("if", "elsif", "else" & "unless") wrap around the region if it is defined. Turning an existing block of code into a conditional block of code is now an easy task with braces and indentation handled automatically. --- README.md | 48 +++-- puppet-mode.el | 208 +++++++++++++++++++ test/puppet-mode-test.el | 428 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 670 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5468e7a..c425c15 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Features 4. Manual validation and linting of manifests (see [Flycheck][] for on-the-fly validation and linting) 5. Integration with [Puppet Debugger][] +6. Skeletons for many standard Puppet statements and resource declarations Installation ------------ @@ -48,20 +49,39 @@ manifests with the extension `.pp`. The following key bindings are available in Puppet Mode: -Key | Command --------------------|-------------------------------------------------- -C-M-a | Move to the beginning of the current block -C-M-e | Move to the end of the current block -C-c C-a | Align parameters in the current block -C-c C-' | Toggle string quoting between single and double -C-c C-; | Blank the string at point -C-c C-j | Jump to a `class`, `define`, variable or resource -C-c C-c | Apply the current manifest in dry-run mode -C-c C-v | Validate the syntax of the current manifest -C-c C-l | Check the current manifest for semantic issues -C-c C-z | Launch a puppet-debugger REPL -C-c C-r | Send the currently marked region to the REPL -C-c C-b | Send the current buffer to the REPL +Key | Command +---------------------|-------------------------------------------------- +C-M-a | Move to the beginning of the current block +C-M-e | Move to the end of the current block +C-c C-a | Align parameters in the current block +C-c C-' | Toggle string quoting between single and double +C-c C-; | Blank the string at point +C-c C-j | Jump to a `class`, `define`, variable or resource +C-c C-c | Apply the current manifest in dry-run mode +C-c C-v | Validate the syntax of the current manifest +C-c C-l | Check the current manifest for semantic issues +C-c C-z | Launch a puppet-debugger REPL +C-c C-r | Send the currently marked region to the REPL +C-c C-b | Send the current buffer to the REPL +C-c C-k c | Insert `class` definition skeleton +C-c C-k d | Insert `define` definition skeleton +C-c C-k n | Insert `node` definition skeleton +C-c C-k i | Insert `if` statement skeleton +C-c C-k e | Insert `elsif` statement skeleton +C-c C-k o | Insert `else` statement skeleton +C-c C-k u | Insert `unless` statement skeleton +C-c C-k s | Insert `case` statement skeleton +C-c C-k ? | Insert `selector` statement skeleton +C-c C-t a | Insert `anchor` resource skeleton +C-c C-t c | Insert `class` resource skeleton +C-c C-t e | Insert `exec` resource skeleton +C-c C-t f | Insert `file` resource skeleton +C-c C-t g | Insert `group` resource skeleton +C-c C-t h | Insert `host` resource skeleton +C-c C-t n | Insert `notify` resource skeleton +C-c C-t p | Insert `package` resource skeleton +C-c C-t s | Insert `service` resource skeleton +C-c C-t u | Insert `user` resource skeleton For the integration with puppet-debugger to work, the puppet-debugger gem needs diff --git a/puppet-mode.el b/puppet-mode.el index a7b503b..ffb69b5 100644 --- a/puppet-mode.el +++ b/puppet-mode.el @@ -105,6 +105,8 @@ buffer-local wherever it is set." (declare-function pkg-info-version-info "pkg-info" (library)) (eval-when-compile + (require 'cl-macs) + (require 'skeleton) (require 'rx)) (require 'align) @@ -1127,6 +1129,189 @@ With a prefix argument SUPPRESS it simply inserts $." (delete-region (+ min 1) (- max 1))))) + +;;; Skeletons + +(defun puppet-dissect-filename (file) + "Return list of path components for the Puppet manifest FILE. +The first element of the list will be the module name and the +remaining elements are the relative path components below the +‘manifests’ subdirectory. The names of the path components are +only derived from the file name by using the Puppet auto-loader +rules. FILE must be an absolute file name. + +The module name \"unidentified\" is returned if a module name +can't be inferred from the file name. + +If the directory name contains characters that are not legal for +a Puppet module name, then all leading characters including the +last illegal character are removed from the module name. The +function will for example return ‘foo’ as module name even if the +module is using the ‘puppet-foo’ directory (e.g. for module +development in a user's home directory)." + (if (stringp file) + (let* ((parts (cl-loop for path = file then (directory-file-name + (file-name-directory path)) + ;; stop iteration at the root of the directory + ;; tree (should work for Windows & Unix/Linux) + until (or (string-suffix-p ":" path) + (string-equal (file-name-directory path) + path)) + collect (file-name-base path))) + ;; Remove "init" if it is the first element + (compact (if (string-equal (car parts) "init") + (cdr parts) + parts))) + (cons + ;; module name with illegal prefixes removed or "unidentified" if + ;; path is not compliant with the standard Puppet file hierarchy + (replace-regexp-in-string + "^.*[^a-z0-9_]" "" (or (cadr (member "manifests" parts)) + "unidentified")) + ;; remaining path components + (cdr (member "manifests" (reverse compact))))) + '("unidentified"))) + +(defun puppet-file-module-name (file) + "Return the module name for the Puppet class in FILE." + (car (puppet-dissect-filename file))) + +(defun puppet-file-class-name (file) + "Return the class name for the Puppet class in FILE." + (mapconcat #'identity (puppet-dissect-filename file) "::")) + +(define-skeleton puppet-keyword-class + "Insert \"class\" skeleton." + nil + "class " (puppet-file-class-name (buffer-file-name)) " (" > \n + ") {" > \n + > _ "}" > \n) + +(define-skeleton puppet-keyword-define + "Insert \"class\" skeleton." + nil + "define " (puppet-file-class-name (buffer-file-name)) " (" > \n + ") {" > \n + > _ "}" > \n) + +(define-skeleton puppet-keyword-node + "Insert \"node\" skeleton." + nil + "node " > - " {" \n + > _ "}" > \n) + +(define-skeleton puppet-keyword-if + "Insert \"if\" statement." + nil + "if " > - " {" \n + > _ "}" > \n) + +(define-skeleton puppet-keyword-elsif + "Insert \"elsif\" statement." + nil + "elsif " > - " {" \n + > _ "}" > \n) + +(define-skeleton puppet-keyword-else + "Insert \"else\" statement." + nil + "else {" > \n + > _ "}" > \n) + +(define-skeleton puppet-keyword-unless + "Insert \"unless\" statement." + nil + "unless " > - " {" \n + > _ "}" > \n) + +(define-skeleton puppet-keyword-case + "Insert \"case\" statement." + nil + "case " > - " {" \n + "default: {" > \n + "}" > \n + "}" > \n) + +(define-skeleton puppet-keyword-selector + "Insert \"?\" selector." + nil + "? {" > \n + "default => " > - "," \n + "}" > \n) + +(define-skeleton puppet-type-anchor + "Insert the \"anchor\" resource type." + nil + "anchor { " > - ": }" \n) + +(define-skeleton puppet-type-class + "Insert the \"class\" resource type." + nil + "class { " > - ":" \n + "}" > \n) + +(define-skeleton puppet-type-exec + "Insert the \"exec\" resource type." + nil + "exec { " > - ":" \n + "path => [ '/bin', '/sbin', '/usr/bin', '/usr/sbin', ]," > \n + "user => 'root'," > \n + "cwd => '/'," > \n + "}" > \n) + +(define-skeleton puppet-type-file + "Insert the \"file\" resource type." + nil + "file { " > - ":" \n + "ensure => file," > \n + "owner => 'root'," > \n + "group => 'root'," > \n + "mode => '0644'," > \n + "}" > \n) + +(define-skeleton puppet-type-group + "Insert the \"group\" resource type." + nil + "group { " > - ":" \n + "ensure => present," > \n + "}" > \n) + +(define-skeleton puppet-type-host + "Insert the \"host\" resource type." + nil + "host { " > - ":" \n + "ensure => present," > \n + "}" > \n) + +(define-skeleton puppet-type-notify + "Insert the \"notify\" resource type." + nil + "notify { " > - ": }" \n) + +(define-skeleton puppet-type-package + "Insert the \"package\" resource type." + nil + "package { " > - ":" \n + "ensure => present," > \n + "}" > \n) + +(define-skeleton puppet-type-service + "Insert the \"service\" resource type." + nil + "service { " > - ":" \n + "ensure => running," > \n + "enable => true," > \n + "}" > \n) + +(define-skeleton puppet-type-user + "Insert the \"user\" resource type." + nil + "user { " > - ":" \n + "ensure => present," > \n + "shell => '/bin/bash'," > \n + "password => '*'," > \n + "}" > \n) + ;;; Imenu @@ -1220,6 +1405,27 @@ for each entry." ;; Linting and validation (define-key map (kbd "C-c C-v") #'puppet-validate) (define-key map (kbd "C-c C-l") #'puppet-lint) + ;; Skeletons for types + (define-key map (kbd "C-c C-t a") #'puppet-type-anchor) + (define-key map (kbd "C-c C-t c") #'puppet-type-class) + (define-key map (kbd "C-c C-t e") #'puppet-type-exec) + (define-key map (kbd "C-c C-t f") #'puppet-type-file) + (define-key map (kbd "C-c C-t g") #'puppet-type-group) + (define-key map (kbd "C-c C-t h") #'puppet-type-host) + (define-key map (kbd "C-c C-t n") #'puppet-type-notify) + (define-key map (kbd "C-c C-t p") #'puppet-type-package) + (define-key map (kbd "C-c C-t s") #'puppet-type-service) + (define-key map (kbd "C-c C-t u") #'puppet-type-user) + ;; Skeletons for keywords + (define-key map (kbd "C-c C-k c") #'puppet-keyword-class) + (define-key map (kbd "C-c C-k d") #'puppet-keyword-define) + (define-key map (kbd "C-c C-k n") #'puppet-keyword-node) + (define-key map (kbd "C-c C-k i") #'puppet-keyword-if) + (define-key map (kbd "C-c C-k e") #'puppet-keyword-elsif) + (define-key map (kbd "C-c C-k o") #'puppet-keyword-else) + (define-key map (kbd "C-c C-k u") #'puppet-keyword-unless) + (define-key map (kbd "C-c C-k s") #'puppet-keyword-case) + (define-key map (kbd "C-c C-k ?") #'puppet-keyword-selector) ;; The menu bar (easy-menu-define puppet-menu map "Puppet Mode menu" `("Puppet" @@ -1278,6 +1484,8 @@ for each entry." ;; Alignment (setq align-mode-rules-list puppet-mode-align-rules) (setq align-mode-exclude-rules-list puppet-mode-align-exclude-rules) + ;; Skeletons + (setq-local skeleton-end-newline nil) ;; IMenu (setq imenu-create-index-function #'puppet-imenu-create-index)) diff --git a/test/puppet-mode-test.el b/test/puppet-mode-test.el index b3e9875..5d30a90 100644 --- a/test/puppet-mode-test.el +++ b/test/puppet-mode-test.el @@ -615,6 +615,434 @@ class foo { } }")))) + +;;;; Skeletons + +(ert-deftest puppet-skeleton/puppet-dissect-filename-unix () + :tags '(skeleton) + (should (equal (puppet-dissect-filename "/modules/foo/manifests/init.pp") + '("foo"))) + (should (equal (puppet-dissect-filename "/modules/foo/manifests/bar.pp") + '("foo" "bar"))) + (should (equal (puppet-dissect-filename "/modules/foo/manifests/bar/baz.pp") + '("foo" "bar" "baz")))) + +(ert-deftest puppet-skeleton/puppet-dissect-filename-windows () + :tags '(skeleton) + (should (equal (puppet-dissect-filename "C:/modules/foo/manifests/init.pp") + '("foo"))) + (should (equal (puppet-dissect-filename "C:/modules/foo/manifests/bar.pp") + '("foo" "bar"))) + (should (equal (puppet-dissect-filename "C:/modules/foo/manifests/bar/baz.pp") + '("foo" "bar" "baz")))) + +(ert-deftest puppet-skeleton/puppet-dissect-filename-unidentified () + :tags '(skeleton) + (should (equal (puppet-dissect-filename "/modules/init.pp") + '("unidentified"))) + (should (equal (puppet-dissect-filename "/modules/foo/init.pp") + '("unidentified"))) + (should (equal (puppet-dissect-filename "/modules/foo/bar/init.pp") + '("unidentified")))) + +(ert-deftest puppet-skeleton/puppet-dissect-filename-nil () + :tags '(skeleton) + (should (equal (puppet-dissect-filename nil) + '("unidentified")))) + +(ert-deftest puppet-skeleton/puppet-dissect-filename-invalid-module () + :tags '(skeleton) + (should (equal (puppet-dissect-filename "/puppet-foo/manifests/init.pp") + '("foo")))) + +(ert-deftest puppet-skeleton/keyword-class () + :tags '(skeleton) + (puppet-test-with-temp-buffer + "" + (set-visited-file-name "/modules/foo/manifests/bar.pp" t) + (puppet-keyword-class) + (should (string= (buffer-string) "class foo::bar ( +) { +}")))) + +(ert-deftest puppet-skeleton/keyword-define () + :tags '(skeleton) + (puppet-test-with-temp-buffer + "" + (set-visited-file-name "/modules/foo/manifests/bar.pp" t) + (puppet-keyword-define) + (should (string= (buffer-string) "define foo::bar ( +) { +}")))) + +(ert-deftest puppet-skeleton/keyword-node () + :tags '(skeleton) + (puppet-test-with-temp-buffer + "" + (puppet-keyword-node) + (should (looking-at " {")) ; node name + (should (string= (buffer-string) "node { +}")))) + +(ert-deftest puppet-skeleton/keyword-if-no-region () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-keyword-if) + (should (looking-at " {")) ; condition + (should (string= (buffer-string) " +class foo { + if { + } +}")))) + +(ert-deftest puppet-skeleton/keyword-if-region () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { + notify { 'foo': } + notify { 'bar': } +}" + (goto-char (point-min)) + (forward-line 2) + (push-mark (line-beginning-position 2) t) + (activate-mark) + (puppet-keyword-if) + (should (looking-at " {")) ; condition + (should (string= (buffer-string) " +class foo { + if { + notify { 'foo': } + } + notify { 'bar': } +}")))) + +(ert-deftest puppet-skeleton/keyword-elsif-no-region () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-keyword-elsif) + (should (looking-at " {")) ; condition + (should (string= (buffer-string) " +class foo { + elsif { + } +}")))) + +(ert-deftest puppet-skeleton/keyword-elsif-region () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { + notify { 'foo': } + notify { 'bar': } +}" + (goto-char (point-min)) + (forward-line 2) + (push-mark (line-beginning-position 2) t) + (activate-mark) + (puppet-keyword-elsif) + (should (looking-at " {")) ; condition + (should (string= (buffer-string) " +class foo { + elsif { + notify { 'foo': } + } + notify { 'bar': } +}")))) + +(ert-deftest puppet-skeleton/keyword-else-no-region () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-keyword-else) + (should (string= (buffer-string) " +class foo { + else { + } +}")))) + +(ert-deftest puppet-skeleton/keyword-else-region () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { + notify { 'foo': } + notify { 'bar': } +}" + (goto-char (point-min)) + (forward-line 2) + (push-mark (line-beginning-position 2) t) + (activate-mark) + (puppet-keyword-else) + (should (string= (buffer-string) " +class foo { + else { + notify { 'foo': } + } + notify { 'bar': } +}")))) + +(ert-deftest puppet-skeleton/keyword-unless-no-region () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-keyword-unless) + (should (looking-at " {")) ; condition + (should (string= (buffer-string) " +class foo { + unless { + } +}")))) + +(ert-deftest puppet-skeleton/keyword-unless-region () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { + notify { 'foo': } + notify { 'bar': } +}" + (goto-char (point-min)) + (forward-line 2) + (push-mark (line-beginning-position 2) t) + (activate-mark) + (puppet-keyword-unless) + (should (looking-at " {")) ; condition + (should (string= (buffer-string) " +class foo { + unless { + notify { 'foo': } + } + notify { 'bar': } +}")))) + +(ert-deftest puppet-skeleton/keyword-case () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-keyword-case) + (should (looking-at " {")) ; expression + (should (string= (buffer-string) " +class foo { + case { + default: { + } + } +}")))) + +(ert-deftest puppet-skeleton/keyword-selector () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (insert " $foo ") + (puppet-keyword-selector) + (should (looking-at ",")) ; value + (should (string= (buffer-string) " +class foo { + $foo ? { + default => , + } +}")))) + +(ert-deftest puppet-skeleton/type-anchor () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-anchor) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + anchor { : } +}")))) + +(ert-deftest puppet-skeleton/type-class () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-class) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + class { : + } +}")))) + +(ert-deftest puppet-skeleton/type-exec () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-exec) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + exec { : + path => [ '/bin', '/sbin', '/usr/bin', '/usr/sbin', ], + user => 'root', + cwd => '/', + } +}")))) + +(ert-deftest puppet-skeleton/type-file () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-file) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + file { : + ensure => file, + owner => 'root', + group => 'root', + mode => '0644', + } +}")))) + +(ert-deftest puppet-skeleton/type-group () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-group) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + group { : + ensure => present, + } +}")))) + +(ert-deftest puppet-skeleton/type-host () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-host) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + host { : + ensure => present, + } +}")))) + +(ert-deftest puppet-skeleton/type-notify () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-notify) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + notify { : } +}")))) + +(ert-deftest puppet-skeleton/type-package () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-package) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + package { : + ensure => present, + } +}")))) + +(ert-deftest puppet-skeleton/type-service () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-service) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + service { : + ensure => running, + enable => true, + } +}")))) + +(ert-deftest puppet-skeleton/type-user () + :tags '(skeleton) + (puppet-test-with-temp-buffer + " +class foo { +}" + (goto-char (point-min)) + (forward-line 2) + (puppet-type-user) + (should (looking-at ":")) ; title + (should (string= (buffer-string) " +class foo { + user { : + ensure => present, + shell => '/bin/bash', + password => '*', + } +}")))) + ;;;; Imenu