Skip to content

use fallback for magic requires when there's no middlware #559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Introduce `cljr-magic-requires-fallback` defcustom to fallback magic requires to `cljr-magic-require-namespaces` if the middlware is not available.

## 3.12.0

- `clean-ns` will now also reformat the `ns` form for whitespace-only changes, if needed.
Expand Down
169 changes: 105 additions & 64 deletions clj-refactor.el
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,45 @@ paths once this flag is removed."
:type 'boolean
:safe #'booleanp)

(defcustom cljr-magic-requires-fallback t
"If t, `cljr-slash'' magic requires' will fallback to `cljr-magic-require-namespaces' if the middleware is not available."
:type 'boolean
:safe #'booleanp)

;; see https://guide.clojure.style/#use-idiomatic-namespace-aliases
(defcustom cljr-magic-require-namespaces
'(("edn" . "clojure.edn")
("io" "clojure.java.io" :only ("clj"))
'(("async" . "clojure.core.async")
("cli" . "clojure.tools.cli")
("csv" . "clojure.data.csv")
("datafy" . "clojure.datafy")
("edn" . "clojure.edn")
("io" "clojure.java.io" :only ("clj"))
("log" . "clojure.tools.logging")
("mat" . "clojure.core.matrix")
("math" . "clojure.math")
("set" . "clojure.set")
("str" . "clojure.string")
("p" . "clojure.core.protocols")
("pp" . "clojure.pprint")
("r" . "clojure.core.reducers")
("s" . "clojure.spec.alpha")
("set" . "clojure.set")
Comment on lines +109 to +113
Copy link
Member

@vemv vemv Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These almost certainly will conflict will real-world preferences/usages from users out there, so I'd rather leave them untouched.

("sh" . "clojure.java.shell")
("str" . "clojure.string")
("walk" . "clojure.walk")
("zip" . "clojure.zip"))
("xml" . "clojure.data.xml")
("zip" . "clojure.zip"))
"Alist of aliases to namespace libspec recommendations for `\\[cljr-slash]'.

An optional keyword `:only` can limit a recommendation to the set of
language contexts (clj, cljs) the libspec is available in."
:type '(repeat (cons (string :tag "Short alias")
(string :tag "Full namespace")))
:type '(repeat (choice
(cons
(string :tag "Short alias")
(string :tag "Full namespace"))
(list
(string :tag "Short alias")
(string :tag "Full namespace")
(symbol :tag "Optional keyword" :only)
(repeat (string :tag "Language contexts")))))
:safe #'listp)

(defcustom cljr-project-clean-prompt t
Expand Down Expand Up @@ -2095,28 +2120,6 @@ is not set to `:prompt'."
'(re-search-forward "[0-9`':#]*" nil t))
(1- (point))))

;; TODO: deprecated by `cljr-slash-uses-suggest-libspec'
(defun cljr--magic-requires-lookup-alias (short)
"Generate a mapping from alias to candidate namespaces.

If we recognize the `short' alias in the project, use namespaces
from the middleware or any `cljr-magic-require-namespaces' that
match. Returns a structure of (alias (ns1 ns2 ...))."
(if-let ((aliases (ignore-errors (cljr--get-aliases-from-middleware)))
(candidates (gethash (intern short) aliases)))
(list short candidates)
(when (and cljr-magic-require-namespaces ; a regex against "" always triggers
(string-match-p (cljr--magic-requires-re) short))
;; This when-let might seem unnecessary but the regexp match
;; isn't perfect.
(let ((long (cljr--aget cljr-magic-require-namespaces short)))
(when-let (libspec (cond ((stringp long)
(list long))
;; handle ("io" "clojure.java.io" :only ("clj"))
((and (listp long) (stringp (car long)))
(list (car long)))))
(list short libspec))))))

(defun cljr--in-keyword-sans-alias-p ()
"Checks if thing at point is keyword without an alias."
(let ((sym (cider-symbol-at-point)))
Expand Down Expand Up @@ -2164,45 +2167,83 @@ Filters out existing alias in the namespace, or a global alias
(cljr--indent-defun)
(cljr--post-command-message "Required %s" libspec)))

(defun cljr--alias-ref-at-point ()
(and cljr-magic-requires
(not (cljr--in-map-destructuring?))
(not (cljr--in-ns-above-point-p))
(not (cljr--in-reader-literal-p))
(not (cider-in-comment-p))
(not (cider-in-string-p))
(not (cljr--in-keyword-sans-alias-p))
(not (cljr--in-number-p))
(clojure-find-ns)
(cljr--unresolved-alias-ref (cljr--ns-alias-at-point))))

(defun cljr--magic-requires-libspec (alias-ref)
"Creates libspec suggestions for ALIAS-REF from `suggest-libspec' middleware op."
(thread-first alias-ref
(cljr--call-middleware-suggest-libspec (cljr--language-context-at-point))
cljr--prompt-or-select-libspec))


