├── .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 | [![CircleCI](https://circleci.com/gh/clojure-emacs/clj-suitable/tree/master.svg?style=svg)](https://circleci.com/gh/clojure-emacs/clj-suitable/tree/master) 4 | [![Clojars Project](https://img.shields.io/clojars/v/org.rksm/suitable.svg)](https://clojars.org/org.rksm/suitable) 5 | [![cljdoc badge](https://cljdoc.org/badge/org.rksm/suitable)](https://cljdoc.org/d/org.rksm/suitable/CURRENT) 6 | [![downloads badge](https://versions.deps.co/org.rksm/suitable/downloads.svg)](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 | ![](doc/2019_07_22_suitable-figwheel.gif) 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 | --------------------------------------------------------------------------------