├── .circleci
├── config.yml
└── deploy
│ └── deploy_release.clj
├── .clj-kondo
└── config.edn
├── .github
├── CONTRIBUTING.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── deps.edn
├── doc
└── 2019_07_22_suitable-figwheel.gif
├── eastwood.clj
├── fig.cljs.edn
├── project.clj
├── resources
├── public
│ └── index.html
└── suitable
│ ├── cljs
│ └── env.cljc
│ ├── test_macros.clj
│ ├── test_ns.cljs
│ ├── test_ns_dep.cljs
│ └── version.edn
├── shadow-cljs.edn
└── src
├── dev
└── suitable
│ ├── main.cljs
│ ├── nrepl.clj
│ ├── nrepl_figwheel.clj
│ ├── nrepl_shadow.clj
│ ├── repl.clj
│ └── scratch.clj
├── main
└── suitable
│ ├── ast.cljc
│ ├── complete_for_nrepl.clj
│ ├── compliment
│ └── sources
│ │ ├── cljs.clj
│ │ └── cljs
│ │ ├── analysis.clj
│ │ └── ast.cljc
│ ├── figwheel
│ └── main.clj
│ ├── hijack_rebel_readline_complete.clj
│ ├── js_completions.clj
│ ├── js_introspection.cljs
│ ├── middleware.clj
│ └── utils.clj
└── test
└── suitable
├── complete_for_nrepl_test.clj
├── compliment
└── sources
│ └── t_cljs.clj
├── js_completion_test.clj
├── js_introspection_test.cljs
└── spec.clj
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | # Default settings for executors
4 |
5 | defaults: &defaults
6 | working_directory: ~/repo
7 |
8 | env_defaults: &env_defaults
9 | LEIN_ROOT: "true" # we intended to run lein as root
10 | # JVM_OPTS:
11 | # - limit the maximum heap size to prevent out of memory errors
12 | # - print stacktraces to console
13 | JVM_OPTS: >
14 | -Xmx3200m
15 | -Dclojure.main.report=stderr
16 |
17 | # Runners for OpenJDK 8/11/16
18 |
19 | executors:
20 | openjdk8:
21 | docker:
22 | - image: circleci/clojure:openjdk-8-lein-2.9.1-node
23 | environment:
24 | <<: *env_defaults
25 | <<: *defaults
26 | openjdk11:
27 | docker:
28 | - image: circleci/clojure:openjdk-11-lein-2.9.1-node
29 | environment:
30 | <<: *env_defaults
31 | <<: *defaults
32 | openjdk16:
33 | docker:
34 | - image: circleci/clojure:openjdk-16-lein-2.9.5-buster-node
35 | environment:
36 | <<: *env_defaults
37 | <<: *defaults
38 | openjdk17:
39 | docker:
40 | - image: circleci/clojure:openjdk-17-lein-2.9.5-buster-node
41 | <<: *defaults
42 | environment:
43 | <<: *env_defaults
44 |
45 | commands:
46 | with_cache:
47 | description: |
48 | Run a set of steps with Maven dependencies and Clojure classpath cache
49 | files cached.
50 | This command restores ~/.m2 and .cpcache if they were previously cached,
51 | then runs the provided steps, and finally saves the cache.
52 | The cache-key is generated based on the contents of `deps.edn` present in
53 | the `working_directory`.
54 | parameters:
55 | steps:
56 | type: steps
57 | files:
58 | description: Files to consider when creating the cache key
59 | type: string
60 | default: "deps.edn project.clj build.boot"
61 | cache_version:
62 | type: string
63 | description: "Change this value to force a cache update"
64 | default: "1"
65 | steps:
66 | - run:
67 | name: Install Clojure
68 | command: |
69 | wget -nc https://download.clojure.org/install/linux-install-1.10.3.855.sh
70 | chmod +x linux-install-1.10.3.855.sh
71 | sudo ./linux-install-1.10.3.855.sh
72 | - run:
73 | name: Install make
74 | command: |
75 | sudo apt-get install make
76 | - run:
77 | name: Generate Cache Checksum
78 | command: |
79 | for file in << parameters.files >>
80 | do
81 | find . -name $file -exec cat {} +
82 | done | shasum | awk '{print $1}' > /tmp/clojure_cache_seed
83 | - restore_cache:
84 | key: clojure-<< parameters.cache_version >>-{{ checksum "/tmp/clojure_cache_seed" }}
85 | - steps: << parameters.steps >>
86 | - save_cache:
87 | paths:
88 | - ~/.m2
89 | - .cpcache
90 | key: clojure-<< parameters.cache_version >>-{{ checksum "/tmp/clojure_cache_seed" }}
91 |
92 | jobs:
93 |
94 | util_job:
95 | description: |
96 | Running utility commands/checks (linter etc.)
97 | Always uses Java11 and Clojure 1.10
98 | parameters:
99 | steps:
100 | type: steps
101 | executor: openjdk11
102 | environment:
103 | VERSION: "1.10"
104 | steps:
105 | - checkout
106 | - with_cache:
107 | cache_version: "1.10"
108 | steps: << parameters.steps >>
109 |
110 | deploy:
111 | executor: openjdk8
112 | steps:
113 | - checkout
114 | - run:
115 | name: Deploy
116 | command: |
117 | lein with-profile -user,+deploy run -m deploy-release make deploy
118 |
119 | test_code:
120 | description: |
121 | Run tests against given version of JDK and Clojure
122 | parameters:
123 | jdk_version:
124 | description: Version of JDK to test against
125 | type: string
126 | clojure_version:
127 | description: Version of Clojure to test against
128 | type: string
129 | executor: << parameters.jdk_version >>
130 | environment:
131 | VERSION: << parameters.clojure_version >>
132 | steps:
133 | - checkout
134 | - with_cache:
135 | cache_version: << parameters.clojure_version >>|<< parameters.jdk_version >>
136 | steps:
137 | - run:
138 | name: Ensure node.js
139 | command: node --version
140 | - run:
141 | name: Running tests
142 | command: make test
143 |
144 | workflows:
145 | version: 2.1
146 | ci-test-matrix:
147 | jobs:
148 | - test_code:
149 | matrix:
150 | parameters:
151 | # FIXME other things worth adding to the matrix:
152 | # - cider-nrepl
153 | # - node.js runtimes
154 | clojure_version: ["1.8", "1.9", "1.10", "1.11", "master"]
155 | jdk_version: [openjdk8, openjdk11, openjdk16, openjdk17]
156 | filters:
157 | branches:
158 | only: /.*/
159 | tags:
160 | only: /^v\d+\.\d+\.\d+(-alpha\d*)?(-beta\d*)?$/
161 | - util_job:
162 | name: Code Linting
163 | filters:
164 | branches:
165 | only: /.*/
166 | tags:
167 | only: /^v\d+\.\d+\.\d+(-alpha\d*)?(-beta\d*)?$/
168 | steps:
169 | - run:
170 | name: Running clj-kondo
171 | command: |
172 | make kondo
173 | - run:
174 | name: Running Eastwood
175 | command: |
176 | make eastwood
177 | - deploy:
178 | requires:
179 | - test_code
180 | - "Code Linting"
181 | filters:
182 | branches:
183 | ignore: /.*/
184 | tags:
185 | only: /^v\d+\.\d+\.\d+(-alpha\d*)?(-beta\d*)?$/
186 |
--------------------------------------------------------------------------------
/.circleci/deploy/deploy_release.clj:
--------------------------------------------------------------------------------
1 | (ns deploy-release
2 | (:require
3 | [clojure.java.io :as io]
4 | [clojure.java.shell :refer [sh]]
5 | [clojure.string :as str]))
6 |
7 | (def release-marker "v")
8 |
9 | (defn make-version [tag]
10 | (str/replace-first tag release-marker ""))
11 |
12 | (defn log-result [m]
13 | (println m)
14 | m)
15 |
16 | (defn -main [& _]
17 | (let [tag (System/getenv "CIRCLE_TAG")]
18 | (if-not tag
19 | (do
20 | (println "No CIRCLE_TAG found.")
21 | (System/exit 1))
22 | (if-not (re-find (re-pattern release-marker) tag)
23 | (do
24 | (println (format "The `%s` marker was not found in %s." release-marker tag))
25 | (System/exit 1))
26 | (let [version (make-version tag)
27 | version-file (io/file "resources" "suitable" "version.edn")]
28 | (assert (.exists version-file))
29 | (spit version-file (pr-str version))
30 | (apply println "Executing" *command-line-args*)
31 | (->> [:env (-> {}
32 | (into (System/getenv))
33 | (assoc "PROJECT_VERSION" version)
34 | (dissoc "CLASSPATH"))]
35 | (into (vec *command-line-args*))
36 | (apply sh)
37 | log-result
38 | :exit
39 | (System/exit)))))))
40 |
--------------------------------------------------------------------------------
/.clj-kondo/config.edn:
--------------------------------------------------------------------------------
1 | {:skip-comments true
2 | :linters {:unresolved-symbol {:exclude [set-descriptor! (suitable.complete-for-nrepl/with-cljs-env [cljs-eval-cljs-fn
3 | cljs-repl-setup-fn
4 | cljs-load-namespace-fn
5 | cljs-evaluate-fn])]}
6 |
7 | :unresolved-namespace {:exclude [transport
8 | js]}
9 |
10 | :unresolved-var {:exclude []}
11 |
12 | :unused-referred-var {:level :off ;; disabled for now because the next line doesn't appear to work
13 | :exclude {goog.object [set]}}}}
14 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | If you discover issues, have ideas for improvements or new features, or
4 | want to contribute a new module, please report them to the
5 | [issue tracker][1] of the repository or submit a pull request. Please,
6 | try to follow these guidelines when you do so.
7 |
8 | ## Issue reporting
9 |
10 | * Check that the issue has not already been reported.
11 | * Check that the issue has not already been fixed in the latest code
12 | (a.k.a. `master`).
13 | * Be clear, concise and precise in your description of the problem.
14 | * Open an issue with a descriptive title and a summary in grammatically correct,
15 | complete sentences.
16 | * Include any relevant code to the issue summary.
17 |
18 | ## Pull requests
19 |
20 | * Read [how to properly contribute to open source projects on Github][2].
21 | * Use a topic branch to easily amend a pull request later, if necessary.
22 | * Write [good commit messages][3].
23 | * Squash related commits together.
24 | * Use the same coding conventions as the rest of the project.
25 | * Include tests for the code you've submitted.
26 | * Make sure the existing tests pass.
27 | * Open a [pull request][4] that relates to *only* one subject with a clear title
28 | and description in grammatically correct, complete sentences.
29 |
30 | [1]: https://github.com/clojure-emacs/clj-suitable/issues
31 | [2]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request
32 | [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
33 | [4]: https://help.github.com/articles/using-pull-requests
34 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Before submitting a PR make sure the following things have been done:
2 |
3 | - [ ] The commits are consistent with our [contribution guidelines](../blob/master/.github/CONTRIBUTING.md)
4 | - [ ] You've added tests to cover your change(s)
5 | - [ ] All tests are passing
6 | - [ ] The new code is not generating reflection warnings
7 | - [ ] You've updated the [changelog](../blob/master/CHANGELOG.md) (if adding/changing user-visible functionality)
8 |
9 | Thanks!
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cljs_*_repl/
2 | .nrepl-port
3 | pom.xml
4 | /.calva/output-window/*.calva-repl
5 | /.cljs_nashorn_repl/
6 | /.cljs_node_repl/
7 | /.cpcache/
8 | /.lsp/sqlite*.db
9 | /.rebel_readline_history
10 | /.shadow-cljs
11 | /nashorn_code_cache/
12 | /out/
13 | /suitable.jar
14 | /target/
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## master
4 |
5 | # 0.6.2 (2033-01-14)
6 |
7 | * [#45](https://github.com/clojure-emacs/clj-suitable/issues/45): don't exclude enumerable properties from `Object`.
8 |
9 | ## 0.6.1 (2023-11-07)
10 |
11 | - [#44](https://github.com/clojure-emacs/clj-suitable/pull/44): More robust completion for referred keywords. If a given namespace is
12 | required using `:as-alias`, completion candidates were missing for some CLJS environments.
13 |
14 | ## 0.6.0 (2023-11-05)
15 |
16 | - [#39](https://github.com/clojure-emacs/clj-suitable/issues/39): Exclude enumerable from JS completion candidates.
17 | - Expand completion for keywords referred using `:as-alias`.
18 |
19 | ## 0.5.1 (2023-10-31)
20 |
21 | - [#41](https://github.com/clojure-emacs/clj-suitable/pull/41): Expand completion for non-namespaced keywords.
22 |
23 | ## 0.5.0 (2023-07-28)
24 |
25 | * [#30](https://github.com/clojure-emacs/clj-suitable/issues/30): don't run side-effects for pure-clojurescript (non-interop) `->` chains.
26 |
27 | ## 0.4.1 (2021-10-02)
28 |
29 | * [#22](https://github.com/clojure-emacs/clj-suitable/issues/22): Gracefully handle string requires.
30 | * [#14](https://github.com/clojure-emacs/clj-suitable/issues/14): Fix a NullPointerException / Fix Node.js detection
31 |
32 | ## 0.4.0 (2021-04-18)
33 |
34 | * Fix dynamic completion for `shadow-cljs`.
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Robert Krahn
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean test install deploy nrepl fig-repl kondo eastwood lint
2 |
3 | VERSION ?= 1.10
4 |
5 | clean:
6 | @-rm -rf target/public/cljs-out \
7 | suitable.jar \
8 | .cpcache \
9 | target \
10 | out \
11 | .cljs_node_repl \
12 | .rebel_readline_history
13 | lein with-profile -user clean
14 |
15 | test: clean
16 | clojure -M:test:test-runner:$(VERSION)
17 |
18 | kondo:
19 | clojure -M:dev-figwheel:fig-repl:dev-shadow:test:kondo
20 |
21 | eastwood:
22 | clojure -M:dev-figwheel:fig-repl:dev-shadow:test:eastwood
23 |
24 | lint: kondo eastwood
25 |
26 | install: clean check-install-env
27 | lein with-profile -user,-dev install
28 |
29 | deploy: clean check-ci-env
30 | lein with-profile -user,-dev deploy clojars
31 |
32 | # starts a figwheel repl with suitable enabled
33 | fig-repl:
34 | clojure -M:fig-repl
35 |
36 | # useful for development, see comment in src/dev/suitable/nrepl_figwheel.clj
37 | nrepl-figwheel:
38 | clojure -M:test:dev-figwheel
39 |
40 | # useful for development, see comment in src/dev/suitable/nrepl_.clj
41 | nrepl-shadow:
42 | clojure -M:test:dev-shadow
43 |
44 | check-ci-env:
45 | ifndef CLOJARS_USERNAME
46 | $(error CLOJARS_USERNAME is undefined)
47 | endif
48 | ifndef CLOJARS_PASSWORD
49 | $(error CLOJARS_PASSWORD is undefined)
50 | endif
51 | ifndef CIRCLE_TAG
52 | $(error CIRCLE_TAG is undefined. Please only perform deployments by publishing git tags. CI will do the rest.)
53 | endif
54 |
55 | check-install-env:
56 | ifndef PROJECT_VERSION
57 | $(error Please set PROJECT_VERSION as an env var beforehand.)
58 | endif
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # suitable - ClojureScript Completion Toolkit
2 |
3 | [](https://circleci.com/gh/clojure-emacs/clj-suitable/tree/master)
4 | [](https://clojars.org/org.rksm/suitable)
5 | [](https://cljdoc.org/d/org.rksm/suitable/CURRENT)
6 | [](https://clojars.org/org.rksm/suitable)
7 |
8 | `suitable` provides static and dynamic code completion for ClojureScript tools.
9 |
10 | It provides two complementary completion sources:
11 |
12 | - It integrates with the CLJS analyzer and using the compilation state for "static" symbol completion. This functionality was briefly part of [compliment](https://github.com/alexander-yakushev/compliment), and before this - [Orchard](https://github.com/clojure-emacs/orchard) and [cljs-tooling](https://github.com/clojure-emacs/cljs-tooling).
13 | - It can use a CLJS REPL session to query and inspect JavaScript runtime state, allowing code completion for JavaScript objects and interfaces.
14 |
15 | ## Static code completion
16 |
17 | The static code completion is based on analysis of the ClojureScript compiler state. This approach was pioneered by `cljs-tooling` and the completion logic was subsequently moved to `orchard`, `compliment` and finally here.
18 |
19 | Why here? Because it's very convenient from the user perspective to have a single library providing both types of completion.
20 |
21 | This type of completion provides a [compliment custom source](https://github.com/alexander-yakushev/compliment/wiki/Custom-sources) for ClojureScript, so it's easy to plug with the most popular completion framework out there.
22 |
23 | ``` clojure
24 | (ns suitable.demo
25 | (:require
26 | [compliment.core :as complete]
27 | [suitable.compliment.sources.cljs :as suitable-sources]))
28 |
29 | (def cljs-sources
30 | "A list of ClojureScript completion sources for compliment."
31 | [::suitable-sources/cljs-source])
32 |
33 | ;; you can obtain the ClojureScript environment in many different ways
34 | ;; we'll leave the details to you
35 | (binding [suitable-sources/*compiler-env* cljs-env]
36 | (complete/completions prefix (merge completion-opts {:sources cljs-sources})))
37 | ```
38 |
39 | Note that you'll need to establish a binding to `suitable-sources/*compiler-env*` for the completion to work.
40 |
41 | ## Dynamic code completion for CLJS repls
42 |
43 | The dynamic code completion features allow for exploratory development by inspecting the runtime. For example you work with DOM objects but can't remember how to query for child elements. Type `(.| js/document)` (with `|` marking the postion of your cursor) and press TAB. Methods and properties of `js/document` will appear — including `querySelector` and `querySelectorAll`.
44 |
45 | ### Beware the Side-Effects
46 |
47 | The dynamic code completion *evaluates* code on completion requests! It does this by trying to [enumerate the properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors) of JavaScript objects, so in the example above it would fetch all properties of the `js/document` object. This might cause side effects: Even just reading property values of an object can run arbitrary code if that object defines getter functions.
48 |
49 | Moreover, also chains of methods and properties will be evaluated upon completion requests. So for example, asking for completions for the code / cursor position `(-> js/some-object (.deleteAllMyFilesAndStartAWar) .|)` will evaluate the JavaScript code `some-object.deleteAllMyFilesAndStartAWar()`! This only applies to JavaScript interop code, i.e. JavaScript methods and properties. Pure ClojureScript is not inspected or evaluated. Please be aware of this behavior when using the dynamic code completion features.
50 |
51 | ### Dynamic completion Demo
52 |
53 | The animation shows how various properties and methods of the native DOM can be accessed (Tab is used to show completions for the expression at the cursor):
54 |
55 | 
56 |
57 | ## Setup
58 |
59 | ### figwheel.main with rebel-readline
60 |
61 | Please note that you need to use [rebel-readline](https://github.com/bhauman/rebel-readline) with figwheel for that to work. Plain repls have no completion feature.
62 |
63 | #### Tools CLI
64 |
65 | First make sure that the [normal Tools CLI setup](https://figwheel.org/#setting-up-a-build-with-tools-cli) works.
66 |
67 | Then modify `deps.edn` and `dev.cljs.edn`, you should end up with the files looking like below:
68 |
69 | - `deps.edn`
70 |
71 | ```clojure
72 | {:deps {com.bhauman/figwheel-main {:mvn/version "RELEASE"}
73 | com.bhauman/rebel-readline-cljs {:mvn/version "RELEASE"}}
74 | :paths ["src" "target" "resources"]
75 | :aliases {:build-dev {:main-opts ["-m" "figwheel.main" "-b" "dev" "-r"]}
76 | :suitable {:extra-deps {org.rksm/suitable {:mvn/version "RELEASE"}}
77 | :main-opts ["-e" "(require,'suitable.hijack-rebel-readline-complete)"
78 | "-m" "figwheel.main"
79 | "--build" "dev" "--repl"]}}}
80 | ```
81 |
82 | - `dev.cljs.edn`
83 |
84 | ```clojure
85 | {:main example.core
86 | :preloads [suitable.js-introspection]}
87 | ```
88 |
89 | - `src/example/core.cljs`
90 |
91 | ```clojure
92 | (ns example.core)
93 | ```
94 |
95 | You can now start a figwheel repl via `clj -M:suitable` and use TAB to complete.
96 |
97 | #### leiningen
98 |
99 | First make sure that the [normal leiningen setup](https://figwheel.org/#setting-up-a-build-with-leiningen) works.
100 |
101 | Add `[org.rksm/suitable "0.6.2"]` to your dependencies vector.
102 |
103 | Then you can start a repl with `lein trampoline run -m suitable.figwheel.main -- -b dev -r`
104 |
105 | ### Emacs CIDER
106 |
107 | `suitable` is used by CIDER's code completion middleware, so no extra installation steps are required.
108 |
109 | CIDER will always use the static code completion provided by suitable, regardless of the ClojureScript runtime.
110 |
111 | In case you run into any issues with suitable's dynamic completion in CIDER you can disable it like this:
112 |
113 | ``` emacs-lisp
114 | (setq cider-enhanced-cljs-completion-p nil)
115 | ```
116 |
117 | You'll still be using `suitable` this way, but only its static completion mechanism.
118 |
119 | ### VS Code Calva
120 |
121 | Everything in the section above applies when using Calva.
122 |
123 | The `calva.enableJSCompletions` setting controls dynamic completion, and it is enabled by default.
124 |
125 | ### Custom nREPL server
126 |
127 | To load suitable into a custom server you can load it using this monstrosity:
128 |
129 | ```shell
130 | clj -Sdeps '{:deps {cider/cider-nrepl {:mvn/version "RELEASE"} cider/piggieback {:mvn/version "RELEASE"}}}' -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware,cider.piggieback/wrap-cljs-repl]"
131 | ```
132 |
133 | Or from within Clojure:
134 |
135 | ```clojure
136 | (ns my-own-nrepl-server
137 | (:require cider.nrepl
138 | cider.piggieback
139 | nrepl.server))
140 |
141 | (defn start-cljs-nrepl-server []
142 | (let [middlewares (conj (map resolve cider.nrepl/cider-middleware)
143 | #'cider.piggieback/wrap-cljs-repl)
144 | handler (apply nrepl.server/default-handler middlewares)]
145 | (nrepl.server/start-server :handler handler))
146 | ```
147 |
148 | **Note:** Make sure to use the latest version of `cider-nrepl` and `piggieback`.
149 |
150 | ## How does it work?
151 |
152 | suitable uses the same input as the widely used
153 | [compliment](https://github.com/alexander-yakushev/compliment). This means it gets a prefix string and a context form from the tool it is connected to. For example you type `(.l| js/console)` with "|" marking where your cursor is. The input we get would then be: prefix = `.l` and context = `(__prefix__ js/console)`.
154 |
155 | suitable recognizes various ways how CLJS can access properties and methods, such as:
156 |
157 | * `.`,
158 | * `..`
159 | * `doto`
160 | * threading forms
161 | * For this specific case, evaluation-based (side-effectful) code completion will be only performed if the threading form appears to deal with js objects.
162 |
163 | Also, direct global access is supported such as `js/console.log`. suitable will then figure out the expression that defines the "parent object" that the property / method we want to use belongs to. For the example above it would be `js/console`. The system then uses the [EcmaScript property descriptor API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) to enumerate the object members. Those are converted into completion candidates and send back to the tooling.
164 |
165 | ## Development
166 |
167 | #### Local install
168 |
169 | ```
170 | PROJECT_VERSION=0.6.2 make install
171 | ```
172 |
173 | #### Releasing to Clojars
174 |
175 | Release to [clojars](https://clojars.org/) by tagging a new release:
176 |
177 | ```
178 | git tag -a v0.6.2 -m "Release 0.6.2"
179 | git push --tags
180 | ```
181 |
182 | The CI will take it from there.
183 |
184 | ## License
185 |
186 | This project is [MIT licensed](LICENSE).
187 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
2 | :deps {org.clojure/clojurescript {:mvn/version "1.11.60"}
3 | org.clojure/clojure {:mvn/version "1.11.1" :scope "provided"}
4 | compliment/compliment {:mvn/version "0.4.0"}}
5 |
6 | :paths ["src/main"]
7 |
8 | :aliases {:1.8 {:extra-deps {org.clojure/clojure {:mvn/version "1.8.0"}
9 | org.clojure/clojurescript {:mvn/version "1.10.520"}}}
10 | :1.9 {:extra-deps {org.clojure/clojure {:mvn/version "1.9.0"}
11 | org.clojure/clojurescript {:mvn/version "1.10.520"}}}
12 | :1.10 {:extra-deps {org.clojure/clojure {:mvn/version "1.10.3"}
13 | org.clojure/clojurescript {:mvn/version "1.10.520"}}}
14 | :1.11 {:extra-deps {org.clojure/clojure {:mvn/version "1.11.1"}
15 | org.clojure/clojurescript {:mvn/version "1.11.60"}}}
16 | :master {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0-master-SNAPSHOT"}
17 | org.clojure/clojurescript {:git/url "https://github.com/clojure/clojurescript"
18 | ;; Please upgrade the following from time to time:
19 | :git/sha "6aefc7354c3f7033d389634595d912f618c2abfc"
20 | ;; For older tools.deps:
21 | :sha "6aefc7354c3f7033d389634595d912f618c2abfc"}}}
22 |
23 | ;; for starting nrepl clj & cljs servers for live development
24 | :dev-figwheel {:extra-paths ["src/dev" "resources" "target"]
25 | :extra-deps {cider/piggieback {:mvn/version "0.5.3"}
26 | cider/cider-nrepl {:mvn/version "0.32.0"}
27 | com.bhauman/figwheel-main {:mvn/version "0.2.18"}}
28 | :main-opts ["-m" "suitable.nrepl-figwheel"]}
29 |
30 | :dev-shadow {:extra-paths ["src/dev" "resources" "target"]
31 | :extra-deps {cider/piggieback {:mvn/version "0.5.3"}
32 | cider/cider-nrepl {:mvn/version "0.32.0"}
33 | thheller/shadow-cljs {:mvn/version "2.24.1"}}
34 | :main-opts ["-m" "suitable.nrepl-shadow"]}
35 |
36 | :fig-repl {:extra-paths ["resources" "target" "src/dev" "src/test"]
37 | :main-opts ["-e" "(require,'suitable.hijack-rebel-readline-complete)"
38 | "-m" "figwheel.main" "--build" "fig" "--repl"]
39 | :extra-deps {com.bhauman/figwheel-main {:mvn/version "0.2.18"}
40 | com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"}}}
41 |
42 | ;; build the cljs dev stuff, optional
43 | :build-cljs {:extra-paths ["resources" "target" "src/dev" "src/test"]
44 | :main-opts ["-m" "figwheel.main" "-b" "fig"]
45 | :extra-deps {com.bhauman/figwheel-main {:mvn/version "0.2.18"}}}
46 |
47 | :test {:extra-paths ["src/test" "resources"]
48 | :extra-deps {cider/cider-nrepl {:mvn/version "0.32.0"}
49 | cider/piggieback {:mvn/version "0.5.3"}}
50 | :jvm-opts ["-Dclojure.main.report=stderr"]}
51 |
52 | :test-runner {:extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
53 | :sha "209b64504cb3bd3b99ecfec7937b358a879f55c1"}}
54 | :main-opts ["-m" "cognitect.test-runner" "-d" "src/test"]}
55 |
56 | ;; build a jar, https://juxt.pro/blog/posts/pack-maven.html
57 | :pack {:extra-deps {pack/pack.alpha {:git/url "https://github.com/juxt/pack.alpha.git"
58 | :sha "2769a6224bfb938e777906ea311b3daf7d2220f5"}}
59 | :main-opts ["-m"]}
60 |
61 | :kondo
62 | {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2023.07.13"}}
63 | :main-opts ["-m" "clj-kondo.main" "--lint" "src/main" "src/test"
64 | ;; No src/dev linting for now - it has scratch namespaces which I don't want to break:
65 | #_ "src/dev"]}
66 |
67 | :eastwood
68 | {:main-opts ["-m" "eastwood.lint" {:config-files ["eastwood.clj"]}]
69 | :extra-deps {jonase/eastwood {:mvn/version "1.4.0"}}}
70 |
71 | :deploy
72 | {:extra-paths [".circleci/deploy"]}}}
73 |
--------------------------------------------------------------------------------
/doc/2019_07_22_suitable-figwheel.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojure-emacs/clj-suitable/ac8b49f9fe5d9083500b79a3bf8f3458310a56dd/doc/2019_07_22_suitable-figwheel.gif
--------------------------------------------------------------------------------
/eastwood.clj:
--------------------------------------------------------------------------------
1 | ;; avoid a corner case in Eastwood / tools.analyzer:
2 | (require 'figwheel.main.api)
3 |
4 | (disable-warning
5 | {:linter :constant-test
6 | :if-inside-macroexpansion-of #{'suitable.complete-for-nrepl/with-cljs-env}})
7 |
--------------------------------------------------------------------------------
/fig.cljs.edn:
--------------------------------------------------------------------------------
1 | ^{:watch-dirs ["src/main" "src/dev" "src/test"]
2 | :css-dirs ["resources/public/css"]
3 | :hot-reload-cljs true
4 | :open-url false
5 | :reload-clj-files #{:clj :cljc}
6 | :cljs-devtools true
7 | :ansi-color-output true}
8 | {:main suitable.main
9 | :preloads [suitable.js-introspection]}
10 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (require '[clojure.string :as string])
2 |
3 | (defn deep-merge
4 | {:license "Copyright © 2019 James Reeves
5 |
6 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version."}
7 | ([])
8 | ([a] a)
9 | ([a b]
10 | (when (or a b)
11 | (letfn [(merge-entry [m e]
12 | (let [k (key e)
13 | v' (val e)]
14 | (if (contains? m k)
15 | (assoc m k (let [v (get m k)]
16 | (cond
17 | (and (map? v) (map? v')) (deep-merge v v')
18 | (and (coll? v) (coll? v')) (into (empty v)
19 | (distinct (into v v')))
20 | true (do
21 | (assert (not (and v v')))
22 | (or v v')))))
23 | (assoc m k v'))))]
24 | (->> b
25 | seq
26 | (reduce merge-entry (or a {}))))))
27 | ([a b & more]
28 | (reduce deep-merge (or a {}) (cons b more))))
29 |
30 | (def repo-mapping (atom {}))
31 |
32 | (def git-hosts (atom #{}))
33 |
34 | (defn process-dep-entry! [{:keys [jars-atom subprojects-atom]}
35 | [k {:keys [mvn/version sha exclusions git/url local/root]}]]
36 | {:pre [@jars-atom @subprojects-atom]}
37 | (when url
38 | (let [{:keys [host path]} (-> url java.net.URI. bean)
39 | [gh-org gh-project] (-> path
40 | (string/replace #"^/" "")
41 | (string/replace #"\.git$" "")
42 | (string/split #"/"))]
43 | (swap! repo-mapping assoc k {:coordinates (symbol (str gh-org "/" gh-project))})
44 | (swap! git-hosts conj host)))
45 | (if-not root
46 | [k (or version sha) :exclusions (->> exclusions
47 | (mapv (fn [e]
48 | (-> e str (string/replace #"\$.*" "") symbol))))]
49 | (do
50 | (if (string/ends-with? root ".jar")
51 | (swap! jars-atom conj root)
52 | (let [f (java.io.File. root "deps.edn")]
53 | (assert (-> f .exists)
54 | (str "Expected " root " to denote an existing deps.edn file"))
55 | (swap! subprojects-atom conj (.getCanonicalPath f))))
56 | nil)))
57 |
58 | (defn prefix [filename item]
59 | (-> filename java.io.File. .getParent (java.io.File. item) .getCanonicalPath))
60 |
61 | (defn add-jars [m jars-atom sub? deps-edn-filename]
62 | {:pre [@jars-atom]}
63 | (cond-> m
64 | (seq @jars-atom) (update :resource-paths (fn [v]
65 | (cond->> @jars-atom
66 | true (into (or v []))
67 | sub? (mapv (partial prefix deps-edn-filename)))))))
68 |
69 | (declare add-subprojects)
70 |
71 | (defn process-profiles [aliases deps-edn-filename root-deps-edn-filename]
72 | (->> (for [[k {:keys [override-deps extra-deps extra-paths replace-paths]}] aliases
73 | :let [jars-atom (atom #{})
74 | subprojects-atom (atom #{} :validator (fn [v]
75 | (not (contains? v deps-edn-filename))))
76 | sub? (not= deps-edn-filename root-deps-edn-filename)
77 | sot (cond->> [[] extra-paths replace-paths]
78 | true (reduce into)
79 | true distinct
80 | sub? (map (partial prefix deps-edn-filename))
81 | true vec)]]
82 | [k (-> {:dependencies (->> [override-deps extra-deps]
83 | (keep (partial map (partial process-dep-entry! {:jars-atom jars-atom
84 | :subprojects-atom subprojects-atom})))
85 | (apply concat)
86 | distinct
87 | vec)
88 | :source-paths sot
89 | :test-paths sot}
90 | (add-jars jars-atom sub? deps-edn-filename)
91 | (add-subprojects subprojects-atom deps-edn-filename))])
92 | (into {})))
93 |
94 | (defn parse-deps-edn [deps-edn-filename root-deps-edn-filename]
95 | (let [{:keys [aliases deps paths]} (clojure.edn/read-string (slurp deps-edn-filename))
96 | profiles (process-profiles aliases deps-edn-filename root-deps-edn-filename)
97 | jars-atom (atom #{})
98 | subprojects-atom (atom #{}
99 | :validator (fn [v]
100 | (not (contains? v root-deps-edn-filename))))
101 | dependencies (->> deps
102 | (keep (partial process-dep-entry! {:jars-atom jars-atom
103 | :subprojects-atom subprojects-atom}))
104 | (vec))
105 | sub? (not= deps-edn-filename root-deps-edn-filename)
106 | sot (cond->> paths
107 | sub? (mapv (partial prefix deps-edn-filename)))]
108 | (-> {:dependencies dependencies
109 | :profiles (clojure.set/rename-keys profiles {:test-common :test})
110 | :source-paths sot
111 | :test-paths sot
112 | :resource-paths (cond->> @jars-atom
113 | sub? (map (partial prefix deps-edn-filename))
114 | true vec)}
115 | (add-subprojects subprojects-atom root-deps-edn-filename))))
116 |
117 | (defn add-subprojects [m subprojects-atom root-deps-edn-filename]
118 | (->> @subprojects-atom
119 | (reduce (fn [v subproject]
120 | (deep-merge v
121 | (parse-deps-edn subproject root-deps-edn-filename)))
122 | m)))
123 |
124 | (let [f (-> "deps.edn" java.io.File. .getCanonicalPath)
125 | {:keys [profiles dependencies resource-paths source-paths]} (parse-deps-edn f f)]
126 | (defproject org.rksm/suitable (or (not-empty (System/getenv "PROJECT_VERSION"))
127 | "0.0.0")
128 | :license {:name "MIT"
129 | :url "https://opensource.org/licenses/MIT"}
130 | :source-paths ~source-paths
131 | :resource-paths ~resource-paths
132 | :dependencies ~dependencies
133 | :profiles ~profiles
134 | :plugins [[reifyhealth/lein-git-down "0.4.0"]]
135 | :middleware [lein-git-down.plugin/inject-properties]
136 | :git-down ~(deref repo-mapping)
137 | :repositories ~(->> @git-hosts
138 | (map (fn [r]
139 | (let [n (-> r (string/split #"\.") first)
140 | u (str "git://" r)]
141 | [[(str "public-" n) {:url u}]
142 | [(str "private-" n) {:url u :protocol :ssh}]])))
143 | (apply concat)
144 | vec)
145 | :deploy-repositories [["clojars" {:url "https://clojars.org/repo"
146 | :username :env/clojars_username
147 | :password :env/clojars_password
148 | :sign-releases false}]]))
149 |
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | test app
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/resources/suitable/cljs/env.cljc:
--------------------------------------------------------------------------------
1 | (ns suitable.cljs.env
2 | (:require [cljs.env :as env]
3 | #?@(:clj [[cljs.analyzer.api :as ana]
4 | [cljs.build.api :as build]
5 | [cljs.compiler.api :as comp]
6 | [clojure.java.io :as io]])))
7 |
8 | (def test-namespace "suitable/test_ns.cljs" )
9 |
10 | (defn create-test-env []
11 | #?(:clj
12 | (let [opts (build/add-implicit-options {:cache-analysis true, :output-dir "target/out"})
13 | env (env/default-compiler-env opts)]
14 | (comp/with-core-cljs env opts
15 | (fn []
16 | (let [r (io/resource test-namespace)]
17 | (assert r (str "Cannot find " test-namespace " on the classpath, did you set it up correctly?"))
18 | (ana/analyze-file env r opts))))
19 | @env)
20 |
21 | :cljs
22 | (do (repl/eval '(require (quote suitable.test-ns)) 'suitable.test-env)
23 | @env/*compiler*)))
24 |
--------------------------------------------------------------------------------
/resources/suitable/test_macros.clj:
--------------------------------------------------------------------------------
1 | (ns ^{:doc "A test macro namespace"} suitable.test-macros)
2 |
3 | (defmacro my-add
4 | "This is an addition macro"
5 | [a b]
6 | `(+ ~a ~b))
7 |
8 | (defmacro my-sub
9 | "This is a subtraction macro"
10 | [a b]
11 | `(- ~a ~b))
12 |
--------------------------------------------------------------------------------
/resources/suitable/test_ns.cljs:
--------------------------------------------------------------------------------
1 | (ns ^{:doc "A test namespace"} suitable.test-ns
2 | (:refer-clojure :exclude [unchecked-byte while])
3 | (:require [clojure.string]
4 | [suitable.test-ns-dep :as test-dep :refer [foo-in-dep]]
5 | [suitable.test-ns-alias :as-alias aliased])
6 | (:require-macros [suitable.test-macros :as test-macros :refer [my-add]])
7 | (:import [goog.ui IdGenerator]))
8 |
9 | (defrecord TestRecord [a b c])
10 |
11 | (def x ::some-namespaced-keyword)
12 |
13 | (def from-aliased-ns ::aliased/kw)
14 |
15 | (def foo
16 | {:one "one"
17 | :two "two"
18 | :three "three"})
19 |
20 | (defn issue-28
21 | []
22 | (str "https://github.com/clojure-emacs/cljs-tooling/issues/28"))
23 |
24 | (defn test-public-fn
25 | []
26 | 42)
27 |
28 | (defn- test-private-fn
29 | []
30 | (inc (test-public-fn)))
31 |
--------------------------------------------------------------------------------
/resources/suitable/test_ns_dep.cljs:
--------------------------------------------------------------------------------
1 | (ns ;; Exercises doc expressed as metadata, `#'see suitable.compliment.sources.t-cljs/extra-metadata`:
2 | ^{:doc "Dependency of test-ns namespace"}
3 | suitable.test-ns-dep
4 | (:require
5 | ;; Exercises libspecs expressed as strings - see https://clojurescript.org/news/2021-04-06-release#_library_property_namespaces :
6 | ["clojure.set" :as set]))
7 |
8 | (defn foo-in-dep [foo] :bar)
9 |
10 | (def x ::dep-namespaced-keyword)
11 |
12 | (def bar-in-dep
13 | {:four "four"})
14 |
--------------------------------------------------------------------------------
/resources/suitable/version.edn:
--------------------------------------------------------------------------------
1 | ;; This file is automatically overwriten from .circleci/deploy/deploy_release.clj,
2 | ;; whenever we perform a deployment.
3 | "0.0.0"
4 |
--------------------------------------------------------------------------------
/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | ;; shadow-cljs configuration
2 | {:source-paths
3 | ["src/dev"
4 | "src/main"
5 | "src/test"]
6 |
7 | :dependencies
8 | []
9 |
10 | :builds
11 | {}}
12 |
--------------------------------------------------------------------------------
/src/dev/suitable/main.cljs:
--------------------------------------------------------------------------------
1 | (ns suitable.main
2 | (:require [cljs.test :refer-macros [run-tests]]
3 | [suitable.js-introspection-test]))
4 |
5 | (enable-console-print!)
6 |
7 | (run-tests 'suitable.js-introspection-test)
8 |
--------------------------------------------------------------------------------
/src/dev/suitable/nrepl.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.nrepl
2 | (:require cider.nrepl
3 | cider.piggieback
4 | [clojure.pprint :refer [cl-format pprint]]
5 | nrepl.core
6 | nrepl.server
7 | [suitable.middleware :refer [wrap-complete]]))
8 |
9 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
10 |
11 | ;; a la https://github.com/nrepl/piggieback/issues/91
12 | ;; 1. start nrepl server with piggieback
13 | ;; 2. get session
14 | ;; 3. send cljs start form (e.g. figwheel)
15 | ;; 4. ...profit!
16 |
17 | ;; 1. start nrepl server with piggieback
18 | (defonce clj-nrepl-server (atom nil))
19 |
20 |
21 | (defn start-clj-nrepl-server []
22 | (let [middlewares (map resolve cider.nrepl/cider-middleware)
23 | middlewares (if-let [rf (resolve 'refactor-nrepl.middleware/wrap-refactor)]
24 | (conj middlewares rf) middlewares)
25 | handler (apply nrepl.server/default-handler middlewares)]
26 | (pprint middlewares)
27 | (reset! clj-nrepl-server (nrepl.server/start-server :handler handler :port 7888)))
28 | (cl-format true "clj nrepl server started~%"))
29 |
30 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
31 |
32 | (defonce cljs-nrepl-server (atom nil))
33 | (defonce cljs-send-msg (atom nil))
34 | (defonce cljs-client (atom nil))
35 | (defonce cljs-client-session (atom nil))
36 |
37 | (defn start-cljs-nrepl-server []
38 | (let [middlewares (map resolve cider.nrepl/cider-middleware)
39 | middlewares (conj middlewares #'cider.piggieback/wrap-cljs-repl)
40 | middlewares (conj middlewares #'wrap-complete)
41 | ;; handler (nrepl.server/default-handler #'cider.piggieback/wrap-cljs-repl)
42 | handler (apply nrepl.server/default-handler middlewares)]
43 | (reset! cljs-nrepl-server (nrepl.server/start-server :handler handler :port 7889)))
44 | (cl-format true "cljs nrepl server started~%"))
45 |
46 | (defn start-cljs-nrepl-client []
47 | (let [conn (nrepl.core/connect :port 7889)
48 | c (nrepl.core/client conn 1000)
49 | sess (nrepl.core/client-session c)]
50 | (reset! cljs-client c)
51 | (reset! cljs-client-session sess)
52 | (cl-format true "nrepl client started~%")
53 | (reset! cljs-send-msg
54 | (fn [msg] (let [response-seq (nrepl.core/message sess msg)]
55 | (cl-format true "nrepl msg send~%")
56 | (pprint (doall response-seq)))))))
57 |
58 | (defn cljs-send-eval [code]
59 | (@cljs-send-msg {:op :eval :code code}))
60 |
--------------------------------------------------------------------------------
/src/dev/suitable/nrepl_figwheel.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.nrepl-figwheel
2 | (:require nrepl.server
3 | [suitable.nrepl :refer [start-clj-nrepl-server
4 | start-cljs-nrepl-server
5 | start-cljs-nrepl-client
6 | cljs-nrepl-server]]))
7 |
8 | ;;;; nrepl servers with figwheel
9 | ;;; This is useful for development. It will start two nrepl servers. The outer
10 | ;;; one on localhost:7888 allows to develop the clojure code. The inner server
11 | ;;; is a cljs repl and connected to figwheel. You can connect with cider
12 | ;;; 'figwheel-main build 'fig and test the cljs side of things (e.g. visit
13 | ;;; suitable.main). Also open localhost:9500 in a web browser to get the
14 | ;;; figwheel js enviornment loaded.
15 |
16 | (defn restart-cljs-server []
17 | (when @cljs-nrepl-server
18 | (nrepl.server/stop-server @cljs-nrepl-server))
19 | (require 'figwheel.main.api)
20 | (try (figwheel.main.api/stop-all) (catch Exception e (prn e)))
21 |
22 | (start-cljs-nrepl-server)
23 | (start-cljs-nrepl-client))
24 |
25 | (defn -main [& args]
26 | (start-clj-nrepl-server)
27 |
28 | (start-cljs-nrepl-server)
29 | (start-cljs-nrepl-client))
30 |
31 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
32 |
33 | (comment
34 | (do (start-nrepl-server)
35 | (start-nrepl-client)
36 | (cljs-send-eval "(require 'figwheel.main) (figwheel.main/start :fig)"))
37 |
38 | (cljs-send-eval "(require 'suitable.core)")
39 | (cljs-send-eval "123")
40 |
41 | (cljs-send-eval "(require 'figwheel.main.api) (figwheel.main.api/cljs-repl \"fig\")")
42 | (cljs-send-eval ":cljs/quit")
43 | )
44 |
45 |
--------------------------------------------------------------------------------
/src/dev/suitable/nrepl_shadow.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.nrepl-shadow
2 | (:require
3 | [shadow.cljs.devtools.server]
4 | [shadow.cljs.devtools.config :as config]
5 | [suitable.nrepl :refer [start-clj-nrepl-server
6 | start-cljs-nrepl-server
7 | start-cljs-nrepl-client
8 | cljs-nrepl-server
9 | cljs-send-eval]]))
10 |
11 | ;;;; nrepl servers with shadow-cljs
12 | ;;; start and connect to localhost:7889 via cider for a cljs repl. connect to
13 | ;;; :7888 for the clojure side.
14 |
15 | (defn -main [& args]
16 | (start-clj-nrepl-server)
17 | (shadow.cljs.devtools.server/start!
18 | (merge
19 | (config/load-cljs-edn)
20 | {:nrepl {:port 7889}})))
21 |
--------------------------------------------------------------------------------
/src/dev/suitable/repl.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.repl
2 | (:require [figwheel.main]
3 | [figwheel.main.api]
4 | [nrepl server core]
5 | [cider nrepl piggieback]
6 | [clojure.pprint :refer [cl-format pprint]]
7 | [clojure.stacktrace :refer [print-stack-trace print-trace-element]]
8 | [clojure.zip :as zip]
9 | [clojure.walk :as walk]
10 | [cider.piggieback]
11 | [suitable.middleware])
12 | (:import (java.lang Thread)))
13 |
14 |
15 |
16 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
17 |
18 | (require '[nrepl.misc :as misc :refer [response-for]])
19 | (require '[cider.piggieback])
20 | (require
21 | '[nrepl
22 | [middleware :refer [set-descriptor!]]
23 | [transport :as transport]])
24 | (require '[cider.nrepl.middleware.util.error-handling :refer [with-safe-transport]])
25 | (import 'nrepl.transport.Transport)
26 |
27 | (defn- cljs-eval
28 | "Abuses the nrepl handler `piggieback/do-eval` in that it injects a pseudo
29 | transport into it that simply captures it's output."
30 | [session ns code]
31 | (let [result (volatile! [])
32 | transport (reify Transport
33 | (recv [this]
34 | this)
35 | (recv [this timeout]
36 | this)
37 | (send [this response]
38 | (vswap! response conj result)
39 | this))
40 | eval-fn (or (resolve 'piggieback.core/do-eval)
41 | (resolve 'cider.piggieback/do-eval))]
42 | (eval-fn {:session session :transport transport :code code :ns ns})
43 | @result))
44 |
45 | (defonce state (atom nil))
46 |
47 | (defn cljs-dynamic-completion-handler
48 | [next-handler {:keys [id session transport op ns symbol context extra-metadata] :as msg}]
49 |
50 |
51 | (when (and (= op "complete")
52 | (some #(get-in @session [(resolve %)]) '(piggieback.core/*cljs-compiler-env*
53 | cider.piggieback/*cljs-compiler-env*))
54 | )
55 | (cl-format true "~A: ~A~%" op (select-keys msg [:ns :symbol :context :extra-metadata]))
56 |
57 | (let [answer (merge (when id {:id id})
58 | (when session {:session (if (instance? clojure.lang.AReference session)
59 | (-> session meta :id)
60 | session)}))]
61 |
62 | ;; (println (cljs-eval session "(properties-by-prototype js/console)" ns))
63 | (reset! state {:handler next-handler
64 | :session session
65 | :ns ns})
66 | (transport/send transport (assoc answer :completions [{:candidate "cljs.hello", :type "var"}]))))
67 |
68 | ;; call next-handler for the default completions
69 | (next-handler msg))
70 |
71 |
72 | (defn wrap-complete [handler]
73 | (fn [msg] (cljs-dynamic-completion-handler handler msg)))
74 |
75 | (set-descriptor! #'wrap-complete
76 | {:requires #{"clone"}
77 | :expects #{"complete" "eval"}
78 | :handles {}})
79 |
80 |
81 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
82 |
83 | ;; a la https://github.com/nrepl/piggieback/issues/91
84 | ;; 1. start nrepl server with piggieback
85 | ;; 2. get session
86 | ;; 3. send cljs start form (e.g. figwheel)
87 | ;; 4. ...profit!
88 |
89 | ;; 1. start nrepl server with piggieback
90 | (defonce server (atom nil))
91 | (defonce send-msg (atom nil))
92 |
93 | (defn start-nrepl-server []
94 | (let [middlewares (map resolve cider.nrepl/cider-middleware)
95 | middlewares (conj middlewares #'cider.piggieback/wrap-cljs-repl)
96 | middlewares (conj middlewares #'suitable.middleware/wrap-complete)
97 | ;; handler (nrepl.server/default-handler #'cider.piggieback/wrap-cljs-repl)
98 | handler (apply nrepl.server/default-handler middlewares)]
99 | (reset! server (nrepl.server/start-server :handler handler :port 7889)))
100 | (cl-format true "nrepl server started~%"))
101 |
102 | (defonce client (atom nil))
103 | (defonce client-session (atom nil))
104 |
105 | (defn start-nrepl-client []
106 | (let [conn (nrepl.core/connect :port 7889)
107 | c (nrepl.core/client conn 1000)
108 | sess (nrepl.core/client-session c)]
109 | (reset! client c)
110 | (reset! client-session sess)
111 | (cl-format true "nrepl client started~%")
112 | (reset! send-msg
113 | (fn [msg] (let [response-seq (nrepl.core/message sess msg)]
114 | (cl-format true "nrepl msg send~%")
115 | (pprint (doall response-seq)))))))
116 |
117 | (defn send-eval [code]
118 | (@send-msg {:op :eval :code code}))
119 |
120 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
121 |
122 | (comment
123 |
124 | (do (start-nrepl-server)
125 | (start-nrepl-client)
126 | (send-eval "(require 'figwheel.main) (figwheel.main/start :fig)"))
127 |
128 | (do (nrepl.server/stop-server @server)
129 | (require 'figwheel.main.api)
130 | (figwheel.main.api/stop-all))
131 |
132 | (pprint (doall (nrepl.core/message sess1 {:op :eval :code "(require 'cider.piggieback) (require 'cljs.repl.nashorn) (cider.piggieback/cljs-repl (cljs.repl.nashorn/repl-env))"})))
133 | (pprint (doall (nrepl.core/message sess1 {:op :eval :code "(require 'figwheel.main) (figwheel.main/start :fig)"})))
134 | (pprint (doall (nrepl.core/message sess1 {:op :eval :code "(require 'figwheel.main) (figwheel.main/stop-builds :fig)"})))
135 | (pprint (doall (nrepl.core/message sess1 {:op :eval :code ":cljs/quit"})))
136 | (pprint (doall (nrepl.core/message sess1 {:op :eval :code "js/console"})))
137 | (pprint (doall (nrepl.core/message sess1 {:op :eval :code "123"})))
138 | (nrepl.core/message sess1 {:op :eval :code "(list 1 2 3)"})
139 |
140 | )
141 |
--------------------------------------------------------------------------------
/src/dev/suitable/scratch.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.scratch
2 | (:require [clojure.zip :as zip]
3 | [clojure.spec.alpha :as s]
4 | [clojure.spec.test.alpha :as st]))
5 |
6 |
7 | (comment
8 |
9 |
10 | (sc.api/defsc 12)
11 | (sc.api/letsc 6 [obj-expr])
12 | (sc.api/letsc 6 [properties])
13 | (sc.api/letsc 2 [error])
14 |
15 | (s/def ::non-empty-string (s/and string? not-empty))
16 |
17 |
18 | (defn foo [x]
19 | {:pre [(s/valid? ::not-empty-string x )]}
20 | ;; {:pre [(s/valid? (s/coll-of string?) x)]}
21 | ;; {:pre [(s/valid? string? x)]}
22 | x)
23 |
24 | (with-redefs {#'foo (fn [x] (inc x))} (foo 23))
25 | (with-redefs [foo (fn [x] (inc x))] (foo 23))
26 |
27 | (with-redefs)
28 |
29 | (foo "23")
30 | (foo "")
31 |
32 |
33 |
34 | (defn my-inc [x]
35 | (inc x))
36 | (s/fdef my-inc
37 | :args (s/cat :x number?)
38 | :ret number?)
39 | (my-inc 0)
40 | (my-inc "foo")
41 | (st/instrument `my-inc)
42 | (st/instrument `my-inc)
43 | (s/exercise-fn `my-inc 10)
44 | (st/unstrument `my-inc)
45 |
46 | (defn bar [x]
47 | x)
48 |
49 | (st/instrument)
50 | (s/fdef bar
51 | :args (s/cat :x (s/and string? not-empty)))
52 |
53 | (st/instrument `bar)
54 |
55 | (s/exercise-fn `bar)
56 |
57 | (bar "foo")
58 | (bar "")
59 |
60 |
61 | (require '[clojure.zip :as zip])
62 |
63 | (sc.api/defsc 30)
64 |
65 | (zip/node prefix)
66 |
67 | (= (sc.api/letsc 1 [session] session) (sc.api/letsc 9 [session] session))
68 |
69 |
70 | (sc.api/letsc 1 [session] (get @session #'suitable.middleware/*object-completion-state*))
71 |
72 | session
73 | (let [a (atom {})] (= a a a))
74 |
75 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
76 |
77 | (->> properties
78 | )
79 |
80 | (merge (for [{:keys [name type]} properties] {:type type :candidate (str "." name)}))
81 | (map (fn [{:keys [name type]}] {:type type :candidate (str "." name)}) properties)
82 |
83 | (into)
84 | (into {} (fn [{:keys [name type]}] [:type type :candidate (str "." name)]) properties)
85 |
86 | (require '[clojure.pprint :refer [cl-format]])
87 |
88 |
89 | (doall
90 | (into {} (completing (fn [result {:keys [name type]}]
91 | (do
92 | (cl-format true "~A??????????~%" result)
93 | [:type type :candidate (str "." name)]))) properties))
94 |
95 | (into {} (map (fn [{:keys [name type]}] {:type type :candidate (str "." name)})) properties)
96 | (eduction (map (fn [{:keys [name type]}] [:type type :candidate (str "." name)])) properties)
97 | (transduce (map (fn [{:keys [name type]}] [:type type :candidate (str "." name)])) identity {} properties)
98 |
99 | (def xform (map #(+ 2 %)))
100 |
101 | (transduce
102 | xform
103 | (fn
104 | ([] (prn "0"))
105 | ([result] (prn "1" result))
106 | ([result input] (prn "2" result input)))
107 | [1 2 3])
108 |
109 | (into [-1 -2] xform (range 10))
110 |
111 | (fn [rf]
112 | (fn
113 | ([] (rf))
114 | ([result] (rf result))
115 | ([result input]
116 | (rf result (f input)))
117 | ([result input & inputs]
118 | (rf result (apply f input inputs)))))
119 |
120 | (into {} (fn [rf]
121 | (letfn [(f [input] (prn "input" input) input)]
122 | (let [f merge]
123 | (fn
124 | ([] (prn "0 =>" (rf)) (rf))
125 | ([result] (prn "1" result (rf result)) (rf result))
126 | ([result input]
127 | (rf result (f input)))))))
128 | properties)
129 |
130 |
131 |
132 |
133 |
134 | )
135 |
136 |
137 |
138 | (comment
139 |
140 | (require '[cider.nrepl.inlined-deps.compliment.v0v3v8.compliment.core :as compliment])
141 |
142 | compliment/all-files
143 |
144 | (compliment/completions ".get" {:ns "user" :context "(__prefix__ (java.lang.Thread.))"})
145 | (compliment/completions "clojure.string/joi" {:ns "user" :context "(__prefix__)"})
146 |
147 | )
148 |
--------------------------------------------------------------------------------
/src/main/suitable/ast.cljc:
--------------------------------------------------------------------------------
1 | (ns suitable.ast
2 | (:require [clojure.pprint :refer [pprint *print-right-margin*]]
3 | [clojure.zip :as z])
4 | #?(:clj (:import [clojure.lang IPersistentList IPersistentMap IPersistentVector ISeq])))
5 |
6 | (def V #?(:clj IPersistentVector
7 | :cljs PersistentVector))
8 | (def M #?(:clj IPersistentMap
9 | :cljs PersistentArrayMap))
10 | (def L #?(:clj IPersistentList
11 | :cljs List))
12 | (def S ISeq)
13 |
14 | ;; Thx @ Alex Miller! http://www.ibm.com/developerworks/library/j-treevisit/
15 | (defmulti tree-branch? type)
16 | (defmethod tree-branch? :default [_] false)
17 | (defmethod tree-branch? V [v] (not-empty v))
18 | (defmethod tree-branch? M [m] (not-empty m))
19 | (defmethod tree-branch? L [_l] true)
20 | (defmethod tree-branch? S [_s] true)
21 | (prefer-method tree-branch? L S)
22 |
23 | (defmulti tree-children type)
24 | (defmethod tree-children V [v] v)
25 | (defmethod tree-children M [m] (->> m seq (apply concat)))
26 | (defmethod tree-children L [l] l)
27 | (defmethod tree-children S [s] s)
28 | (prefer-method tree-children L S)
29 |
30 | (defmulti tree-make-node (fn [node _children] (type node)))
31 | (defmethod tree-make-node V [_v children]
32 | (vec children))
33 | (defmethod tree-make-node M [_m children]
34 | (apply hash-map children))
35 | (defmethod tree-make-node L [_ children]
36 | children)
37 | (defmethod tree-make-node S [_node children]
38 | (apply list children))
39 | (prefer-method tree-make-node L S)
40 |
41 | (defn tree-zipper [node]
42 | (z/zipper tree-branch? tree-children tree-make-node node))
43 |
44 | (defn print-tree
45 | "for debugging"
46 | [node]
47 | (let [all (take-while (complement z/end?) (iterate z/next (tree-zipper node)))]
48 | (binding [*print-right-margin* 20]
49 | (pprint
50 | (->> all
51 | (map z/node) (zipmap (range))
52 | sort)))))
53 |
--------------------------------------------------------------------------------
/src/main/suitable/complete_for_nrepl.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.complete-for-nrepl
2 | (:require [clojure.edn :as edn]
3 | [suitable.js-completions :refer [cljs-completions]]
4 | [clojure.string :as string]))
5 |
6 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
7 | ;; 2019-08-15 rk: FIXME! When being build as part of cider-nrepl, names of
8 | ;; cider-nrepl dependencies get munged by mranderson. This also munges cljs
9 | ;; namespaces but not references to them. So as a hack we grab the name of this
10 | ;; ns (can't use *ns* when bundled so abusing `dummy-var` for that) which has
11 | ;; the munged prefix. We then convert that into the cljs namespace we need. This
12 | ;; of course breaks when suitable.complete-for-nrepl is renamed(!).
13 | (def dummy-var nil)
14 | (def this-ns (:ns (meta #'dummy-var)))
15 |
16 | (defn munged-js-introspection-ns []
17 | (suitable.js-completions/js-introspection-ns))
18 |
19 | (defn munged-js-introspection-js-name []
20 | (-> (munged-js-introspection-ns)
21 | (string/replace #"-" "_")
22 | symbol))
23 |
24 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
25 |
26 | (def debug? false)
27 |
28 | (defonce ^{:private true} resolved-vars (atom nil))
29 |
30 | (defn- resolve-vars!
31 | "This lazy loads runtime state we depend on so that there are no static
32 | dependencies to piggieback or cljs."
33 | []
34 | (or
35 | @resolved-vars
36 | (let [piggieback-vars (cond
37 | (resolve 'cider.piggieback/*cljs-compiler-env*)
38 | {:cenv-var (resolve 'cider.piggieback/*cljs-compiler-env*)
39 | :renv-var (resolve 'cider.piggieback/*cljs-repl-env*)
40 | :opts-var (resolve 'cider.piggieback/*cljs-repl-options*)}
41 |
42 | (resolve 'piggieback.core/*cljs-compiler-env*)
43 | {:cenv-var (resolve 'piggieback.core/*cljs-compiler-env*)
44 | :renv-var (resolve 'piggieback.core/*cljs-repl-env*)
45 | :opts-var (resolve 'piggieback.core/*cljs-repl-options*)}
46 |
47 | :else nil)
48 |
49 | cljs-vars (do
50 | (require 'cljs.repl)
51 | (require 'cljs.analyzer)
52 | (require 'cljs.env)
53 | {:cljs-cenv-var (resolve 'cljs.env/*compiler*)
54 | :cljs-ns-var (resolve 'cljs.analyzer/*cljs-ns*)
55 | :cljs-repl-setup-fn (resolve 'cljs.repl/setup)
56 | :cljs-evaluate-fn (resolve 'cljs.repl/evaluate)
57 | :cljs-eval-cljs-fn (resolve 'cljs.repl/eval-cljs)
58 | :cljs-load-namespace-fn (resolve 'cljs.repl/load-namespace)})]
59 |
60 | (reset! resolved-vars
61 | (merge cljs-vars piggieback-vars)))))
62 |
63 | (defn- extract-cljs-state
64 | [session]
65 | (let [s @session
66 | {:keys [cenv-var renv-var opts-var]} (resolve-vars!)]
67 | {:cenv (get s cenv-var)
68 | :renv (get s renv-var)
69 | :opts (get s opts-var)}))
70 |
71 | (defn- update-cljs-state!
72 | [session cenv renv]
73 | (let [{:keys [cenv-var renv-var]} (resolve-vars!)]
74 | (swap! session assoc
75 | cenv-var cenv
76 | renv-var renv)))
77 |
78 | (defmacro with-cljs-env
79 | "Binds `cljs.env/*compiler*`, `cljs.analyzer/*cljs-ns*`, assigns bindings from
80 | `resolve-vars!` and runs `body`."
81 | [cenv ns cljs-bindings & body]
82 | (let [vars (gensym)]
83 | `(let [~vars (resolve-vars!)]
84 | (with-bindings {(:cljs-cenv-var ~vars) ~cenv
85 | (:cljs-ns-var ~vars) (if (string? ~ns) (symbol ~ns) ~ns)}
86 | (let ~(into []
87 | (apply concat
88 | (for [sym cljs-bindings]
89 | `(~sym ((keyword '~sym) ~vars)))))
90 | ~@body)))))
91 |
92 | (defn- cljs-eval
93 | "Grabs the necessary compiler and repl envs from the message and uses the plain
94 | cljs.repl interface for evaluation. Returns a map with :value and :error. Note
95 | that :value will be a still stringified edn value."
96 | [session ns code]
97 | (let [{:keys [cenv renv opts]} (extract-cljs-state session)
98 | ;; when run with mranderson as an inlined dep, the ns and it's interns
99 | ;; aren't recognized correctly by the analyzer, suppress an undefined
100 | ;; var warining for `js-properties-of-object`
101 | ;; TODO only add when run as inline-dep??
102 | opts (assoc opts :warnings {:undeclared-var false})]
103 | (with-cljs-env cenv ns
104 | [cljs-eval-cljs-fn]
105 | (try
106 | (let [result (cljs-eval-cljs-fn renv @cenv (read-string code) opts)]
107 | (update-cljs-state! session cenv renv)
108 | {:value result})
109 | (catch Exception e {:error e})))))
110 |
111 | (defn node-env?
112 | "Returns true iff RENV is a NodeEnv or more precisely a piggiebacked delegating
113 | NodeEnv. Since the renv is wrapped we can't just compare the type but have to
114 | do some string munging according to
115 | `cider.piggieback/generate-delegating-repl-env`."
116 | [renv]
117 | (let [normalize (fn [^Class c]
118 | (-> c
119 | .getName
120 | (string/replace "." "_")))
121 | expected (some-> 'cljs.repl.node.NodeEnv
122 | resolve
123 | normalize)
124 | actual (some-> renv
125 | class
126 | normalize
127 | ;; Examples of what has to be stripped away:
128 | ;; user$eval4016$__GT_Delegatingcljs_repl_node_NodeEnv__4018
129 | ;; user$eval4016$__GT_Delegatingcljs_repl_node_NodeEnv__4018@56478522
130 | (string/replace #".*Delegating" "")
131 | (string/replace #"__\d+$" "")
132 | (string/replace #"__\d+@.*$" ""))]
133 | (and (some? actual) ;; avoid (= nil nil), which would return a spurious result
134 | (= expected actual))))
135 |
136 | (defn ensure-suitable-cljs-is-loaded [session]
137 | (let [{:keys [cenv renv opts]} (extract-cljs-state session)]
138 | (with-cljs-env cenv 'cljs.user
139 | [cljs-repl-setup-fn cljs-load-namespace-fn cljs-evaluate-fn]
140 |
141 | (when (node-env? renv)
142 | ;; rk 2019-09-02 FIXME
143 | ;; Due to this issue:
144 | ;; https://github.com/clojure-emacs/cider-nrepl/pull/644#issuecomment-526953982
145 | ;; we can't just eval with a node env but have to make sure that it's
146 | ;; local buffer is initialized for this thread.
147 | (cljs-repl-setup-fn renv opts))
148 |
149 | (when (not= "true" (some-> (cljs-evaluate-fn
150 | renv "" 1
151 | ;; see above, would be suitable.js_introspection
152 | (format "!!goog.getObjectByName('%s')" (munged-js-introspection-js-name))) :value))
153 | (try
154 | ;; see above, would be suitable.js-introspection
155 | (cljs-load-namespace-fn renv (read-string (munged-js-introspection-ns)) opts)
156 | (catch Exception e
157 | ;; when run with mranderson, cljs does not seem to handle the ns
158 | ;; annotation correctly and does not recognize the namespace even
159 | ;; though it loads correctly.
160 | (when-not (and (string/includes? (munged-js-introspection-ns) "inlined-deps")
161 | (string/includes? (string/lower-case (str e)) "does not provide a namespace"))
162 | (throw e))))
163 | (cljs-evaluate-fn renv "" 1 (format "goog.require(\"%s\");%s"
164 | (munged-js-introspection-js-name)
165 | (if debug? " console.log(\"suitable loaded\");" "")))
166 | ;; wait as depending on the implemention of goog.require provide by the
167 | ;; cljs repl might be async. See
168 | ;; https://github.com/clojure-emacs/clj-suitable/issues/1 for more details.
169 | (Thread/sleep 100)
170 |
171 | (update-cljs-state! session cenv renv)))))
172 |
173 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
174 |
175 | (def ^:private ^:dynamic *object-completion-state* nil)
176 |
177 | (defn- empty-state [] {:context ""})
178 |
179 | (defn handle-completion-msg!
180 | "Tracks the completion state (contexts) and reuses old contexts if necessary.
181 | State is kept in session."
182 | [{:keys [session symbol context ns extra-metadata] :as _msg}
183 | cljs-eval-fn
184 | ensure-loaded-fn]
185 | (let [prev-state (or (get @session #'*object-completion-state*) (empty-state))
186 | prev-context (:context prev-state)
187 | context (cond
188 | (nil? context) ""
189 | (= context ":same") prev-context
190 | (= context "nil") ""
191 | :else context)
192 | options-map {:context context :ns ns :extra-metadata extra-metadata}]
193 |
194 | ;; make sure we can call the object completion api in cljs, i.e. loads
195 | ;; suitable cljs code if necessary.
196 | (ensure-loaded-fn session)
197 |
198 | (when (not= prev-context context)
199 | (swap! session #(merge % {#'*object-completion-state*
200 | (assoc prev-state :context context)})))
201 |
202 | (try
203 | (cljs-completions cljs-eval-fn symbol options-map)
204 | (catch Exception e
205 | (if debug?
206 | (do (println "suitable error")
207 | (.printStackTrace e))
208 | (println (format "suitable error (enable %s/debug for more details): %s" this-ns (.getMessage e))))))))
209 |
210 | (defn- complete-for-default-cljs-env
211 | [{:keys [session] :as msg}]
212 | (let [cljs-eval-fn
213 | (fn [ns code] (let [result (cljs-eval session ns code)]
214 | {:error (some->> result :error str)
215 | :value (some->> result :value edn/read-string)}))
216 | ensure-loaded-fn ensure-suitable-cljs-is-loaded]
217 | (handle-completion-msg! msg cljs-eval-fn ensure-loaded-fn)))
218 |
219 | (defn- shadow-cljs? [msg]
220 | (:shadow.cljs.devtools.server.nrepl-impl/build-id msg))
221 |
222 | (defn- complete-for-shadow-cljs
223 | "Shadow-cljs handles evals quite differently from normal cljs so we need some
224 | special handling here."
225 | [msg]
226 | (let [build-id (-> msg :shadow.cljs.devtools.server.nrepl-impl/build-id)
227 | cljs-eval-fn
228 | (fn [ns code]
229 | (let [result ((resolve 'shadow.cljs.devtools.api/cljs-eval) build-id code {:ns (symbol ns)})]
230 | {:error (some->> result :err str)
231 | :value (some->> result :results first edn/read-string)}))
232 | ensure-loaded-fn (fn [_] nil)]
233 | (handle-completion-msg! msg cljs-eval-fn ensure-loaded-fn)))
234 |
235 | (defn complete-for-nrepl
236 | "Computes the completions using the cljs environment found in msg."
237 | [msg]
238 | (if (shadow-cljs? msg)
239 | (complete-for-shadow-cljs msg)
240 | (complete-for-default-cljs-env msg)))
241 |
--------------------------------------------------------------------------------
/src/main/suitable/compliment/sources/cljs.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.compliment.sources.cljs
2 | "Standalone auto-complete library based on cljs analyzer state"
3 | (:refer-clojure :exclude [meta])
4 | (:require [clojure.set :as set]
5 | [compliment.sources :refer [defsource]]
6 | [compliment.utils :as utils :refer [*extra-metadata*]]
7 | [suitable.compliment.sources.cljs.analysis :as ana])
8 | (:import java.io.StringWriter))
9 |
10 | (defn unquote-first
11 | "Handles some weird double-quoting in the analyzer"
12 | [form]
13 | (if (= (first form) 'quote)
14 | (first (rest form))
15 | form))
16 |
17 | (defn normalize-arglists
18 | "Take metadata arglists and normalize them.
19 | Normalization being mainly removing the quote of the first element and
20 | wrapping in a list."
21 | [arglists]
22 | (some->> arglists
23 | (unquote-first)
24 | (map pr-str)
25 | (apply list)))
26 |
27 | (defn- candidate-extra
28 | [extra-metadata candidate-meta]
29 | (when (and (seq extra-metadata) candidate-meta)
30 | (let [extra (select-keys candidate-meta extra-metadata)]
31 | (cond-> extra
32 | (:arglists extra) (update :arglists normalize-arglists)))))
33 |
34 | (defn- candidate-data
35 | "Returns a map of candidate data for the given arguments."
36 | ([candidate ns type]
37 | (candidate-data candidate ns type nil nil))
38 | ([candidate ns type meta extra-metadata]
39 | (merge {:candidate (str candidate)
40 | :type type}
41 | (when ns {:ns (str ns)})
42 | (when (seq extra-metadata)
43 | (candidate-extra extra-metadata meta)))))
44 |
45 | (defn- var->type
46 | "Returns the candidate type corresponding to the given metadata map."
47 | [var]
48 | (condp #(get %2 %1) var
49 | :protocol :protocol-function
50 | :fn-var :function
51 | :record :record
52 | :protocols :type
53 | :protocol-symbol :protocol
54 | :var))
55 |
56 | (def ^:private special-forms
57 | '#{& . case* catch def defrecord* deftype* do finally fn* if js* let*
58 | letfn* loop* new ns quote recur set! throw try})
59 |
60 | (def ^:private special-form-candidates
61 | "Candidate data for all special forms."
62 | (for [form special-forms]
63 | (candidate-data form 'cljs.core :special-form)))
64 |
65 | (defn- all-ns-candidates
66 | "Returns candidate data for all namespaces in the environment."
67 | [env extra-metadata]
68 | ;; recent CLJS versions include data about macro namespaces in the
69 | ;; compiler env, but we should not include them in completions or pass
70 | ;; them to format-ns unless they're actually required (which is handled
71 | ;; by macro-ns-candidates below)
72 | (for [[ns meta] (ana/all-ns env)]
73 | (candidate-data ns nil :namespace meta extra-metadata)))
74 |
75 | (defn- ns-candidates
76 | "Returns candidate data for all referred namespaces (and their aliases) in context-ns."
77 | [env context-ns extra-metadata]
78 | (for [[alias ns] (ana/ns-aliases env context-ns)]
79 | (candidate-data alias
80 | (when-not (= alias ns) ns)
81 | :namespace
82 | (ana/ns-meta ns)
83 | extra-metadata)))
84 |
85 | (defn- macro-ns-candidates
86 | "Returns candidate data for all referred macro namespaces (and their aliases) in
87 | context-ns."
88 | [env context-ns extra-metadata]
89 | (for [[alias ns] (ana/macro-ns-aliases env context-ns)]
90 | (candidate-data alias
91 | (when-not (= alias ns)
92 | ns)
93 | :namespace
94 | ;; given macros are Clojure code we can simply find-ns
95 | ;; the meta should probably be in the compiler env instead
96 | (ana/ns-meta ns)
97 | extra-metadata)))
98 |
99 | (defn- referred-var-candidates
100 | "Returns candidate data for all referred vars in context-ns."
101 | [env context-ns extra-metadata]
102 | (for [[refer qualified-sym] (ana/referred-vars env context-ns)
103 | :let [ns (namespace qualified-sym)
104 | meta (ana/qualified-symbol-meta env qualified-sym)
105 | type (var->type meta)]]
106 | (candidate-data refer ns type meta extra-metadata)))
107 |
108 | (defn- referred-macro-candidates
109 | "Returns candidate data for all referred macros in context-ns."
110 | [env context-ns extra-metadata]
111 | (for [[refer qualified-sym] (ana/referred-macros env context-ns)
112 | :let [ns (namespace qualified-sym)
113 | meta (ana/macro-meta env qualified-sym)]]
114 | (candidate-data refer ns :macro meta extra-metadata)))
115 |
116 | (defn- var-candidates
117 | [vars extra-metadata]
118 | (for [[name meta] vars
119 | :let [qualified-name (:name meta)
120 | ns (some-> qualified-name namespace)
121 | type (var->type meta)]]
122 | (candidate-data name ns type meta extra-metadata)))
123 |
124 | (defn- ns-var-candidates
125 | "Returns candidate data for all vars defined in ns."
126 | [env ns extra-metadata]
127 | (var-candidates (ana/ns-vars env ns) extra-metadata))
128 |
129 | (defn- core-var-candidates
130 | "Returns candidate data for all cljs.core vars visible in context-ns."
131 | [env ns extra-metadata]
132 | (var-candidates (ana/core-vars env ns) extra-metadata))
133 |
134 | (defn- macro-candidates
135 | [macros extra-metadata]
136 | (for [[name var] macros
137 | :let [meta (ana/var-meta var)
138 | ns (:ns meta)]]
139 | (candidate-data name ns :macro meta extra-metadata)))
140 |
141 | (defn- core-macro-candidates
142 | "Returns candidate data for all cljs.core macros visible in ns."
143 | [env ns extra-metadata]
144 | (macro-candidates (ana/core-macros env ns) extra-metadata))
145 |
146 | (defn- import-candidates
147 | "Returns candidate data for all imports in context-ns."
148 | [env context-ns]
149 | (flatten
150 | (for [[import qualified-name] (ana/imports env context-ns)]
151 | [(candidate-data import nil :class)
152 | (candidate-data qualified-name nil :class)])))
153 |
154 | (defn- keyword-candidates
155 | "Returns candidate data for all keyword constants in the environment."
156 | [env]
157 | (map #(candidate-data % nil :keyword) (ana/keyword-constants env)))
158 |
159 | (defn- namespaced-keyword-candidates
160 | "Returns all namespaced keywords defined in context-ns."
161 | [env context-ns]
162 | (when context-ns
163 | (for [kw (ana/keyword-constants env)
164 | :when (= context-ns (ana/as-sym (namespace kw)))]
165 | (candidate-data (str "::" (name kw)) context-ns :keyword))))
166 |
167 | (defn- referred-namespaced-keyword-candidates
168 | "Returns all namespaced keywords referred in context-ns."
169 | [env context-ns]
170 | (when context-ns
171 | (let [aliases (->> (ana/ns-aliases env context-ns)
172 | (filter (fn [[k v]] (not= k v)))
173 | (into {})
174 | (set/map-invert))]
175 | (for [kw (ana/keyword-constants env)
176 | :let [ns (ana/as-sym (namespace kw))
177 | alias (get aliases ns)]
178 | :when alias]
179 | (candidate-data (str "::" alias "/" (name kw)) ns :keyword)))))
180 |
181 | (defn- unscoped-candidates
182 | "Returns all non-namespace-qualified potential candidates in context-ns."
183 | [env context-ns extra-metadata]
184 | (concat special-form-candidates
185 | (all-ns-candidates env extra-metadata)
186 | (ns-candidates env context-ns extra-metadata)
187 | (macro-ns-candidates env context-ns extra-metadata)
188 | (referred-var-candidates env context-ns extra-metadata)
189 | (referred-macro-candidates env context-ns extra-metadata)
190 | (ns-var-candidates env context-ns extra-metadata)
191 | (core-var-candidates env context-ns extra-metadata)
192 | (core-macro-candidates env context-ns extra-metadata)
193 | (import-candidates env context-ns)
194 | (keyword-candidates env)
195 | (namespaced-keyword-candidates env context-ns)
196 | (referred-namespaced-keyword-candidates env context-ns)))
197 |
198 | (defn- prefix-candidate
199 | [prefix candidate-data]
200 | (let [candidate (:candidate candidate-data)
201 | prefixed-candidate (str prefix "/" candidate)]
202 | (assoc candidate-data :candidate prefixed-candidate)))
203 |
204 | (defn- prefix-candidates
205 | [prefix candidates]
206 | (map #(prefix-candidate prefix %) candidates))
207 |
208 | (defn- ->ns
209 | [env symbol-ns context-ns]
210 | (if (ana/find-ns env symbol-ns)
211 | symbol-ns
212 | (ana/ns-alias env symbol-ns context-ns)))
213 |
214 | (defn- ->macro-ns
215 | [env symbol-ns context-ns]
216 | (if (= symbol-ns 'cljs.core)
217 | symbol-ns
218 | (ana/macro-ns-alias env symbol-ns context-ns)))
219 |
220 | (defn- ns-public-var-candidates
221 | "Returns candidate data for all public vars defined in ns."
222 | [env ns extra-metadata]
223 | (var-candidates (ana/public-vars env ns) extra-metadata))
224 |
225 | (defn- ns-macro-candidates
226 | "Returns candidate data for all macros defined in ns."
227 | [env ns extra-metadata]
228 | (-> env
229 | (ana/public-macros ns)
230 | (macro-candidates extra-metadata)))
231 |
232 | (defn- scoped-candidates
233 | "Returns all candidates for the namespace of sym. Sym must be
234 | namespace-qualified. Macro candidates are included if the namespace has its
235 | macros required in context-ns."
236 | [env sym context-ns extra-metadata]
237 | (let [sym-ns (-> sym ana/as-sym ana/namespace-sym)
238 | computed-ns (->ns env sym-ns context-ns)
239 | macro-ns (->macro-ns env sym-ns context-ns)
240 | sym-ns-as-string (str sym-ns)]
241 | (mapcat #(prefix-candidates sym-ns-as-string %)
242 | [(ns-public-var-candidates env computed-ns extra-metadata)
243 | (when macro-ns
244 | (ns-macro-candidates env macro-ns extra-metadata))])))
245 |
246 | (defn- potential-candidates
247 | "Returns all candidates for sym. If sym is namespace-qualified, the candidates
248 | for that namespace will be returned (including macros if the namespace has its
249 | macros required in context-ns). Otherwise, all non-namespace-qualified
250 | candidates for context-ns will be returned."
251 | [env context-ns ^String sym extra-metadata]
252 | (if (or (= (.indexOf sym "/") -1) (.startsWith sym ":"))
253 | (unscoped-candidates env context-ns extra-metadata)
254 | (scoped-candidates env sym context-ns extra-metadata)))
255 |
256 | (defn- distinct-candidates
257 | "Filters candidates to have only one entry for each value of :candidate. If
258 | multiple such entries do exist, the first occurrence is used."
259 | [candidates]
260 | (map first (vals (group-by :candidate candidates))))
261 |
262 | (defn- candidate-match?
263 | [candidate prefix]
264 | (.startsWith ^String (:candidate candidate) prefix))
265 |
266 | (defn plain-symbol?
267 | "Tests if prefix is a symbol with no / (qualified), : (keyword) and
268 | . (segmented namespace)."
269 | [s]
270 | (re-matches #"[^\/\:\.]+" s))
271 |
272 | (defn nscl-symbol?
273 | "Tests if prefix looks like a namespace or classname."
274 | [x]
275 | (re-matches #"[^\/\:\.][^\/\:]+" x))
276 |
277 | (def ^:dynamic *compiler-env* nil)
278 |
279 | (defn- candidates* [prefix context-ns]
280 | (->> (potential-candidates *compiler-env* context-ns prefix *extra-metadata*)
281 | (distinct-candidates)
282 | (filter #(candidate-match? % prefix))))
283 |
284 | (defn candidates
285 | "Returns a sequence of candidate data for completions matching the given
286 | prefix string and options.
287 |
288 | It requires the compliment.sources.cljs/*compiler-env* var to be dynamically
289 | bound to the ClojureScript compiler env."
290 | [prefix ns _context]
291 | (let [context-ns (try
292 | (ns-name ns)
293 | (catch Exception _
294 | nil))]
295 | (candidates* prefix context-ns)))
296 |
297 | (defn generate-docstring
298 | "Generates a docstring from a given var metadata.
299 |
300 | Copied from `cljs.repl` with some minor modifications."
301 | [m]
302 | (binding [*out* (StringWriter.)]
303 | (println "-------------------------")
304 | (println (or (:spec m) (str (when-let [ns (:ns m)] (str ns "/")) (:name m))))
305 | (when (:protocol m)
306 | (println "Protocol"))
307 | (cond
308 | (:forms m) (doseq [f (:forms m)]
309 | (println " " f))
310 | (:arglists m) (let [arglists (:arglists m)]
311 | (if (or (:macro m)
312 | (:repl-special-function m))
313 | (prn arglists)
314 | (prn
315 | (if (= 'quote (first arglists))
316 | (second arglists)
317 | arglists)))))
318 | (if (:special-form m)
319 | (do
320 | (println "Special Form")
321 | (println " " (:doc m))
322 | (if (contains? m :url)
323 | (when (:url m)
324 | (println (str "\n Please see http://clojure.org/" (:url m))))
325 | (println (str "\n Please see http://clojure.org/special_forms#"
326 | (:name m)))))
327 | (do
328 | (when (:macro m)
329 | (println "Macro"))
330 | (when (:spec m)
331 | (println "Spec"))
332 | (when (:repl-special-function m)
333 | (println "REPL Special Function"))
334 | (println " " (:doc m))
335 | (when (:protocol m)
336 | (doseq [[name {:keys [doc arglists]}] (:methods m)]
337 | (println)
338 | (println " " name)
339 | (println " " arglists)
340 | (when doc
341 | (println " " doc))))
342 | ;; Specs are handled separately in cider-nrepl
343 | ;;
344 | ;; (when n
345 | ;; (when-let [fnspec (spec/get-spec (symbol (str (ns-name n)) (name nm)))]
346 | ;; (print "Spec")
347 | ;; (doseq [role [:args :ret :fn]]
348 | ;; (when-let [spec (get fnspec role)]
349 | ;; (print (str "\n " (name role) ":") (spec/describe spec))))))
350 | ))
351 | (str *out*)))
352 |
353 | (defn doc
354 | [s ns]
355 | (let [ns-sym (some-> ns ns-name)]
356 | (some->
357 | (cond
358 | ;; This is needed because compliment defaults to 'user in the absence of
359 | ;; a ns. Additionally, in order to preserve the Clojure's behavior we
360 | ;; try against cljs.core if nothing is found for cljs.user
361 | (or (= ns-sym 'user) (= ns-sym 'cljs.user))
362 | (or (ana/qualified-symbol-meta *compiler-env* (symbol "cljs.user" s))
363 | (ana/macro-meta *compiler-env* (symbol "cljs.user" s))
364 | (ana/qualified-symbol-meta *compiler-env* (symbol "cljs.core" s))
365 | (ana/macro-meta *compiler-env* (symbol "cljs.core" s)))
366 |
367 | (plain-symbol? s) (let [ns-sym (cond
368 | (nil? ns) 'cljs.core
369 | (= ns 'user) 'cljs.user
370 | :else (ns-name ns))
371 | qualified-sym (symbol (str ns-sym) s)]
372 | (or (ana/qualified-symbol-meta *compiler-env* qualified-sym)
373 | (ana/macro-meta *compiler-env* qualified-sym)))
374 | (nscl-symbol? s) (-> s symbol ana/ns-meta)
375 | :else nil)
376 | (not-empty)
377 | (generate-docstring))))
378 |
379 | (defsource ::cljs-source
380 | :candidates #'candidates
381 | :doc #'doc)
382 |
--------------------------------------------------------------------------------
/src/main/suitable/compliment/sources/cljs/analysis.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.compliment.sources.cljs.analysis
2 | (:require [clojure.string :as str])
3 | (:refer-clojure :exclude [find-ns all-ns ns-aliases]))
4 |
5 | (def NSES :cljs.analyzer/namespaces)
6 |
7 | (defn as-sym [x]
8 | (some-> x symbol))
9 |
10 | (defn namespace-sym
11 | "Return the namespace of a fully qualified symbol if possible.
12 |
13 | It leaves the symbol untouched if not."
14 | [sym]
15 | (if-let [ns (and sym (namespace sym))]
16 | (as-sym ns)
17 | sym))
18 |
19 | (defn name-sym
20 | "Return the name of a fully qualified symbol if possible.
21 |
22 | It leaves the symbol untouched if not."
23 | [sym]
24 | (if-let [n (and sym (name sym))]
25 | (as-sym n)
26 | sym))
27 |
28 | (defn all-ns
29 | [env]
30 | (->> (NSES env)
31 | ;; recent CLJS versions include data about macro namespaces in the
32 | ;; compiler env, but we should not include them in completions or pass
33 | ;; them to format-ns unless they're actually required
34 | (into {} (filter (fn [[_ ns]]
35 | (not (and (contains? ns :macros)
36 | (= 1 (count ns)))))))))
37 |
38 | (defn find-ns
39 | [env ns]
40 | (get (all-ns env) ns))
41 |
42 | (defn add-ns-macros
43 | "Append $macros to the input symbol"
44 | [sym]
45 | (some-> sym
46 | (str "$macros")
47 | symbol))
48 |
49 | (defn remove-macros
50 | "Remove $macros from the input symbol"
51 | [sym]
52 | (some-> sym
53 | str
54 | (str/replace #"\$macros" "")
55 | symbol))
56 |
57 | ;; Code adapted from clojure-complete (http://github.com/ninjudd/clojure-complete)
58 |
59 | (defn imports
60 | "Returns a map of [import-name] to [ns-qualified-import-name] for all imports
61 | in the given namespace."
62 | [env ns]
63 | (:imports (find-ns env ns)))
64 |
65 | (defn ns-aliases
66 | "Returns a map {ns-name-or-alias ns-name} for the given namespace."
67 | [env ns]
68 | (when-let [found (find-ns env ns)]
69 | (let [imports (:imports found)]
70 | (into {}
71 | (comp cat (remove #(contains? imports (key %))))
72 | (vals (select-keys found
73 | [:requires
74 | :as-aliases
75 | :reader-aliases]))))))
76 |
77 | (defn macro-ns-aliases
78 | "Returns a map of [macro-ns-name-or-alias] to [macro-ns-name] for the given namespace."
79 | [env ns]
80 | (:require-macros (find-ns env ns)))
81 |
82 | (defn- expand-refer-map
83 | [m]
84 | (into {} (for [[k v] m] [k (symbol (str v "/" k))])))
85 |
86 | (defn referred-vars
87 | "Returns a map of [var-name] to [ns-qualified-var-name] for all referred vars
88 | in the given namespace."
89 | [env ns]
90 | (->> (find-ns env ns)
91 | :uses
92 | expand-refer-map))
93 |
94 | (defn referred-macros
95 | "Returns a map of [macro-name] to [ns-qualified-macro-name] for all referred
96 | macros in the given namespace."
97 | [env ns]
98 | (->> (find-ns env ns)
99 | :use-macros
100 | expand-refer-map))
101 |
102 | (defn ns-alias
103 | "If sym is an alias to, or the name of, a namespace referred to in ns, returns
104 | the name of the namespace; else returns nil."
105 | [env sym ns]
106 | (get (ns-aliases env ns) (as-sym sym)))
107 |
108 | (defn macro-ns-alias
109 | "If sym is an alias to, or the name of, a macro namespace referred to in ns,
110 | returns the name of the macro namespace; else returns nil."
111 | [env sym ns]
112 | (get (macro-ns-aliases env ns) (as-sym sym)))
113 |
114 | (defn- public?
115 | [[_ var]]
116 | (not (:private var)))
117 |
118 | (defn- named?
119 | [[_ var]]
120 | (not (:anonymous var)))
121 |
122 | (defn- foreign-protocol?
123 | [[_ var]]
124 | (and (:impls var)
125 | (not (:protocol-symbol var))))
126 |
127 | (defn- macro?
128 | [[_ var]]
129 | (:macro (meta var)))
130 |
131 | (defn ns-vars
132 | "Returns a list of the vars declared in the ns."
133 | [env ns]
134 | (->> (find-ns env ns)
135 | :defs
136 | (filter (every-pred named? (complement foreign-protocol?)))
137 | (into {})))
138 |
139 | (defn public-vars
140 | "Returns a list of the public vars declared in the ns."
141 | [env ns]
142 | (->> (find-ns env ns)
143 | :defs
144 | (filter (every-pred named? public? (complement foreign-protocol?)))
145 | (into {})))
146 |
147 | (defn public-macros
148 | "Given a namespace return all the public var analysis maps. Analagous to
149 | clojure.core/ns-publics but returns var analysis maps not vars.
150 |
151 | Inspired by the ns-publics in cljs.analyzer.api."
152 | [_env ns]
153 | {:pre [(symbol? ns)]}
154 | (when (and ns (clojure.core/find-ns ns))
155 | (->> (ns-publics ns)
156 | (filter macro?)
157 | (into {}))))
158 |
159 | (defn core-vars
160 | "Returns a list of cljs.core vars visible to the ns."
161 | [env ns]
162 | (let [vars (public-vars env 'cljs.core)
163 | excludes (:excludes (find-ns env ns))]
164 | (apply dissoc vars excludes)))
165 |
166 | (defn core-macros
167 | "Returns a list of cljs.core macros visible to the ns."
168 | [env ns]
169 | (let [macros (public-macros env 'cljs.core)
170 | excludes (:excludes (find-ns env ns))]
171 | (apply dissoc macros excludes)))
172 |
173 | (def ^:private language-keywords
174 | #{:require :require-macros :import
175 | :refer :refer-macros :include-macros
176 | :refer-clojure :exclude
177 | :keys :strs :syms
178 | :as :as-alias :or
179 | :pre :post
180 | :let :when :while
181 |
182 | ;; reader conditionals
183 | :clj :cljs :default
184 |
185 | ;; common meta keywords
186 | :private :tag :static
187 | :doc :author :arglists
188 | :added :const
189 |
190 | ;; spec keywords
191 | :req :req-un :opt :opt-un
192 | :args :ret :fn
193 |
194 | ;; misc
195 | :keywordize-keys :else :gen-class})
196 |
197 | (defn keyword-constants
198 | "Returns a list of both keyword constants in the environment and
199 | language specific ones."
200 | [env]
201 | (into language-keywords
202 | (filter keyword?)
203 | (keys (:cljs.analyzer/constant-table env))))
204 |
205 | ;; grabbing directly from cljs.analyzer.api
206 |
207 | (defn ns-interns-from-env
208 | "Given a namespace return all the var analysis maps. Analagous to
209 | clojure.core/ns-interns but returns var analysis maps not vars.
210 |
211 | Directly from cljs.analyzer.api."
212 | [env ns]
213 | {:pre [(symbol? ns)]}
214 | (merge
215 | (get-in env [NSES ns :macros])
216 | (get-in env [NSES ns :defs])))
217 |
218 | (defn sanitize-ns
219 | "Add :ns from :name if missing."
220 | [m]
221 | (cond-> m
222 | (or (:name m) (:ns m)) (-> (assoc :ns (or (:ns m) (:name m)))
223 | (update :ns namespace-sym)
224 | (update :name name-sym))))
225 |
226 | (defn ns-obj?
227 | "Return true if n is a namespace object"
228 | [ns]
229 | (instance? clojure.lang.Namespace ns))
230 |
231 | (defn var-meta
232 | "Return meta for the var, we wrap it in order to support both JVM and
233 | self-host."
234 | [var]
235 | (cond-> {}
236 | (map? var) (merge var)
237 | (var? var) (-> (merge (meta var))
238 | (update :ns #(cond-> % (ns-obj? %) ns-name)))
239 | true sanitize-ns))
240 |
241 | (defn qualified-symbol-meta
242 | "Given a namespace-qualified var name, gets the analyzer metadata for
243 | that var."
244 | [env sym]
245 | {:pre [(symbol? sym)]}
246 | (let [ns (find-ns env (namespace-sym sym))]
247 | (some-> (:defs ns)
248 | (get (name-sym sym))
249 | var-meta)))
250 |
251 | (defn ns-meta
252 | [ns]
253 | (if-not (symbol? ns)
254 | {}
255 | (meta (clojure.core/find-ns ns))))
256 |
257 | (defn macro-meta
258 | [_env qualified-sym]
259 | (some-> (find-var qualified-sym) var-meta))
260 |
--------------------------------------------------------------------------------
/src/main/suitable/compliment/sources/cljs/ast.cljc:
--------------------------------------------------------------------------------
1 | (ns suitable.compliment.sources.cljs.ast
2 | (:require [clojure.pprint :refer [pprint *print-right-margin*]]
3 | [clojure.zip :as z])
4 | #?(:clj (:import [clojure.lang IPersistentList IPersistentMap IPersistentVector ISeq])))
5 |
6 | (def V #?(:clj IPersistentVector
7 | :cljs PersistentVector))
8 | (def M #?(:clj IPersistentMap
9 | :cljs PersistentArrayMap))
10 | (def L #?(:clj IPersistentList
11 | :cljs List))
12 | (def S ISeq)
13 |
14 | ;; Thx @ Alex Miller! http://www.ibm.com/developerworks/library/j-treevisit/
15 | (defmulti tree-branch? type)
16 | (defmethod tree-branch? :default [_] false)
17 | (defmethod tree-branch? V [v] (not-empty v))
18 | (defmethod tree-branch? M [m] (not-empty m))
19 | (defmethod tree-branch? L [_l] true)
20 | (defmethod tree-branch? S [_s] true)
21 | (prefer-method tree-branch? L S)
22 |
23 | (defmulti tree-children type)
24 | (defmethod tree-children V [v] v)
25 | (defmethod tree-children M [m] (->> m seq (apply concat)))
26 | (defmethod tree-children L [l] l)
27 | (defmethod tree-children S [s] s)
28 | (prefer-method tree-children L S)
29 |
30 | (defmulti tree-make-node (fn [node _children] (type node)))
31 | (defmethod tree-make-node V [_v children]
32 | (vec children))
33 | (defmethod tree-make-node M [_m children]
34 | (apply hash-map children))
35 | (defmethod tree-make-node L [_ children]
36 | children)
37 | (defmethod tree-make-node S [_node children]
38 | (apply list children))
39 | (prefer-method tree-make-node L S)
40 |
41 | (defn tree-zipper [node]
42 | (z/zipper tree-branch? tree-children tree-make-node node))
43 |
44 | (defn print-tree
45 | "for debugging"
46 | [node]
47 | (let [all (take-while (complement z/end?) (iterate z/next (tree-zipper node)))]
48 | (binding [*print-right-margin* 20]
49 | (pprint
50 | (->> all
51 | (map z/node) (zipmap (range))
52 | sort)))))
53 |
--------------------------------------------------------------------------------
/src/main/suitable/figwheel/main.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.figwheel.main
2 | (:require figwheel.main
3 | ;; requiring this will override the rebel-readline completion
4 | ;; service
5 | suitable.hijack-rebel-readline-complete))
6 |
7 | (def extra-figwheel-args ["--compile-opts" "{:preloads [suitable.js-introspection]}"])
8 |
9 | (defn -main [& args]
10 | (apply figwheel.main/-main (concat extra-figwheel-args args)))
11 |
--------------------------------------------------------------------------------
/src/main/suitable/hijack_rebel_readline_complete.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.hijack-rebel-readline-complete
2 | (:require [cljs-tooling.complete :as cljs-complete]
3 | cljs.env
4 | cljs.repl
5 | [rebel-readline.cljs.service.local :as rebel-cljs]
6 | [rebel-readline.clojure.line-reader :as clj-reader]
7 | [suitable.js-completions :refer [cljs-completions]]
8 | [suitable.utils :refer [wrapped-cljs-repl-eval]]))
9 |
10 |
11 | ;; This is a rather huge hack. rebel-readline doesn't really have any hooks for
12 | ;; other service provider to add to existing rebel-readline services. So what
13 | ;; we're doing here is to boldly redefine
14 | ;; `rebel-readline.cljs.service.local/-complete`. This is clearly very brittle
15 | ;; but the only way I found to piggieback our runtime completions without too
16 | ;; much setup code for the user to implement.
17 |
18 | (defmethod clj-reader/-complete ::rebel-cljs/service [_ word {:keys [ns context]}]
19 | (let [options (cond-> nil
20 | ns (assoc :current-ns ns))
21 | renv cljs.repl/*repl-env*
22 | cenv @cljs.env/*compiler*
23 | suitables (cljs-completions
24 | (wrapped-cljs-repl-eval renv cenv)
25 | word {:ns ns :context context})
26 | completions (cljs-complete/completions cenv word options)]
27 | (concat suitables completions)))
28 |
--------------------------------------------------------------------------------
/src/main/suitable/js_completions.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.js-completions
2 | (:refer-clojure :exclude [replace])
3 | (:require
4 | [clojure.pprint :refer [cl-format]]
5 | [clojure.string :refer [replace split starts-with?]]
6 | [clojure.zip :as zip]
7 | [suitable.ast :refer [tree-zipper]]))
8 |
9 | (def debug? false)
10 |
11 | ;; mranderson-friendly
12 | (defn js-introspection-ns []
13 | (-> ::_ namespace (replace ".js-completions" ".js-introspection")))
14 |
15 | (defn js-properties-of-object
16 | "Returns the properties of the object we get by evaluating `obj-expr` filtered
17 | by all those that start with `prefix`."
18 | ([cljs-eval-fn ns obj-expr]
19 | (js-properties-of-object cljs-eval-fn ns obj-expr nil))
20 | ([cljs-eval-fn ns obj-expr prefix]
21 | (try
22 | ;; :Not using a single expressiont / eval call here like
23 | ;; (do (require ...) (runtime ...))
24 | ;; to avoid
25 | ;; Compile Warning: Use of undeclared Var
26 | ;; suitable.js-introspection/property-names-and-types
27 | (let [template (str "("
28 | (js-introspection-ns)
29 | "/property-names-and-types ~A ~S)")
30 | code (cl-format nil template obj-expr prefix)]
31 | (cljs-eval-fn ns (str "(require '" (js-introspection-ns) ")"))
32 | (cljs-eval-fn ns code))
33 | (catch Exception e {:error e}))))
34 |
35 | (defn find-prefix
36 | "Tree search for the symbol '__prefix. Returns a zipper."
37 | [form]
38 | (loop [node (tree-zipper form)]
39 | (if (= '__prefix__ (zip/node node))
40 | node
41 | (when-not (zip/end? node)
42 | (recur (zip/next node))))))
43 |
44 | (defn thread-form?
45 | "True if form looks like the name of a thread macro."
46 | [form]
47 | (->> form
48 | str
49 | (re-find #"^->")
50 | some?))
51 |
52 | (defn doto-form? [form]
53 | (#{'doto 'cljs.core/doto} form))
54 |
55 | (defn js-interop? [x]
56 | (boolean (or (and (symbol? x)
57 | (-> x namespace (= "js")))
58 | (and (seq? x)
59 | (-> x first symbol?)
60 | (-> x first namespace (= "js"))))))
61 |
62 | (defn expr-for-parent-obj
63 | "Given the `context` and `symbol` of a completion request,
64 | will try to find an expression that evaluates to the object being accessed."
65 | [symbol context]
66 | (when-let [form (if (string? context)
67 | (try
68 | (with-in-str context (read *in* nil nil))
69 | (catch Exception _e
70 | (when debug?
71 | (binding [*out* *err*]
72 | (cl-format true "[suitable] Error reading context: ~s" context)))))
73 | context)]
74 | (let [prefix (find-prefix form)
75 | left-sibling (zip/left prefix)
76 | first? (nil? left-sibling)
77 | first-sibling (when-not first?
78 | (some-> prefix zip/leftmost zip/node))
79 | first-sibling-in-parent (some-> prefix zip/up zip/leftmost zip/node)
80 | relevant-first-member (if first?
81 | first-sibling-in-parent
82 | first-sibling)
83 | threaded? (thread-form? relevant-first-member)
84 | doto? (doto-form? relevant-first-member)
85 | base (cond-> prefix
86 | (or (and threaded? first?)
87 | (and doto? first?))
88 | zip/up)
89 | ;; the operand is the element over which the main -> (or doto, .., ., .-, etc) is being applied
90 | ;; e.g. for (-> x f g h), it's x
91 | ;; and for (-> x f (__prefix__)), it's also x
92 | operand (some-> base zip/leftmost zip/right zip/node)
93 | dot-fn? (starts-with? symbol ".")]
94 |
95 | (letfn [(with-type [type likely-interop? maybe-expr]
96 | (when maybe-expr
97 | {:type type
98 | :js-interop? (or likely-interop? ;; .., doto, . and .- are mainly used for interop, so we can infer the intent is for interop.
99 | ;; NOTE: detection could be extended to also detect when a given symbol `foo`
100 | ;; maps to a JS library that was `require`d with "string requires".
101 | ;; The cljs analyzer could be used for that.
102 | (js-interop? operand))
103 | :obj-expr maybe-expr}))]
104 | (cond
105 | (nil? prefix) nil
106 |
107 | ;; is it a threading macro?
108 | threaded?
109 | (with-type :-> false (if first?
110 | ;; parent is the thread
111 | (some-> prefix zip/up zip/lefts str)
112 | ;; thread on same level
113 | (-> prefix zip/lefts str)))
114 |
115 | doto?
116 | (with-type :doto true (if first?
117 | ;; parent is the thread
118 | (some-> prefix zip/up zip/leftmost zip/right zip/node str)
119 | ;; thread on same level
120 | (some-> prefix zip/leftmost zip/right zip/node str)))
121 |
122 | ;; a .. form: if __prefix__ is a prop deeper than one level we need the ..
123 | ;; expr up to that point. If just the object that is accessed is left of
124 | ;; prefix, we can take that verbatim.
125 | ;; (.. js/console log) => js/console
126 | ;; (.. js/console -memory -jsHeapSizeLimit) => (.. js/console -memory)
127 | (and first-sibling (#{"." ".."} (str first-sibling)) left-sibling)
128 | (with-type :.. true (let [lefts (-> prefix zip/lefts)]
129 | (if (<= (count lefts) 2)
130 | (str (last lefts))
131 | (str lefts))))
132 |
133 | ;; (.. js/window -console (log "foo")) => (.. js/window -console)
134 | (and first? (some-> prefix zip/up zip/leftmost zip/node str (= "..")))
135 | (with-type :.. true (let [lefts (-> prefix zip/up zip/lefts)]
136 | (if (<= (count lefts) 2)
137 | (str (last lefts))
138 | (str lefts))))
139 |
140 | ;; simple (.foo bar)
141 | (and first? dot-fn?)
142 | (with-type :. true (some-> prefix zip/right zip/node str)))))))
143 |
144 | (def global-expr-re #"^js/((?:[^\.]+\.)*)([^\.]*)$")
145 | (def dot-dash-prefix-re #"^\.-?")
146 | (def dash-prefix-re #"^-")
147 | (def dot-prefix-re #"\.")
148 |
149 | (defn analyze-symbol-and-context
150 | "Build a configuration that we can use to fetch the properties from an object
151 | that is the result of some `obj-expr` when evaled and that is used to convert
152 | those properties into candidates for completion."
153 | [symbol context]
154 | (if (starts-with? symbol "js/")
155 | ;; symbol is a global like js/console or global/property like js/console.log
156 | (let [[_ dotted-obj-expr prefix] (re-matches global-expr-re symbol)
157 | obj-expr-parts (keep not-empty (split dotted-obj-expr dot-prefix-re))
158 | ;; builds an expr like
159 | ;; "(this-as this (.. this -window))" for symbol = "js/window.console"
160 | ;; or "(this-as this this)" for symbol = "js/window"
161 | obj-expr (cl-format nil "(this-as this ~[this~:;(.. this ~{-~A~^ ~})~])"
162 | (count obj-expr-parts) obj-expr-parts)]
163 | {:prefix prefix
164 | :js-interop? true
165 | :prepend-to-candidate (str "js/" dotted-obj-expr)
166 | :vars-have-dashes? false
167 | :obj-expr obj-expr
168 | :type :global})
169 |
170 | ;; symbol is just a property name embedded in some expr
171 | (let [{:keys [type] :as expr-and-type} (expr-for-parent-obj symbol context)]
172 | (assoc expr-and-type
173 | :prepend-to-candidate (if (starts-with? symbol ".") "." "")
174 | :prefix (case type
175 | :.. (replace symbol dash-prefix-re "")
176 | (replace symbol dot-dash-prefix-re ""))
177 | :vars-have-dashes? true))))
178 |
179 | (defn cljs-completions
180 | "Given some context (the toplevel form that has changed) and a symbol string
181 | that represents the last typed input, we try to find out if the context/symbol
182 | are object access (property access or method call). If so, we try to extract a
183 | form that we can evaluate to get the object that is accessed. If we get the
184 | object, we enumerate it's properties and methods and generate a list of
185 | matching completions for those.
186 |
187 | The arguments to this function are
188 |
189 | 1. `cljs-eval-fn`: a function that given a namespace (as string) and cljs
190 | code (string) will evaluate it and return the value as a clojure object. See
191 | `suitable.middleware/cljs-dynamic-completion-handler` for how to
192 | setup an eval function with nREPL.
193 |
194 | The last two arguments mirror the interface of `compliment.core/completions`
195 | from https://github.com/alexander-yakushev/compliment:
196 |
197 | 2. A symbol (as string) to complete, basically the prefix.
198 |
199 | 3. An options map that should have at least the keys :ns and :context. :ns is
200 | the name (string) of the namespace the completion request is coming
201 | from. :context is a s-expression (as string) of the toplevel form the symbol
202 | comes from, the symbol being replaced by \"__prefix__\". See the compliment
203 | library for details on this format.
204 | Currently unsupported options that compliment implements
205 | are :extra-metadata :sort-order and :plain-candidates."
206 | [cljs-eval-fn symbol {:keys [ns context]}]
207 | (when (and symbol (not= "nil" symbol))
208 | (let [{:keys [prefix prepend-to-candidate vars-have-dashes? obj-expr type js-interop?]}
209 | (analyze-symbol-and-context symbol context)
210 | global? (#{:global} type)]
211 | ;; ONLY evaluate forms that are detected as js interop,
212 | ;; it's Suitable's promise per its README.
213 | ;; There was issue #30 that broke this expectation at some point.
214 | (when js-interop?
215 | (when-let [{error :error properties :value} (and obj-expr (js-properties-of-object cljs-eval-fn ns obj-expr prefix))]
216 | (if (seq error)
217 | (when debug?
218 | (binding [*out* *err*]
219 | (println "[suitable] error in suitable cljs-completions:" error)))
220 | (for [{:keys [name type]} properties
221 | :let [maybe-dash (if (and vars-have-dashes? (= "var" type)) "-" "")
222 | candidate (str prepend-to-candidate maybe-dash name)]
223 | :when (starts-with? candidate symbol)]
224 | {:type type :candidate candidate :ns (if global? "js" obj-expr)})))))))
225 |
--------------------------------------------------------------------------------
/src/main/suitable/js_introspection.cljs:
--------------------------------------------------------------------------------
1 | (ns suitable.js-introspection
2 | (:require [clojure.string :refer [starts-with?]]
3 | [goog.object :refer [get] :rename {get oget}]))
4 |
5 | (def own-property-descriptors
6 | (if (js-in "getOwnPropertyDescriptors" js/Object)
7 | ;; ES 6+ version
8 | (fn [obj] (js/Object.getOwnPropertyDescriptors obj))
9 | ;; ES 5.1 version
10 | (fn [obj] (->> obj
11 | js/Object.getOwnPropertyNames
12 | (map (fn [key] [key (js/Object.getOwnPropertyDescriptor obj key)]))
13 | (into {})
14 | clj->js))))
15 |
16 | (defn properties-by-prototype [obj]
17 | (loop [obj obj protos []]
18 | (if obj
19 | (recur
20 | (js/Object.getPrototypeOf obj)
21 | (conj protos {:obj obj :props (own-property-descriptors obj)}))
22 | protos)))
23 |
24 | (defn property-names-and-types
25 | ([js-obj] (property-names-and-types js-obj nil))
26 | ([js-obj prefix]
27 | (let [seen (volatile! #{})]
28 | (for [[i {:keys [_obj props]}] (map-indexed vector (properties-by-prototype js-obj))
29 | key (js-keys props)
30 | :when (and (not (get @seen key))
31 | (if (or (= "[object String]" (js/Object.prototype.toString.call js-obj))
32 | (js/Array.isArray js-obj))
33 | (not (oget (oget props key) "enumerable"))
34 | true)
35 | (or (empty? prefix)
36 | (starts-with? key prefix)))]
37 | (let [prop (oget props key)]
38 | (vswap! seen conj key)
39 | {:name key
40 | :hierarchy i
41 | :type (try
42 | (if-let [value (or (oget prop "value")
43 | (-> prop (oget "get")
44 | (apply [])))]
45 | (if (fn? value) "function" "var")
46 | "var")
47 | (catch js/Error _e "var"))})))))
48 |
49 | (comment
50 | (require '[cljs.pprint :refer [pprint]])
51 | ;; (-> js/console property-names-and-types pprint)
52 | (-> js/document.body property-names-and-types pprint)
53 |
54 | (let [obj (new (fn [x] (this-as this (goog.object/set this "foo" 23))))]
55 | (pprint (property-names-and-types obj)))
56 |
57 | (oget js/console "log")
58 | (-> js/console property-names-and-types pprint)
59 | (-> js/window property-names-and-types pprint))
60 |
--------------------------------------------------------------------------------
/src/main/suitable/middleware.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.middleware
2 | (:require [suitable.complete-for-nrepl :refer [complete-for-nrepl]]))
3 |
4 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
5 | ;; rk 2019-07-23: this is adapted from refactor_nrepl.middleware
6 | ;; Compatibility with the legacy tools.nrepl and the new nREPL 0.4.x.
7 | ;; The assumption is that if someone is using old lein repl or boot repl
8 | ;; they'll end up using the tools.nrepl, otherwise the modern one.
9 | (when-not (resolve 'set-descriptor!)
10 | (if (find-ns 'clojure.tools.nrepl)
11 | (require
12 | '[clojure.tools.nrepl.middleware :refer [set-descriptor!]]
13 | '[nrepl.misc :refer [response-for]]
14 | '[clojure.tools.nrepl.transport :as transport])
15 | (require
16 | '[nrepl.middleware :refer [set-descriptor!]]
17 | '[nrepl.misc :refer [response-for]]
18 | '[nrepl.transport :as transport])))
19 |
20 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
21 |
22 | (defn- completion-answer
23 | "Creates an answer message with computed completions. Note that no done state is
24 | set in the message as we expect the default handler to finish the completion
25 | response."
26 | ([msg completions]
27 | (completion-answer msg completions false))
28 | ([{:keys [id session] :as _msg} completions done?]
29 | (cond-> {:completions completions}
30 | id (assoc :id id)
31 | session (assoc :session (if (instance? clojure.lang.AReference session)
32 | (-> session meta :id)
33 | session))
34 | done? (assoc :status #{"done"}))))
35 |
36 | (defn- cljs-dynamic-completion-handler
37 | "Handles op = \"complete\". Will try to fetch object completions and puts them
38 | on the wire with transport but also allows the default completion handler to
39 | act."
40 | [standalone? next-handler {:keys [_id session _ns transport op symbol] :as msg}]
41 |
42 | (if (and
43 | ;; completion request?
44 | (= op "complete") (not= "" symbol)
45 | ;; cljs?
46 | (some #(get @session (resolve %)) '(piggieback.core/*cljs-compiler-env*
47 | cider.piggieback/*cljs-compiler-env*)))
48 |
49 | (let [completions (complete-for-nrepl msg)]
50 | ;; in standalone mode we send an answer, even if we have no completions
51 | (if standalone?
52 | (transport/send transport (completion-answer msg completions true))
53 |
54 | ;; otherwise we send if we have some completions but leave it to the
55 | ;; other middleware to send additional + marking the message as done
56 | (do (when completions
57 | (transport/send transport (completion-answer msg completions)))
58 | (next-handler msg))))
59 |
60 | (next-handler msg)))
61 |
62 | (defn wrap-complete [handler]
63 | (fn [msg] (cljs-dynamic-completion-handler false handler msg)))
64 |
65 | (defn wrap-complete-standalone [handler]
66 | (fn [msg] (cljs-dynamic-completion-handler true handler msg)))
67 |
68 | (set-descriptor! #'wrap-complete
69 | {:doc "Middleware providing runtime completion support - in addition to normal cljs completions."
70 | :requires #{"clone"}
71 | :expects #{"complete" "eval"}
72 | :handles {;; "complete"
73 | ;; {:doc "Return a list of symbols matching the specified (partial) symbol."
74 | ;; :requires {"ns" "The symbol's namespace"
75 | ;; "symbol" "The symbol to lookup"
76 | ;; "session" "The current session"}
77 | ;; :optional {"context" "Completion context for compliment."
78 | ;; "extra-metadata" "List of extra-metadata fields. Possible values: arglists, doc."}
79 | ;; :returns {"completions" "A list of possible completions"}}
80 | ;; "complete-doc"
81 | ;; {:doc "Retrieve documentation suitable for display in completion popup"
82 | ;; :requires {"ns" "The symbol's namespace"
83 | ;; "symbol" "The symbol to lookup"}
84 | ;; :returns {"completion-doc" "Symbol's documentation"}}
85 | ;; "complete-flush-caches"
86 | ;; {:doc "Forces the completion backend to repopulate all its caches"}
87 | }})
88 |
89 | (set-descriptor! #'wrap-complete-standalone
90 | {:doc "Middleware providing runtime completion support."
91 | :requires #{"clone"}
92 | :expects #{"complete" "eval"}
93 | :handles {}})
94 |
--------------------------------------------------------------------------------
/src/main/suitable/utils.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.utils
2 | (:require
3 | [cljs.env]
4 | [cljs.repl]))
5 |
6 | (defn wrapped-cljs-repl-eval
7 | "cljs-eval-fn for `suitable.cljs-completions` that can be used when a
8 | repl-env and compiler env are accessible, e.g. when running a normal repl."
9 | [repl-env compiler-env]
10 | (fn [_ns code]
11 | (try
12 | {:value (->> code
13 | read-string
14 | (cljs.repl/eval-cljs repl-env compiler-env)
15 | read-string)}
16 | (catch Exception e {:error e}))))
17 |
--------------------------------------------------------------------------------
/src/test/suitable/complete_for_nrepl_test.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.complete-for-nrepl-test
2 | (:require
3 | [cider.piggieback :as piggieback]
4 | [clojure.java.shell]
5 | [clojure.test :as t :refer [are deftest is run-tests testing]]
6 | [nrepl.core :as nrepl]
7 | [nrepl.server :refer [default-handler start-server]]
8 | [suitable.complete-for-nrepl :as sut]
9 | [suitable.middleware :refer [wrap-complete-standalone]]))
10 |
11 | (require 'cljs.repl)
12 | (require 'cljs.repl.node)
13 |
14 | (def ^:dynamic *session* nil)
15 | (def ^:dynamic ^nrepl.server.Server *server* nil)
16 | (def ^:dynamic ^nrepl.transport.FnTransport *transport* nil)
17 |
18 | (defn message
19 | ([msg]
20 | (message msg true))
21 |
22 | ([msg combine-responses?]
23 | {:pre [*session*]}
24 | (let [responses (nrepl/message *session* msg)]
25 | (if combine-responses?
26 | (nrepl/combine-responses responses)
27 | responses))))
28 |
29 | (def handler nil)
30 | (def server nil)
31 | (def transport nil)
32 | (def client nil)
33 | (def session nil)
34 |
35 | (defn stop []
36 | (message {:op :eval :code (nrepl/code :cljs/quit)})
37 | (.close *transport*)
38 | (.close *server*))
39 |
40 | (defmacro with-repl-env
41 | {:style/indent 1}
42 | [renv-form & body]
43 | `(do
44 | (alter-var-root #'handler (constantly (default-handler #'piggieback/wrap-cljs-repl #'wrap-complete-standalone)))
45 | (alter-var-root #'server (constantly (start-server :handler handler)))
46 | (alter-var-root #'transport (constantly (nrepl/connect :port (:port server))))
47 | (alter-var-root #'client (constantly (nrepl/client transport (:port server))))
48 | (alter-var-root #'session (constantly (doto (nrepl/client-session client)
49 | assert)))
50 | (binding [*server* server
51 | *transport* transport
52 | *session* session]
53 | (try
54 | (dorun (message
55 | {:op :eval
56 | :code (nrepl/code (require '[cider.piggieback :as piggieback])
57 | (piggieback/cljs-repl ~renv-form))}))
58 | (dorun (message {:op :eval
59 | :code (nrepl/code (require 'clojure.data))}))
60 |
61 | (do
62 | ~@body)
63 | (finally
64 | (stop))))))
65 |
66 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
67 |
68 | (deftest sanity-test-node
69 | (let [{:keys [exit]
70 | :as v} (clojure.java.shell/sh "node" "--version")]
71 | (assert (zero? exit)
72 | (pr-str v)))
73 |
74 | (with-repl-env (cljs.repl.node/repl-env)
75 | (testing "cljs repl is active"
76 | (let [response (message {:op :eval
77 | :code (nrepl/code (js/Object.))})
78 | explanation (pr-str response)]
79 | (is (= "cljs.user" (:ns response))
80 | explanation)
81 | (is (= ["#js{}"] (:value response))
82 | explanation)
83 | (is (= #{"done"} (:status response))
84 | explanation)))))
85 |
86 | (deftest suitable-node
87 | (with-repl-env (cljs.repl.node/repl-env)
88 | (testing "js global completion"
89 | (let [response (message {:op "complete"
90 | :ns "cljs.user"
91 | :symbol "js/Ob"})
92 | candidates (:completions response)]
93 | (is (= [{:candidate "js/Object", :ns "js", :type "function"}] candidates))))
94 |
95 | (testing "manages context state"
96 | (message {:op "complete"
97 | :ns "cljs.user"
98 | :symbol ".xxxx"
99 | :context "(__prefix__ js/Object)"})
100 | (let [response (message {:op "complete"
101 | :ns "cljs.user"
102 | :symbol ".key"
103 | :context ":same"})
104 | candidates (:completions response)]
105 | (is (= [{:ns "js/Object", :candidate ".keys" :type "function"}] candidates)
106 | (pr-str response))))
107 |
108 | (testing "enumerable items are filtered out"
109 | (are [context candidates] (= candidates
110 | (let [response (message {:op "complete"
111 | :ns "cljs.user"
112 | :symbol ".-"
113 | :context context})]
114 | (:completions response)))
115 | "(__prefix__ (js/String \"abc\"))"
116 | [{:candidate ".-length", :ns "(js/String \"abc\")", :type "var"}]
117 |
118 | "(-> (js/String \"abc\") __prefix__)"
119 | [{:candidate ".-length", :ns "(-> (js/String \"abc\"))", :type "var"}]
120 |
121 | "(__prefix__ #js [1 2 3])"
122 | []
123 |
124 | "(__prefix__ (array 1 2 3))"
125 | [{:candidate ".-length", :ns "(array 1 2 3)", :type "var"}]
126 |
127 | "(__prefix__ (js/Array. 1 2 3))"
128 | [{:candidate ".-length", :ns "(js/Array. 1 2 3)", :type "var"}]
129 |
130 | "(__prefix__ (js/Set. (js/Array. 1 2 3)))"
131 | [{:candidate ".-size", :ns "(js/Set. (js/Array. 1 2 3))", :type "var"}]))))
132 |
133 | (deftest node-env?
134 | (is (false? (sut/node-env? nil)))
135 | (is (false? (sut/node-env? 42)))
136 | (is (sut/node-env? (cljs.repl.node/repl-env)))
137 | ;; Exercise `piggieback/generate-delegating-repl-env` because it's mentioned in the docstring of `sut/node-env?`:
138 | (is (sut/node-env? (#'piggieback/generate-delegating-repl-env (cljs.repl.node/repl-env)))))
139 |
140 | (comment
141 | (run-tests))
142 |
--------------------------------------------------------------------------------
/src/test/suitable/compliment/sources/t_cljs.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.compliment.sources.t-cljs
2 | (:require
3 | [clojure.java.io :as io]
4 | [clojure.set :as set]
5 | [clojure.string :as string]
6 | [clojure.test :as test :refer [deftest is testing use-fixtures]]
7 | [compliment.utils :refer [*extra-metadata*]]
8 | [suitable.cljs.env :as cljs-env]
9 | [suitable.compliment.sources.cljs :as cljs-sources]))
10 |
11 | (use-fixtures :once
12 | (fn [f]
13 | (binding [cljs-sources/*compiler-env* (cljs-env/create-test-env)]
14 | (f))))
15 |
16 | (defn completions
17 | [prefix & [ns]]
18 | (cljs-sources/candidates prefix (some-> ns find-ns) nil))
19 |
20 | (defn set=
21 | [coll1 coll2 & colls]
22 | (apply = (set coll1) (set coll2) (map set colls)))
23 |
24 | (deftest sanity
25 | (testing "Nothing returned for non-existent prefix"
26 | (is (= '()
27 | (completions "abcdefghijk")
28 | (completions "abcdefghijk" 'suitable.test-ns))))
29 |
30 | (testing "cljs.core candidates returned for new namespaces"
31 | (is (= (completions "")
32 | (completions "" 'abcdefghijk))))
33 |
34 | (let [all-candidates (completions "" 'suitable.test-ns)]
35 | (testing "All candidates have a string for :candidate"
36 | (is (every? (comp string? :candidate) all-candidates)))
37 |
38 | (testing "All candidates that should have an :ns, do"
39 | (let [filter-fn #(not (#{:import :keyword :namespace :class} (:type %)))
40 | filtered-candidates (filter filter-fn all-candidates)]
41 | (is (every? :ns filtered-candidates))))
42 |
43 | (testing "All candidates have a valid :type"
44 | (let [valid-types #{:function
45 | :class
46 | :keyword
47 | :macro
48 | :namespace
49 | :protocol
50 | :protocol-function
51 | :record
52 | :special-form
53 | :type
54 | :var}]
55 | (is (every? (comp valid-types :type) all-candidates))))))
56 |
57 | (deftest special-form-completions
58 | (testing "Special form"
59 | (is (= '({:candidate "throw" :ns "cljs.core" :type :special-form})
60 | (completions "thr")))
61 |
62 | (is (set= '({:candidate "def" :ns "cljs.core" :type :special-form}
63 | {:candidate "do" :ns "cljs.core" :type :special-form}
64 | {:candidate "defrecord*" :ns "cljs.core" :type :special-form}
65 | {:candidate "deftype*" :ns "cljs.core" :type :special-form})
66 | (->> (completions "d")
67 | (filter #(= :special-form (:type %))))))))
68 |
69 | (deftest string-requires
70 | ;; See: https://github.com/clojure-emacs/clj-suitable/issues/22
71 | (testing "Libspecs expressed under the newer string notation"
72 | (let [ns-sym 'suitable.test-ns-dep
73 | ns-filename (str (-> ns-sym
74 | str
75 | (string/replace "." "/")
76 | (string/replace "-" "_")
77 | (str ".cljs")))]
78 | (assert (-> ns-filename io/resource slurp (string/includes? "[\"clojure.set\" :as set]"))
79 | "The exercised ns has in fact a string require")
80 | (is (seq (#'cljs-sources/candidates* "set" ns-sym))
81 | "Can be successfully run without throwing errors"))))
82 |
83 | (deftest namespace-completions
84 | (testing "Namespace"
85 | (is (set= '({:candidate "suitable.test-ns" :type :namespace}
86 | {:candidate "suitable.test-ns-dep" :type :namespace})
87 | (completions "suitable.t"))))
88 |
89 | (testing "Macro Namespace"
90 | (is (set= '({:candidate "suitable.test-ns" :type :namespace}
91 | {:candidate "suitable.test-ns-dep" :type :namespace}
92 | {:candidate "suitable.test-macros" :type :namespace})
93 | (completions "suitable.t" 'suitable.test-ns))))
94 |
95 | (testing "Namespace alias"
96 | (is (= '()
97 | (completions "test-d")
98 | (completions "test-d" 'cljs.user)))
99 | (is (= '({:candidate "test-dep" :ns "suitable.test-ns-dep" :type :namespace})
100 | (completions "test-d" 'suitable.test-ns)))))
101 |
102 | (deftest macro-namespace-completions
103 | (testing "Macro namespace"
104 | (is (= '()
105 | (completions "suitable.test-macros")
106 | (completions "suitable.test-macros" 'cljs.user)))
107 | (is (= '({:candidate "suitable.test-macros" :type :namespace})
108 | (completions "suitable.test-m" 'suitable.test-ns))))
109 |
110 | (testing "Macro namespace alias"
111 | (is (= '()
112 | (completions "test-m")))
113 | (is (= '({:candidate "test-macros" :ns "suitable.test-macros" :type :namespace})
114 | (completions "test-m" 'suitable.test-ns)))))
115 |
116 | (deftest fn-completions
117 | (testing "cljs.core fn"
118 | (is (set= '({:candidate "unchecked-add" :ns "cljs.core" :type :function}
119 | {:candidate "unchecked-add-int" :ns "cljs.core" :type :function})
120 | (completions "unchecked-a")
121 | (completions "unchecked-a" 'cljs.user)))
122 | (is (set= '({:candidate "cljs.core/unchecked-add" :ns "cljs.core" :type :function}
123 | {:candidate "cljs.core/unchecked-add-int" :ns "cljs.core" :type :function})
124 | (completions "cljs.core/unchecked-a")
125 | (completions "cljs.core/unchecked-a" 'cljs.user))))
126 |
127 | (testing "Excluded cljs.core fn"
128 | (is (= '()
129 | (completions "unchecked-b" 'suitable.test-ns)))
130 | (is (= '({:candidate "cljs.core/unchecked-byte" :ns "cljs.core" :type :function})
131 | (completions "cljs.core/unchecked-b" 'suitable.test-ns))))
132 |
133 | (testing "Namespace-qualified fn"
134 | (is (= '({:candidate "suitable.test-ns/issue-28" :ns "suitable.test-ns" :type :function})
135 | (completions "suitable.test-ns/iss")
136 | (completions "suitable.test-ns/iss" 'cljs.user)
137 | (completions "suitable.test-ns/iss" 'suitable.test-ns))))
138 |
139 | (testing "Referred fn"
140 | (is (= '()
141 | (completions "bla")
142 | (completions "bla" 'suitable.test-ns)))
143 | (is (= '({:candidate "foo-in-dep" :ns "suitable.test-ns-dep" :type :function})
144 | (completions "foo" 'suitable.test-ns))))
145 |
146 | (testing "Local fn"
147 | (is (= '({:candidate "test-public-fn" :ns "suitable.test-ns" :type :function})
148 | (completions "test-pu" 'suitable.test-ns))))
149 |
150 | (testing "Private fn"
151 | (is (= '()
152 | (completions "test-pri")
153 | (completions "test-pri" 'cljs.user)))
154 | (is (= '({:candidate "test-private-fn" :ns "suitable.test-ns" :type :function})
155 | (completions "test-pri" 'suitable.test-ns))))
156 |
157 | (testing "Fn shadowing macro with same name"
158 | (is (= '({:candidate "identical?" :ns "cljs.core" :type :function})
159 | (completions "identical?")))))
160 |
161 | (deftest macro-completions
162 | (testing "cljs.core macro"
163 | (is (set= '({:candidate "cond->" :ns "cljs.core" :type :macro}
164 | {:candidate "cond->>" :ns "cljs.core" :type :macro})
165 | (completions "cond-")
166 | (completions "cond-" 'suitable.test-ns)))
167 | (is (set= '({:candidate "cljs.core/cond->" :ns "cljs.core" :type :macro}
168 | {:candidate "cljs.core/cond->>" :ns "cljs.core" :type :macro})
169 | (completions "cljs.core/cond-")
170 | (completions "cljs.core/cond-" 'suitable.test-ns))))
171 |
172 | (testing "Excluded cljs.core macro"
173 | (is (= '()
174 | (completions "whil" 'suitable.test-ns)))
175 | (is (= '({:candidate "cljs.core/while" :ns "cljs.core" :type :macro})
176 | (completions "cljs.core/whil" 'suitable.test-ns))))
177 |
178 | (testing "Namespace-qualified macro"
179 | (is (= '()
180 | (completions "suitable.test-macros/non-existent")
181 | (completions "suitable.test-macros/non-existent" 'cljs.user)))
182 | (is (set= '({:candidate "suitable.test-macros/my-add" :ns "suitable.test-macros" :type :macro}
183 | {:candidate "suitable.test-macros/my-sub" :ns "suitable.test-macros" :type :macro})
184 | (completions "suitable.test-macros/my-" 'suitable.test-ns))))
185 |
186 | (testing "Referred macro"
187 | (is (= '()
188 | (completions "non-existent")
189 | (completions "non-existent" 'suitable.test-ns)))
190 | ;; only my-add cause it is the only one referred
191 | (is (= '({:candidate "my-add" :ns "suitable.test-macros" :type :macro})
192 | (completions "my-" 'suitable.test-ns)))))
193 |
194 | (deftest import-completions
195 | (testing "Import"
196 | (is (= '()
197 | (completions "IdGen")
198 | (completions "IdGen" 'cljs.user)))
199 | (is (= '({:candidate "IdGenerator" :type :class})
200 | (completions "IdGen" 'suitable.test-ns))))
201 |
202 | (testing "Namespace-qualified import"
203 | ;; TODO Investigate if this is a bug in the implementation.
204 | ;;
205 | ;; This test used to pass as would not complete unless you have :import in
206 | ;; the ns. It might a change in behavior in the way the compiler fills its
207 | ;; env with the newer versions.
208 | ;;
209 | ;; (is (= '()
210 | ;; (completions "goog.ui.IdGen")
211 | ;; (completions "goog.ui.IdGen" 'cljs.user)))
212 |
213 | (is (= '({:candidate "goog.ui.IdGenerator" :type :namespace})
214 | (completions "goog.ui.IdGen" 'suitable.test-ns)))))
215 |
216 | (deftest keyword-completions
217 | (testing "Keyword"
218 | (is (empty? (set/difference (set '({:candidate ":refer-macros" :type :keyword}
219 | {:candidate ":require-macros" :type :keyword}))
220 | (set (completions ":re")))) "the completion should include ClojureScript-specific keywords."))
221 |
222 | (testing "Local keyword"
223 | (is (= '({:candidate ":one" :type :keyword})
224 | (completions ":on" 'suitable.test-ns))))
225 |
226 | (testing "Keyword from another namespace"
227 | (is (= '({:candidate ":four" :type :keyword})
228 | (completions ":fo" 'suitable.test-ns))))
229 |
230 | (testing "Local namespaced keyword"
231 | (is (= '({:candidate "::some-namespaced-keyword" :ns "suitable.test-ns" :type :keyword})
232 | (completions "::so" 'suitable.test-ns)))
233 |
234 | (is (= '()
235 | (completions "::i" 'suitable.test-ns))))
236 |
237 | (testing "Referred namespaced keyword"
238 | (is (= '()
239 | (completions "::test-dep/f" 'suitable.test-ns)))
240 |
241 | (is (= '({:candidate "::test-dep/dep-namespaced-keyword" :ns "suitable.test-ns-dep" :type :keyword})
242 | (completions "::test-dep/d" 'suitable.test-ns))))
243 |
244 | (testing "Referred namespaced keyword :as-alias"
245 | (is (= '({:candidate "::aliased/kw" :type :keyword :ns "suitable.test-ns-alias"})
246 | (completions "::aliased/k" 'suitable.test-ns)))))
247 |
248 | (deftest protocol-completions
249 | (testing "Protocol"
250 | (is (set= '({:candidate "IIndexed" :ns "cljs.core" :type :protocol}
251 | {:candidate "IIterable" :ns "cljs.core" :type :protocol})
252 | (completions "II"))))
253 |
254 | (testing "Protocol fn"
255 | (is (set= '({:candidate "-with-meta" :ns "cljs.core" :type :protocol-function}
256 | {:candidate "-write" :ns "cljs.core" :type :protocol-function})
257 | (completions "-w")))))
258 |
259 | (deftest record-completions
260 | (testing "Record"
261 | (is (= '({:candidate "TestRecord" :ns "suitable.test-ns" :type :record})
262 | (completions "Te" 'suitable.test-ns)))))
263 |
264 | (deftest type-completions
265 | (testing "Type"
266 | (is (set= '({:candidate "ES6Iterator" :ns "cljs.core" :type :type}
267 | {:candidate "ES6IteratorSeq" :ns "cljs.core" :type :type})
268 | (completions "ES6I")))))
269 |
270 | (deftest extra-metadata
271 | (testing "Extra metadata: namespace :doc"
272 | (binding [*extra-metadata* #{:doc}]
273 | (is (set= '({:candidate "suitable.test-ns" :doc "A test namespace" :type :namespace}
274 | {:candidate "suitable.test-ns-dep" :doc "Dependency of test-ns namespace" :type :namespace})
275 | (completions "suitable.test-")))))
276 |
277 | (testing "Extra metadata: aliased namespace :doc"
278 | (binding [*extra-metadata* #{:doc}]
279 | (is (= '({:candidate "test-dep" :doc "Dependency of test-ns namespace" :ns "suitable.test-ns-dep" :type :namespace})
280 | (completions "test-d" 'suitable.test-ns)))))
281 |
282 | (testing "Extra metadata: macro namespace :doc"
283 | (binding [*extra-metadata* #{:doc}]
284 | (is (= '({:candidate "suitable.test-macros" :doc "A test macro namespace" :type :namespace})
285 | (completions "suitable.test-m" 'suitable.test-ns)))))
286 |
287 | (testing "Extra metadata: normal var :arglists"
288 | (binding [*extra-metadata* #{:arglists}]
289 | (is (set= '({:candidate "unchecked-add" :ns "cljs.core" :arglists ("[]" "[x]" "[x y]" "[x y & more]") :type :function}
290 | {:candidate "unchecked-add-int" :ns "cljs.core" :arglists ("[]" "[x]" "[x y]" "[x y & more]") :type :function})
291 | (completions "unchecked-a" 'cljs.user)))))
292 |
293 | (testing "Extra metadata: normal var :doc"
294 | (binding [*extra-metadata* #{:doc}]
295 | (is (set= '({:candidate "unchecked-add" :ns "cljs.core" :doc "Returns the sum of nums. (+) returns 0." :type :function}
296 | {:candidate "unchecked-add-int" :ns "cljs.core" :doc "Returns the sum of nums. (+) returns 0." :type :function})
297 | (completions "unchecked-a" 'cljs.user)))))
298 |
299 | (testing "Extra metadata: macro :arglists"
300 | (binding [*extra-metadata* #{:arglists}]
301 | (is (= '({:candidate "defprotocol" :ns "cljs.core" :arglists ("[psym & doc+methods]") :type :macro})
302 | (completions "defproto" 'cljs.user)))))
303 |
304 | (testing "Extra metadata: referred var :arglists"
305 | (binding [*extra-metadata* #{:arglists}]
306 | (is (= '({:candidate "foo-in-dep" :ns "suitable.test-ns-dep" :arglists ("[foo]") :type :function})
307 | (completions "foo" 'suitable.test-ns)))))
308 |
309 | (testing "Extra metadata: referred macro :arglists"
310 | (binding [*extra-metadata* #{:arglists}]
311 | (is (= '({:candidate "my-add" :ns "suitable.test-macros" :arglists ("[a b]") :type :macro})
312 | (completions "my-a" 'suitable.test-ns))))))
313 |
314 | (deftest predicates
315 | (testing "The plain-symbol? predicate"
316 | (is (cljs-sources/plain-symbol? "foo") "should detect a \"plain\" symbol")
317 | (is (not (cljs-sources/plain-symbol? "foo.bar")) "should NOT match a namespace")
318 | (is (not (cljs-sources/plain-symbol? ":foo")) "should NOT match a keyword")
319 | (is (not (cljs-sources/plain-symbol? "::foo/bar")) "should NOT match a qualified keyword")
320 | (is (not (cljs-sources/plain-symbol? "foo/bar")) "should NOT match a qualified symbol")))
321 |
322 | (deftest docstring-generation
323 | (testing "symbol docstring"
324 | (is (string? (cljs-sources/doc "map" nil)) "should return the map docstring, defaulting to the cljs.core namespace")
325 | (is (string? (cljs-sources/doc "map" (find-ns 'cljs.core))) "should return the map docstring")
326 | (is (string? (cljs-sources/doc "my-add" (find-ns 'suitable.test-macros)))))
327 |
328 | (testing "namespace docstring"
329 | (is (= "-------------------------\n\n A test macro namespace\n" (cljs-sources/doc "suitable.test-macros" nil)))
330 | (is (= "-------------------------\n\n A test namespace\n" (cljs-sources/doc "suitable.test-ns" nil)))))
331 |
--------------------------------------------------------------------------------
/src/test/suitable/js_completion_test.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.js-completion-test
2 | (:require
3 | [clojure.pprint :refer [cl-format]]
4 | [clojure.string :refer [starts-with?]]
5 | [clojure.test :as t :refer [are deftest is testing]]
6 | [suitable.js-completions :as sut]))
7 |
8 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
9 | ;; helpers
10 |
11 | (defn candidate
12 | ([completion ns]
13 | (candidate completion ns (cond
14 | (re-find #"^(?:js/|\.-|-)" completion) "var"
15 | (starts-with? completion ".") "function"
16 | :else "function")))
17 |
18 | ([completion ns type]
19 | {:type type, :candidate completion :ns ns}))
20 |
21 | (defn fake-cljs-eval-fn [expected-obj-expression expected-prefix properties]
22 | (fn [_ns code]
23 | (when-let [[_ obj-expr prefix]
24 | (re-matches
25 | #"^\(suitable.js-introspection/property-names-and-types (.*) \"(.*)\"\)"
26 | code)]
27 | (if (and (= obj-expr expected-obj-expression)
28 | (= prefix expected-prefix))
29 | {:error nil
30 | :value properties}
31 | (is false
32 | (cl-format nil
33 | "expected obj-expr / prefix~% ~S / ~S~% passed to fake-js-props-fn does not match actual expr / prefix~% ~S / ~S"
34 | expected-obj-expression expected-prefix
35 | obj-expr prefix))))))
36 |
37 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
38 | ;; expr-for-parent-obj
39 |
40 | (deftest expr-for-parent-obj
41 | (testing "Finds an expression that evaluates to the object being accessed"
42 | (let [tests [;; should not trigger object completion
43 | {:desc "no object in sight"
44 | :symbol-and-context [".log" "(__prefix__)"]
45 | :expected nil}
46 |
47 | {:desc "The user typed a normal object/class name, not a method or special name."
48 | :symbol-and-context ["bar" "(.log __prefix__)"]
49 | :expected nil}
50 |
51 | ;; should trigger object completion
52 | {:desc ". method"
53 | :symbol-and-context [".log" "(__prefix__ js/console)"]
54 | :expected {:type :. :obj-expr "js/console"}}
55 |
56 | {:desc ". method nested"
57 | :symbol-and-context [".log" "(__prefix__ (.-console js/window) \"foo\")"]
58 | :expected {:type :. :obj-expr "(.-console js/window)"}}
59 |
60 | {:desc ".- prop"
61 | :symbol-and-context [".-memory" "(__prefix__ js/console)"]
62 | :expected {:type :. :obj-expr "js/console"}}
63 |
64 | {:desc ".- prop nested"
65 | :symbol-and-context [".-memory" "(__prefix__ (.-console js/window) \"foo\")"]
66 | :expected {:type :. :obj-expr "(.-console js/window)"}}
67 |
68 | {:desc ".. method"
69 | :symbol-and-context ["log" "(.. js/console __prefix__)"]
70 | :expected {:type :.. :obj-expr "js/console"}}
71 |
72 | {:desc ".. method nested"
73 | :symbol-and-context ["log" "(.. js/console (__prefix__ \"foo\"))"]
74 | :expected {:type :.. :obj-expr "js/console"}}
75 |
76 | {:desc ".. method chained"
77 | :symbol-and-context ["log" "(.. js/window -console __prefix__)"]
78 | :expected {:type :.. :obj-expr "(.. js/window -console)"}}
79 |
80 | {:desc ".. method chained and nested"
81 | :symbol-and-context ["log" "(.. js/window -console (__prefix__ \"foo\"))"]
82 | :expected {:type :.. :obj-expr "(.. js/window -console)"}}
83 |
84 | {:desc ".. prop"
85 | :symbol-and-context ["-memory" "(.. js/console __prefix__)"]
86 | :expected {:type :.. :obj-expr "js/console"}}
87 |
88 | {:desc "-> (first member of the chain is a constant)"
89 | :symbol-and-context [".log" "(-> 52 __prefix__)"]
90 | :expected {:type :-> :obj-expr "(-> 52)"}}
91 |
92 | {:desc "->"
93 | :symbol-and-context [".log" "(-> js/console __prefix__)"]
94 | :expected {:type :-> :obj-expr "(-> js/console)"}}
95 |
96 | {:desc "-> (.)"
97 | :symbol-and-context [".log" "(-> js/console (__prefix__ \"foo\"))"]
98 | :expected {:type :-> :obj-expr "(-> js/console)"}}
99 |
100 | {:desc "-> chained"
101 | :symbol-and-context [".log" "(-> js/window .-console __prefix__)"]
102 | :expected {:type :-> :obj-expr "(-> js/window .-console)"}}
103 |
104 | {:desc "-> (.)"
105 | :symbol-and-context [".log" "(-> js/window .-console (__prefix__ \"foo\"))"]
106 | :expected {:type :-> :obj-expr "(-> js/window .-console)"}}
107 |
108 | {:desc "doto"
109 | :symbol-and-context [".log" "(doto (. js/window -console) __prefix__)"]
110 | :expected {:type :doto :obj-expr "(. js/window -console)"}}
111 |
112 | {:desc "doto (.)"
113 | :symbol-and-context [".log" "(doto (. js/window -console) (__prefix__ \"foo\"))"]
114 | :expected {:type :doto :obj-expr "(. js/window -console)"}}
115 |
116 | {:desc "doto (.)"
117 | :symbol-and-context ["js/cons" "(doto (. js/window -console) (__prefix__ \"foo\"))"]
118 | :expected {:type :doto :obj-expr "(. js/window -console)"}}
119 |
120 | {:desc "no prefix"
121 | :symbol-and-context ["xx" "(foo bar (baz))"]
122 | :expected nil}
123 |
124 | {:desc "broken form"
125 | :symbol-and-context ["xx" "(foo "]
126 | :expected nil}]]
127 |
128 | (doseq [{[symbol context] :symbol-and-context :keys [expected desc]} tests]
129 | (is (= expected
130 | (dissoc (sut/expr-for-parent-obj symbol context)
131 | :js-interop?))
132 | desc)))))
133 |
134 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
135 | ;; cljs-completions
136 |
137 | (deftest global
138 | (let [cljs-eval-fn (fake-cljs-eval-fn "(this-as this this)" "c" [{:name "console" :hierarchy 1 :type "var"}
139 | {:name "confirm" :hierarchy 1 :type "function"}])]
140 | (is (= [(candidate "js/console" "js" "var")
141 | (candidate "js/confirm" "js" "function")]
142 | (sut/cljs-completions cljs-eval-fn "js/c" {:ns "cljs.user" :context ""})))))
143 |
144 | (deftest global-prop
145 | (testing "console"
146 | (let [cljs-eval-fn (fake-cljs-eval-fn "(this-as this (.. this -console))" "lo"
147 | [{:name "log" :hierarchy 1 :type "function"}
148 | {:name "clear" :hierarchy 1 :type "function"}])]
149 | (is (= [(candidate "js/console.log" "js" "function")]
150 | (sut/cljs-completions cljs-eval-fn "js/console.lo" {:ns "cljs.user" :context "js/console"})))))
151 |
152 | (testing "window.console"
153 | (let [cljs-eval-fn (fake-cljs-eval-fn "(this-as this (.. this -window -console))" "lo"
154 | [{:name "log" :hierarchy 1 :type "function"}
155 | {:name "clear" :hierarchy 1 :type "function"}])]
156 | (is (= [(candidate "js/window.console.log" "js" "function")]
157 | (sut/cljs-completions cljs-eval-fn "js/window.console.lo" {:ns "cljs.user" :context "js/console"}))))))
158 |
159 | (deftest simple
160 | (let [cljs-eval-fn (fake-cljs-eval-fn "js/console" "l" [{:name "log" :hierarchy 1 :type "function"}
161 | {:name "clear" :hierarchy 1 :type "function"}])]
162 | (is (= [(candidate ".log" "js/console")]
163 | (sut/cljs-completions cljs-eval-fn ".l" {:ns "cljs.user" :context "(__prefix__ js/console)"})))
164 | (is (= [(candidate "log" "js/console")]
165 | (sut/cljs-completions cljs-eval-fn "l" {:ns "cljs.user" :context "(. js/console __prefix__)"})))
166 | ;; note: we're testing context with a form here
167 | (is (= [(candidate "log" "js/console")]
168 | (sut/cljs-completions cljs-eval-fn "l" {:ns "cljs.user" :context '(.. js/console (__prefix__ "foo"))})))))
169 |
170 | (deftest thread-first-completion
171 | (let [cljs-eval-fn (fake-cljs-eval-fn "(-> js/foo)" "ba" [{:name "bar" :hierarchy 1 :type "var"}
172 | {:name "baz" :hierarchy 1 :type "function"}])]
173 | (is (= [(candidate ".-bar" "(-> js/foo)")]
174 | (sut/cljs-completions cljs-eval-fn ".-ba" {:ns "cljs.user" :context "(-> js/foo __prefix__)"})))))
175 |
176 | (deftest analyze-symbol-and-context
177 | (are [context expected] (= expected
178 | (select-keys (sut/analyze-symbol-and-context ".-ba" context)
179 | [:type :js-interop? :obj-expr]))
180 | "(-> 42 __prefix__)" {:type :->, :js-interop? false, :obj-expr "(-> 42)"}
181 | "(-> foo __prefix__)" {:type :->, :js-interop? false, :obj-expr "(-> foo)"}
182 | "(-> 42 (__prefix__))" {:type :->, :js-interop? false, :obj-expr "(-> 42)"}
183 | "(-> foo (__prefix__))" {:type :->, :js-interop? false, :obj-expr "(-> foo)"}
184 | "(-> 42 (__prefix__ 42))" {:type :->, :js-interop? false, :obj-expr "(-> 42)"}
185 | "(-> foo (__prefix__ 42))" {:type :->, :js-interop? false, :obj-expr "(-> foo)"}
186 | "(-> 42 __prefix__ FOO)" {:type :->, :js-interop? false, :obj-expr "(-> 42)"}
187 | "(-> foo __prefix__ FOO)" {:type :->, :js-interop? false, :obj-expr "(-> foo)"}
188 | "(-> 42 (__prefix__) FOO)" {:type :->, :js-interop? false, :obj-expr "(-> 42)"}
189 | "(-> foo (__prefix__) FOO)" {:type :->, :js-interop? false, :obj-expr "(-> foo)"}
190 | "(-> 42 (__prefix__ 42) FOO)" {:type :->, :js-interop? false, :obj-expr "(-> 42)"}
191 | "(-> foo (__prefix__ 42) FOO)" {:type :->, :js-interop? false, :obj-expr "(-> foo)"}
192 | "(-> js/foo __prefix__)" {:type :->, :js-interop? true, :obj-expr "(-> js/foo)"}
193 | "(-> (js/foo) __prefix__)" {:type :->, :js-interop? true, :obj-expr "(-> (js/foo))"}
194 | "(-> (js/foo 42) __prefix__)" {:type :->, :js-interop? true, :obj-expr "(-> (js/foo 42))"}
195 | "(-> (js/foo 42) (__prefix__))" {:type :->, :js-interop? true, :obj-expr "(-> (js/foo 42))"}
196 | "(-> (js/foo 42) (__prefix__ 42))" {:type :->, :js-interop? true, :obj-expr "(-> (js/foo 42))"}
197 | "(-> (js/foo 42) (__prefix__ 42) FOO)" {:type :->, :js-interop? true, :obj-expr "(-> (js/foo 42))"}
198 | "(->> 42 __prefix__)" {:type :->, :js-interop? false, :obj-expr "(->> 42)"}
199 | "(->> foo __prefix__)" {:type :->, :js-interop? false, :obj-expr "(->> foo)"}
200 | "(->> 42 (__prefix__))" {:type :->, :js-interop? false, :obj-expr "(->> 42)"}
201 | "(->> foo (__prefix__))" {:type :->, :js-interop? false, :obj-expr "(->> foo)"}
202 | "(->> 42 (__prefix__ 42))" {:type :->, :js-interop? false, :obj-expr "(->> 42)"}
203 | "(->> foo (__prefix__ 42))" {:type :->, :js-interop? false, :obj-expr "(->> foo)"}
204 | "(->> 42 __prefix__ FOO)" {:type :->, :js-interop? false, :obj-expr "(->> 42)"}
205 | "(->> foo __prefix__ FOO)" {:type :->, :js-interop? false, :obj-expr "(->> foo)"}
206 | "(->> 42 (__prefix__) FOO)" {:type :->, :js-interop? false, :obj-expr "(->> 42)"}
207 | "(->> foo (__prefix__) FOO)" {:type :->, :js-interop? false, :obj-expr "(->> foo)"}
208 | "(->> 42 (__prefix__ 42) FOO)" {:type :->, :js-interop? false, :obj-expr "(->> 42)"}
209 | "(->> foo (__prefix__ 42) FOO)" {:type :->, :js-interop? false, :obj-expr "(->> foo)"}
210 | "(->> js/foo __prefix__)" {:type :->, :js-interop? true, :obj-expr "(->> js/foo)"}
211 | "(->> (js/foo) __prefix__)" {:type :->, :js-interop? true, :obj-expr "(->> (js/foo))"}
212 | "(->> (js/foo 42) __prefix__)" {:type :->, :js-interop? true, :obj-expr "(->> (js/foo 42))"}
213 | "(->> (js/foo 42) (__prefix__))" {:type :->, :js-interop? true, :obj-expr "(->> (js/foo 42))"}
214 | "(->> (js/foo 42) (__prefix__ 42))" {:type :->, :js-interop? true, :obj-expr "(->> (js/foo 42))"}
215 | "(->> (js/foo 42) (__prefix__ 42) FOO)" {:type :->, :js-interop? true, :obj-expr "(->> (js/foo 42))"}
216 | "(doto 42 __prefix__)" {:type :doto, :js-interop? true, :obj-expr "42"}
217 | "(doto foo __prefix__)" {:type :doto, :js-interop? true, :obj-expr "foo"}
218 | "(doto 42 (__prefix__))" {:type :doto, :js-interop? true, :obj-expr "42"}
219 | "(doto foo (__prefix__))" {:type :doto, :js-interop? true, :obj-expr "foo"}
220 | "(doto 42 (__prefix__ 42))" {:type :doto, :js-interop? true, :obj-expr "42"}
221 | "(doto foo (__prefix__ 42))" {:type :doto, :js-interop? true, :obj-expr "foo"}
222 | "(doto 42 __prefix__ FOO)" {:type :doto, :js-interop? true, :obj-expr "42"}
223 | "(doto foo __prefix__ FOO)" {:type :doto, :js-interop? true, :obj-expr "foo"}
224 | "(doto 42 (__prefix__) FOO)" {:type :doto, :js-interop? true, :obj-expr "42"}
225 | "(doto foo (__prefix__) FOO)" {:type :doto, :js-interop? true, :obj-expr "foo"}
226 | "(doto 42 (__prefix__ 42) FOO)" {:type :doto, :js-interop? true, :obj-expr "42"}
227 | "(doto foo (__prefix__ 42) FOO)" {:type :doto, :js-interop? true, :obj-expr "foo"}
228 | "(doto js/foo __prefix__)" {:type :doto, :js-interop? true, :obj-expr "js/foo"}
229 | "(doto (js/foo) __prefix__)" {:type :doto, :js-interop? true, :obj-expr "(js/foo)"}
230 | "(doto (js/foo 42) __prefix__)" {:type :doto, :js-interop? true, :obj-expr "(js/foo 42)"}
231 | "(doto (js/foo 42) (__prefix__))" {:type :doto, :js-interop? true, :obj-expr "(js/foo 42)"}
232 | "(doto (js/foo 42) (__prefix__ 42))" {:type :doto, :js-interop? true, :obj-expr "(js/foo 42)"}
233 | "(doto (js/foo 42) (__prefix__ 42) FOO)" {:type :doto, :js-interop? true, :obj-expr "(js/foo 42)"}
234 | "(doto 42 bar baz __prefix__)" {:type :doto, :js-interop? true, :obj-expr "42"}
235 | "(doto foo bar baz __prefix__)" {:type :doto, :js-interop? true, :obj-expr "foo"}
236 | "(doto 42 bar baz (__prefix__))" {:type :doto, :js-interop? true, :obj-expr "42"}
237 | "(doto foo bar baz (__prefix__))" {:type :doto, :js-interop? true, :obj-expr "foo"}
238 | "(doto 42 bar baz (__prefix__ 42))" {:type :doto, :js-interop? true, :obj-expr "42"}
239 | "(doto foo bar baz (__prefix__ 42))" {:type :doto, :js-interop? true, :obj-expr "foo"}
240 | "(doto 42 bar baz __prefix__ FOO)" {:type :doto, :js-interop? true, :obj-expr "42"}
241 | "(doto foo bar baz __prefix__ FOO)" {:type :doto, :js-interop? true, :obj-expr "foo"}
242 | "(doto 42 bar baz (__prefix__) FOO)" {:type :doto, :js-interop? true, :obj-expr "42"}
243 | "(doto foo bar baz (__prefix__) FOO)" {:type :doto, :js-interop? true, :obj-expr "foo"}
244 | "(doto 42 bar baz (__prefix__ 42) FOO)" {:type :doto, :js-interop? true, :obj-expr "42"}
245 | "(doto foo bar baz (__prefix__ 42) FOO)" {:type :doto, :js-interop? true, :obj-expr "foo"}
246 | "(doto js/foo bar baz __prefix__)" {:type :doto, :js-interop? true, :obj-expr "js/foo"}
247 | "(doto (js/foo) bar baz __prefix__)" {:type :doto, :js-interop? true, :obj-expr "(js/foo)"}
248 | "(doto (js/foo 42) bar baz __prefix__)" {:type :doto, :js-interop? true, :obj-expr "(js/foo 42)"}
249 | "(doto (js/foo 42) bar baz (__prefix__))" {:type :doto, :js-interop? true, :obj-expr "(js/foo 42)"}
250 | "(doto (js/foo 42) bar baz (__prefix__ 42))" {:type :doto, :js-interop? true, :obj-expr "(js/foo 42)"}
251 | "(doto (js/foo 42) bar baz (__prefix__ 42) FOO)" {:type :doto, :js-interop? true, :obj-expr "(js/foo 42)"}
252 | "(. foo __prefix__)" {:type :.., :js-interop? true, :obj-expr "foo"}
253 | "(. foo __prefix__ 42)" {:type :.., :js-interop? true, :obj-expr "foo"}
254 | "(. js/a __prefix__)" {:type :.., :js-interop? true, :obj-expr "js/a"}
255 | "(. js/a __prefix__ 42)" {:type :.., :js-interop? true, :obj-expr "js/a"}
256 | "(. (js/a) __prefix__)" {:type :.., :js-interop? true, :obj-expr "(js/a)"}
257 | "(. (js/a 42) __prefix__)" {:type :.., :js-interop? true, :obj-expr "(js/a 42)"}
258 | "(. (js/a 42) __prefix__ 42)" {:type :.., :js-interop? true, :obj-expr "(js/a 42)"}
259 | "(.. foo __prefix__)" {:type :.., :js-interop? true, :obj-expr "foo"}
260 | "(.. foo __prefix__ foo)" {:type :.., :js-interop? true, :obj-expr "foo"}
261 | "(.. foo __prefix__ (foo 42))" {:type :.., :js-interop? true, :obj-expr "foo"}
262 | "(.. js/a __prefix__)" {:type :.., :js-interop? true, :obj-expr "js/a"}
263 | "(.. js/a __prefix__ foo)" {:type :.., :js-interop? true, :obj-expr "js/a"}
264 | "(.. js/a __prefix__ (foo 42))" {:type :.., :js-interop? true, :obj-expr "js/a"}
265 | "(.. (js/a) __prefix__)" {:type :.., :js-interop? true, :obj-expr "(js/a)"}
266 | "(.. (js/a 42) __prefix__)" {:type :.., :js-interop? true, :obj-expr "(js/a 42)"}
267 | "(.. (js/a 42) __prefix__ foo)" {:type :.., :js-interop? true, :obj-expr "(js/a 42)"}
268 | "(.. (js/a 42) __prefix__ (foo 42))" {:type :.., :js-interop? true, :obj-expr "(js/a 42)"}))
269 |
270 | (deftest dotdot-completion
271 | (let [cljs-eval-fn (fake-cljs-eval-fn "js/foo" "ba" [{:name "bar" :hierarchy 1 :type "var"}
272 | {:name "baz" :hierarchy 1 :type "function"}])]
273 | (is (= [(candidate "-bar" "js/foo")]
274 | (sut/cljs-completions cljs-eval-fn "-ba" {:ns "cljs.user" :context "(.. js/foo __prefix__)"})))
275 | (is (= [(candidate "baz" "js/foo")]
276 | (sut/cljs-completions cljs-eval-fn "ba" {:ns "cljs.user" :context "(.. js/foo __prefix__)"})))))
277 |
278 | (deftest dotdot-completion-chained+nested
279 | (let [cljs-eval-fn (fake-cljs-eval-fn "(.. js/foo zork)" "ba" [{:name "bar" :hierarchy 1 :type "var"}
280 | {:name "baz" :hierarchy 1 :type "function"}])]
281 | (is (= [(candidate "-bar" "(.. js/foo zork)")]
282 | (sut/cljs-completions cljs-eval-fn "-ba" {:ns "cljs.user" :context "(.. js/foo zork (__prefix__ \"foo\"))"})))
283 | (is (= [(candidate "baz" "(.. js/foo zork)")]
284 | (sut/cljs-completions cljs-eval-fn "ba" {:ns "cljs.user" :context "(.. js/foo zork (__prefix__ \"foo\"))"})))))
285 |
286 | (deftest dotdot-completion-chained+nested-2
287 | (let [cljs-eval-fn (fake-cljs-eval-fn "(.. js/foo zork)" "ba" [{:name "bar" :hierarchy 1 :type "var"}
288 | {:name "baz" :hierarchy 1 :type "function"}])]
289 | (is (= [(candidate "-bar" "(.. js/foo zork)")]
290 | (sut/cljs-completions cljs-eval-fn "-ba" {:ns "cljs.user" :context "(.. js/foo zork (__prefix__ \"foo\"))"})))
291 | (is (= [(candidate "baz" "(.. js/foo zork)")]
292 | (sut/cljs-completions cljs-eval-fn "ba" {:ns "cljs.user" :context "(.. js/foo zork (__prefix__ \"foo\"))"})))))
293 |
294 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
295 |
296 | (comment
297 |
298 | (run-tests 'suitable.js-completion-test)
299 |
300 | (with-fake-js-completions cljs-eval-fn
301 | "js/console" "l" [{:name "log" :hierarchy 1 :type "function"}
302 | {:name "clear" :hierarchy 1 :type "function"}]
303 | (sut/cljs-completions cljs-eval-fn ".l" {:ns "cljs.user" :context "(__prefix__ js/console)"}))
304 |
305 | (with-fake-js-completions cljs-eval-fn
306 | "(this-as this this)" "c" [{:name "console" :hierarchy 1 :type "var"}]
307 | (sut/cljs-completions cljs-eval-fn "js/c" {:ns "cljs.user" :context ""}))
308 |
309 | (sut/expr-for-parent-obj {:ns nil :symbol "foo" :context "(__prefix__ foo)"})
310 | (sut/expr-for-parent-obj {:ns "cljs.user" :symbol ".l" :context "(__prefix__ js/console)"})
311 |
312 | (with-redefs [sut/js-properties-of-object (fn [obj-expr msg] [])]
313 | (sut/cljs-completions {:ns "cljs.user" :symbol ".l" :context "(__prefix__ js/console)"} nil)))
314 |
--------------------------------------------------------------------------------
/src/test/suitable/js_introspection_test.cljs:
--------------------------------------------------------------------------------
1 | (ns suitable.js-introspection-test
2 | (:require [cljs.test :refer-macros [deftest is]]
3 | [goog.object :refer [get set] :rename {get oget, set oset}]
4 | [suitable.js-introspection :as inspector]))
5 |
6 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
7 | ;; helpers
8 |
9 | (defn- find-prop-named [obj name]
10 | (->> obj inspector/property-names-and-types
11 | (filter (comp (set [name]) :name))
12 | first))
13 |
14 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
15 | ;; test data
16 |
17 | (def obj-1 (new (fn [_x]
18 | (this-as this
19 | (oset this "foo" 23)
20 | (oset this "bar" (fn [] 23))))))
21 |
22 | (def obj-2 (new (fn [_x]
23 | (this-as this (oset this "foo" 23)))))
24 |
25 | ;; -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
26 | ;; tests
27 |
28 | (deftest find-props-in-obj
29 | (is (= (find-prop-named obj-1 "foo") {:name "foo", :hierarchy 0, :type "var"}))
30 | (is (= (find-prop-named obj-1 "bar") {:name "bar", :hierarchy 0, :type "function"})))
31 |
--------------------------------------------------------------------------------
/src/test/suitable/spec.clj:
--------------------------------------------------------------------------------
1 | (ns suitable.spec
2 | (:require [clojure.spec.alpha :as s]
3 | [suitable.js-completions :as suitable-js]))
4 |
5 | (s/def ::non-empty-string (s/and string? not-empty))
6 |
7 | (s/def ::type #{"var" "function"})
8 | (s/def ::name ::non-empty-string)
9 | (s/def ::candidate ::non-empty-string)
10 | (s/def ::ns ::non-empty-string)
11 | (s/def ::hierarchy int?)
12 |
13 | (s/def ::context ::non-empty-string)
14 | (s/def ::state (s/keys :req-un [::context]))
15 |
16 | (s/def ::completion (s/keys :req-un [::type ::candidate] :opt-un [::ns]))
17 | (s/def ::completions (s/coll-of ::completion))
18 | (s/def ::completions-and-state (s/keys :req-un [::state ::completions]))
19 |
20 | (s/def ::obj-property (s/keys :req-un [::name ::hierarchy ::type]))
21 |
22 | (s/fdef suitable-js/js-properties-of-object
23 | :args (s/cat :obj-expr ::non-empty-string
24 | :prefix (s/nilable string?))
25 | #_ :ret #_ (s/keys :error (s/nilable string?)
26 | :value (s/coll-of (s/keys {:name non-empty-string
27 | :hierarchy int?
28 | :type non-empty-string}))))
29 |
30 | ;; (require 'clojure.spec.test.alpha)
31 | ;; (clojure.spec.test.alpha/check ['suitable.js-completions/js-properties-of-object])
32 |
--------------------------------------------------------------------------------