(defun cljr--aliases-to-libspec (aliases)
"Select a candidate from ALIASES if needed and format."
(let ((short (cl-first aliases))
;; Ensure it's a list (and not a vector):
(candidates (mapcar 'identity (cl-second aliases))))
(when-let (long (cljr--prompt-user-for "Require " candidates))
(when (and (not (cljr--in-namespace-declaration-p (concat ":as " short "\b")))
(not (cljr--in-namespace-declaration-p (concat ":as-alias " short "\b")))
(or (not (eq :prompt cljr-magic-requires))
(not (> (length candidates) 1)) ; already prompted
(yes-or-no-p (format "Add %s :as %s to requires?" long short))))
(format "[%s :as %s]" long short)))))


;; TODO: deprecated by `cljr-slash-uses-suggest-libspec'
(defun cljr--magic-requires-libspec-deprecated (short)
"Generate a mapping from SHORT alias to candidate namespaces.

If we recognize the `short' alias in the project, use namespaces
from the middleware Returns a structure of (alias (ns1 ns2 ...))."
(when-let ((aliases (cljr--get-aliases-from-middleware))
(candidates (gethash (intern short) aliases)))
(cljr--aliases-to-libspec (list short candidates))))

(defun cljr--magic-requires-libspec-defcustom (short)
"Generate a mapping from SHORT alias to candidate namespaces.

If we recognize the `short' alias in the project, use namespaces from
`cljr-magic-require-namespaces' Returns a structure of (alias (ns1 ns2 ...))."
(when (and cljr-magic-require-namespaces ; a regex against "" always triggers
(string-match-p (cljr--magic-requires-re) short))
;; This when-let might seem unnecessary but the regexp match
;; isn't perfect.
(let ((long (cljr--aget cljr-magic-require-namespaces short)))
(when-let (libspec (cond ((stringp long)
(list long))
;; handle ("io" "clojure.java.io" :only ("clj"))
((and (listp long) (stringp (car long)))
(list (car long)))))
(cljr--aliases-to-libspec (list short libspec))))))

;;;###autoload
(defun cljr-slash ()
"Inserts `/' as normal, but also checks for common namespace shorthands to require.
If `cljr-magic-requires' is non-nil, executing this command after one
of the aliases listed in `cljr-magic-require-namespaces', or any alias
used elsewhere in the project, will add the corresponding require statement
to the ns form."
(interactive)
(insert "/")
(when-let (alias-ref (and cljr-magic-requires
(not (cljr--in-map-destructuring?))
(not (cljr--in-ns-above-point-p))
(not (cljr--in-reader-literal-p))
(not (cider-in-comment-p))
(not (cider-in-string-p))
(not (cljr--in-keyword-sans-alias-p))
(not (cljr--in-number-p))
(clojure-find-ns)
(cljr--unresolved-alias-ref (cljr--ns-alias-at-point))))
(if cljr-slash-uses-suggest-libspec
;; creates suggestions from `suggest-libspec' middleware op
(when-let (libspec
(thread-first alias-ref
(cljr--call-middleware-suggest-libspec (cljr--language-context-at-point))
cljr--prompt-or-select-libspec))
;; only insert a require if a candidate exists and was selected
(cljr--insert-require-libspec libspec))
;; Deprecated, creates suggestions from `namespace-aliases' middleware op
(when-let (aliases (cljr--magic-requires-lookup-alias alias-ref))
(let ((short (cl-first aliases))
;; Ensure it's a list (and not a vector):
(candidates (mapcar 'identity (cl-second aliases))))
(when-let (long (cljr--prompt-user-for "Require " candidates))
(when (and (not (cljr--in-namespace-declaration-p (concat ":as " short "\b")))
(not (cljr--in-namespace-declaration-p (concat ":as-alias " short "\b")))
(or (not (eq :prompt cljr-magic-requires))
(not (> (length candidates) 1)) ; already prompted
(yes-or-no-p (format "Add %s :as %s to requires?" long short))))
(cljr--insert-require-libspec (format "[%s :as %s]" long short)))))))))
(when-let* ((alias-ref (cljr--alias-ref-at-point))
(libspec (condition-case err
(if cljr-slash-uses-suggest-libspec
(cljr--magic-requires-libspec alias-ref)
(cljr--magic-requires-libspec-deprecated alias-ref))
(user-error (if cljr-magic-requires-fallback
(cljr--magic-requires-libspec-defcustom alias-ref)
err)))))
;; only insert a require if a candidate exists and was selected
(if (stringp libspec)
(cljr--insert-require-libspec libspec)
(user-error (error-message-string libspec)))))


;; TODO: deprecated by `cljr-slash-uses-suggest-libspec'
(defun cljr--in-namespace-declaration-p (s)
Expand Down
16 changes: 16 additions & 0 deletions tests/unit-test.el
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
;;; -*- lexical-binding: t; -*-
(require 'paredit)
(require 'clj-refactor)
(require 'buttercup)
Expand Down Expand Up @@ -359,6 +360,21 @@
:to-equal "[a.a :as a]"))
(expect 'completing-read :to-have-been-called-times 1)))

(describe "cljr--magic-requires-libspec-defcustom"
(it "generates libspec for aliases from `cljr-magic-require-namespaces'"
(spy-on 'cljr--in-namespace-declaration-p :and-return-value nil)
(expect
(let ((cljr-magic-require-namespaces '(("foo" . "clojure.foo"))))
(cljr--magic-requires-libspec-defcustom "foo")
:to-equal "[clojure.foo :as foo]")))

(it "doesn't generate libspec when alias is unknown"
(spy-on 'cljr--in-namespace-declaration-p :and-return-value nil)
(expect
(let ((cljr-magic-require-namespaces '(("foo" . "clojure.foo"))))
(cljr--magic-requires-libspec-defcustom "bar")
:to-be nil))))

(describe "cljr-slash"
(it "inserts single selection from suggest-libspec"
(spy-on 'cljr--call-middleware-suggest-libspec
Expand Down