;;; alchemist-scope.el --- Provides information about Elixir source code context -*- lexical-binding: t -*- ;; Copyright © 2015 Samuel Tonini ;; Author: Samuel Tonini . ;;; Commentary: ;; Provides information about the Elixir source code context. ;;; Code: (require 'dash) (defgroup alchemist-scope nil "Provides information about the Elixir source code context." :prefix "alchemist-scope-" :group 'alchemist) (defconst alchemist-scope-defmodule-regex "defmodule \\([A-Za-z\._]+\\)\s+" "The regex for matching Elixir defmodule macro.") (defconst alchemist-scope-alias-regex "^\s+alias\s+\\([-:_A-Za-z0-9,\.\?!\]+\\)\\(\s*,\s*as:\s*\\)?\\([-_A-Za-z0-9,\.\?!\]+\\)?\n" "The regex for matching Elixir alias definitions. Example: alias Phoenix.Router.Resource, as: Special") (defconst alchemist-scope-alias-regex-two "^\s+alias\s+\\([-:_A-Za-z0-9,\.\?!\]+\\)\.{\\([-:_A-Za-z0-9\s,\.\?!\]+\\)}\n" "The regex for matching Elixir alias definitions. Example: alias List.Chars.{Atom, Float}") (defconst alchemist-scope-use-regex "^\s+use\s+\\([A-Za-z0-9\.]+\\)" "The regex for matching Elixir use definitions.") (defconst alchemist-scope-import-regex "^\s+import\s+\\([A-Za-z0-9\.]+\\)" "The regex for matching Elixir import definitions.") (defun alchemist-scope-inside-string-p () "Return non-nil if `point' is inside a string or heredoc." (let* ((pos (point)) (parse-info (syntax-ppss pos))) (or (and (nth 3 parse-info) (nth 8 parse-info)) (and (looking-at "\"\"\"\\|'''\\|\"\\|\'") (match-beginning 0))))) (defun alchemist-scope-inside-module-p () "Return non-nil if `point' is currently inside a module." (save-excursion (end-of-line) (let ((found-flag-p nil) (found-p nil)) (while (and (not found-flag-p) (re-search-backward alchemist-scope-defmodule-regex nil t)) (when (not (alchemist-scope-inside-string-p)) (setq found-flag-p t) (setq found-p t))) found-p))) (defun alchemist-scope-module () "Return name from the current defmodule." (save-excursion (let ((found-flag-p nil) (module-name "")) (save-match-data (while (and (not found-flag-p) (re-search-backward alchemist-scope-defmodule-regex nil t)) (when (not (alchemist-scope-inside-string-p)) (setq module-name (match-string 1)) (setq found-flag-p t)) (when (equal 1 (line-number-at-pos (point))) (setq found-flag-p t))) module-name)))) (defun alchemist-scope-aliases () "Return aliases from the current module." (let* ((aliases '()) (context (alchemist-scope-module))) (save-excursion (when (alchemist-scope-inside-module-p) (end-of-line) ;; alias definition like: ;; ;; alias Phoenix.Router.Resource, as: Special (while (re-search-backward alchemist-scope-alias-regex nil t) (when (and (not (alchemist-scope-inside-string-p)) (equal context (alchemist-scope-module))) (let* ((alias (match-string 1)) (as (if (match-string 3) (match-string 3) nil)) (as (if as as (car (last (split-string alias "\\.")))))) (setq aliases (append aliases (list (list (alchemist-utils-remove-dot-at-the-end alias) (alchemist-utils-remove-dot-at-the-end as)))))))) ;; alias definition like: ;; ;; alias List.Chars.{Atom, Float} (while (re-search-backward alchemist-scope-alias-regex-two nil t) (when (and (not (alchemist-scope-inside-string-p)) (equal context (alchemist-scope-module))) (let* ((prefix (match-string 1)) (alias-collection (if (match-string 2) (split-string (match-string 2) ",") nil))) (-map (lambda (alias) (let* ((alias (replace-regexp-in-string "\s+" "" alias)) (namespace (format "%s.%s" prefix alias))) (setq aliases (append aliases (list (list (alchemist-utils-remove-dot-at-the-end namespace) (alchemist-utils-remove-dot-at-the-end alias))))))) alias-collection)))))) aliases)) (defun alchemist-scope--modules (regex) (let ((modules '()) (context (alchemist-scope-module))) (save-excursion (when (not (alchemist-utils-empty-string-p context)) (while (re-search-backward regex nil t) (when (and (match-string 1) (not (alchemist-scope-inside-string-p)) (equal context (alchemist-scope-module))) (cl-pushnew (substring-no-properties (match-string 1)) modules)))) modules))) (defun alchemist-scope-use-modules () "Return `use' introduced module names from the current module." (alchemist-scope--modules alchemist-scope-use-regex)) (defun alchemist-scope-import-modules () "Return `import' introduced module names from the current module." (alchemist-scope--modules alchemist-scope-import-regex)) (defun alchemist-scope-all-modules () "Return `use' and `import' introduced modules from the current module." (let ((current (alchemist-scope-module)) (use (alchemist-scope-use-modules)) (import (alchemist-scope-import-modules)) (modules '())) (push current modules) (push use modules) (push import modules) (-flatten modules))) (defun alchemist-scope-extract-module (expr) "Extract module from EXPR." (let* ((parts (split-string expr "\\.")) (function (car (last parts))) (case-fold-search nil)) (when (string-match-p "^[a-z_\?!]+" function) (delete function parts)) (unless (string-match-p "^[a-z_\?!]+" (car parts)) (alchemist-utils-remove-dot-at-the-end (mapconcat 'concat parts "."))))) (defun alchemist-scope-extract-function (expr) "Extract function from EXPR." (let* ((parts (split-string expr "\\.")) (function (car (last parts))) (case-fold-search nil)) (when (and function (string-match-p "^[a-z_\?!]+" function)) function))) (defun alchemist-scope-alias-full-path (module) "Solve the full path for the MODULE alias." (if (not (alchemist-utils-empty-string-p module)) (let* ((aliases (-map (lambda (m) (when (string-match-p (format "^%s" (car (cdr m))) module) (replace-regexp-in-string (format "^%s" (car (cdr m))) (car m) module t))) (alchemist-scope-aliases))) (aliases (delete nil aliases))) (if aliases (car aliases) module)))) (defun alchemist-scope-expression () "Return the expression under the cursor." (let (p1 p2) (save-excursion (skip-chars-backward "-_A-Za-z0-9.?!:@") (setq p1 (point)) (skip-chars-forward "-_A-Za-z0-9.?!:@") (setq p2 (point)) (buffer-substring-no-properties p1 p2)))) (provide 'alchemist-scope) ;;; alchemist-scope.el ends here