diff --git a/README.md b/README.md
index 5468e7a..7e1c0bf 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,11 @@ Features
1. Syntax highlighting
2. Indentation and alignment of expressions and statements
3. Tag navigation (aka `imenu`)
-4. Manual validation and linting of manifests (see [Flycheck][] for on-the-fly
+4. Cross-reference navigation (aka `xref`) to classes, defined types, data
+ types or functions defined in other modules
+5. Manual validation and linting of manifests (see [Flycheck][] for on-the-fly
validation and linting)
-5. Integration with [Puppet Debugger][]
+6. Integration with [Puppet Debugger][]
Installation
------------
@@ -62,6 +64,8 @@ Key | Command
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
+M-. | Jump to the resource definition at point
+M-, | Jump back after visiting a resource definition
For the integration with puppet-debugger to work, the puppet-debugger gem needs
diff --git a/puppet-mode.el b/puppet-mode.el
index 6261f8e..ca3383a 100644
--- a/puppet-mode.el
+++ b/puppet-mode.el
@@ -201,6 +201,14 @@ buffer-local wherever it is set."
:group 'puppet
:package-version '(puppet-mode . "0.4"))
+(defcustom puppet-module-path
+ '("/etc/puppetlabs/code/environments/production/modules")
+ "Paths to search for modules when resolving cross references.
+Remote directories as defined by TRAMP are possible but very slow
+when accessed."
+ :group 'puppet
+ :type '(repeat directory))
+
;;; Version information
(defun puppet-version (&optional show-version)
@@ -1122,6 +1130,105 @@ With a prefix argument SUPPRESS it simply inserts $."
(delete-region (+ min 1) (- max 1)))))
+
+;;; Xref
+
+(defun puppet-module-root (file)
+ "Return the Puppet module root directory for FILE.
+Walk up the directory tree until a directory is found, that
+either contains a \"manifests\", \"lib\" or \"types\" subdir.
+Return the directory name or nil if no directory is found."
+ (locate-dominating-file
+ file
+ (lambda (path)
+ (and (file-accessible-directory-p path)
+ (or (file-readable-p (expand-file-name "manifests" path))
+ (file-readable-p (expand-file-name "lib" path))
+ (file-readable-p (expand-file-name "types" path)))))))
+
+(defun puppet-autoload-path (identifier &optional directory extension)
+ "Resolve IDENTIFIER into Puppet module and relative autoload path.
+Use DIRECTORY as module subdirectory (defaults to \"manifests\"
+and EXTENSION as file extension (defaults to \".pp\") when
+building the path. Return a cons cell where the first part is
+the module name and the second part is a relative path name below
+that module where the identifier should be defined according to
+the Puppet autoload rules."
+ (let* ((components (split-string identifier "::"))
+ (module (car components))
+ (path (cons (or directory "manifests")
+ (butlast (cdr components))))
+ (file (if (cdr components)
+ (car (last components))
+ "init")))
+ (cons module
+ (concat (mapconcat #'file-name-as-directory path "")
+ file
+ (or extension ".pp")))))
+
+(defun puppet--xref-backend ()
+ "The Xref backend for `puppet-mode'."
+ 'puppet)
+
+(cl-defmethod xref-backend-identifier-at-point ((_backend (eql puppet)))
+ "Return the Puppet identifier at point."
+ (let ((thing (thing-at-point 'symbol)))
+ (and thing (substring-no-properties thing))))
+
+(cl-defmethod xref-backend-definitions ((_backend (eql puppet)) identifier)
+ "Find the definitions of a Puppet resource IDENTIFIER.
+First the location of the visited file is checked. Then all
+directories from `puppet-module-path' are searched for the module
+and the file according to Puppet's autoloading rules."
+ (let* ((resource (downcase (if (string-prefix-p "::" identifier)
+ (substring identifier 2)
+ identifier)))
+ (pupfiles (puppet-autoload-path resource))
+ (typfiles (puppet-autoload-path resource "types"))
+ (funfiles (puppet-autoload-path resource "functions"))
+ (xrefs '()))
+ (if pupfiles
+ (let* ((module (car pupfiles))
+ ;; merged list of relative path names to classes/defines/types
+ (pathlist (mapcar #'cdr (list pupfiles typfiles funfiles)))
+ ;; list of directories where this module might be
+ (moddirs (mapcar (lambda (dir) (expand-file-name module dir))
+ puppet-module-path))
+ ;; the regexp to find the resource definition in the file
+ (resdef (concat "^\\(class\\|define\\|type\\|function\\)\\s-+"
+ resource
+ "\\((\\|{\\|\\s-\\|$\\)"))
+ ;; files to visit when searching for the resource
+ (files '()))
+ ;; Check the current module directory (if the buffer actually visits
+ ;; a file) and all module subdirectories from `puppet-module-path'.
+ (dolist (dir (if buffer-file-name
+ (cons (puppet-module-root buffer-file-name) moddirs)
+ moddirs))
+ ;; Try all relative path names below the module directory that
+ ;; might contain the resource; save the file name if the file
+ ;; exists and we haven't seen it (we might try to check a file
+ ;; twice if the current module is also below one of the dirs in
+ ;; `puppet-module-path').
+ (dolist (path pathlist)
+ (let ((file (expand-file-name path dir)))
+ (if (and (not (member file files))
+ (file-readable-p file))
+ (setq files (cons file files))))))
+ ;; Visit all found files to finally locate the resource definition
+ (dolist (file files)
+ (with-temp-buffer
+ (insert-file-contents-literally file)
+ (save-match-data
+ (when (re-search-forward resdef nil t)
+ (push (xref-make
+ (match-string-no-properties 0)
+ (xref-make-file-location
+ file (line-number-at-pos (match-beginning 1)) 0))
+ xrefs)))))))
+ xrefs))
+
+
;;; Imenu
@@ -1273,6 +1380,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)
+ ;; Xref
+ (add-hook 'xref-backend-functions #'puppet--xref-backend)
;; IMenu
(setq imenu-create-index-function #'puppet-imenu-create-index))