|
| 1 | +(ns nextjournal.clerk.sci-env.completions |
| 2 | + (:require ["@codemirror/autocomplete" :as cm-autocomplete :refer [CompletionContext]] |
| 3 | + ["@codemirror/language" :as cm-lang] |
| 4 | + [clojure.string :as str] |
| 5 | + [goog.object :as gobject] |
| 6 | + [sci.core :as sci] |
| 7 | + [sci.ctx-store])) |
| 8 | + |
| 9 | +(defn format [fmt-str x] |
| 10 | + (str/replace fmt-str "%s" x)) |
| 11 | + |
| 12 | +(defn fully-qualified-syms [ctx ns-sym] |
| 13 | + (let [syms (sci/eval-string* ctx (format "(keys (ns-map '%s))" ns-sym)) |
| 14 | + sym-strs (map #(str "`" %) syms) |
| 15 | + sym-expr (str "[" (str/join " " sym-strs) "]") |
| 16 | + syms (sci/eval-string* ctx sym-expr) |
| 17 | + syms (remove #(str/starts-with? (str %) "nbb.internal") syms)] |
| 18 | + syms)) |
| 19 | + |
| 20 | +(defn- ns-imports->completions [ctx query-ns query] |
| 21 | + (let [[_ns-part name-part] (str/split query #"/") |
| 22 | + resolved (sci/eval-string* ctx |
| 23 | + (pr-str `(let [resolved# (resolve '~query-ns)] |
| 24 | + (when-not (var? resolved#) |
| 25 | + resolved#))))] |
| 26 | + (when resolved |
| 27 | + (when-let [[prefix imported] (if name-part |
| 28 | + (let [ends-with-dot? (str/ends-with? name-part ".") |
| 29 | + fields (str/split name-part #"\.") |
| 30 | + fields (if ends-with-dot? |
| 31 | + fields |
| 32 | + (butlast fields))] |
| 33 | + [(str query-ns "/" (when (seq fields) |
| 34 | + (let [joined (str/join "." fields)] |
| 35 | + (str joined ".")))) |
| 36 | + (apply gobject/getValueByKeys resolved |
| 37 | + fields)]) |
| 38 | + [(str query-ns "/") resolved])] |
| 39 | + (let [props (loop [obj imported |
| 40 | + props []] |
| 41 | + (if obj |
| 42 | + (recur (js/Object.getPrototypeOf obj) |
| 43 | + (into props (js/Object.getOwnPropertyNames obj))) |
| 44 | + props)) |
| 45 | + completions (map (fn [k] |
| 46 | + [nil (str prefix k)]) props)] |
| 47 | + completions))))) |
| 48 | + |
| 49 | +(defn- match [_alias->ns ns->alias query [sym-ns sym-name qualifier]] |
| 50 | + (let [pat (re-pattern query)] |
| 51 | + (or (when (and (= :unqualified qualifier) (re-find pat sym-name)) |
| 52 | + [sym-ns sym-name]) |
| 53 | + (when sym-ns |
| 54 | + (or (when (re-find pat (str (get ns->alias (symbol sym-ns)) "/" sym-name)) |
| 55 | + [sym-ns (str (get ns->alias (symbol sym-ns)) "/" sym-name)]) |
| 56 | + (when (re-find pat (str sym-ns "/" sym-name)) |
| 57 | + [sym-ns (str sym-ns "/" sym-name)])))))) |
| 58 | + |
| 59 | +(defn completions [{:keys [ctx] |
| 60 | + ns-str :ns |
| 61 | + :as request}] |
| 62 | + (js/console.log "request" request) |
| 63 | + (try |
| 64 | + (let [sci-ns (when ns-str |
| 65 | + (sci/find-ns ctx (symbol ns-str)))] |
| 66 | + (sci/binding [sci/ns (or sci-ns @sci/ns)] |
| 67 | + (if-let [query (or (:symbol request) |
| 68 | + (:prefix request))] |
| 69 | + (let [has-namespace? (str/includes? query "/") |
| 70 | + query-ns (when has-namespace? (some-> (str/split query #"/") |
| 71 | + first symbol)) |
| 72 | + from-current-ns (fully-qualified-syms ctx (sci/eval-string* ctx "(ns-name *ns*)")) |
| 73 | + from-current-ns (map (fn [sym] |
| 74 | + [(namespace sym) (name sym) :unqualified]) |
| 75 | + from-current-ns) |
| 76 | + alias->ns (sci/eval-string* ctx "(let [m (ns-aliases *ns*)] (zipmap (keys m) (map ns-name (vals m))))") |
| 77 | + ns->alias (zipmap (vals alias->ns) (keys alias->ns)) |
| 78 | + from-aliased-nss (doall (mapcat |
| 79 | + (fn [alias] |
| 80 | + (let [ns (get alias->ns alias) |
| 81 | + syms (sci/eval-string* ctx (format "(keys (ns-publics '%s))" ns))] |
| 82 | + (map (fn [sym] |
| 83 | + [(str ns) (str sym) :qualified]) |
| 84 | + syms))) |
| 85 | + (keys alias->ns))) |
| 86 | + all-namespaces (->> (sci/eval-string* ctx "(all-ns)") |
| 87 | + (map (fn [ns] |
| 88 | + [(str ns) nil :qualified]))) |
| 89 | + from-imports (when has-namespace? (ns-imports->completions ctx query-ns query)) |
| 90 | + fully-qualified-names (when-not from-imports |
| 91 | + (when has-namespace? |
| 92 | + (let [ns (get alias->ns query-ns query-ns) |
| 93 | + syms (sci/eval-string* ctx (format "(and (find-ns '%s) |
| 94 | + (keys (ns-publics '%s)))" |
| 95 | + ns))] |
| 96 | + (map (fn [sym] |
| 97 | + [(str ns) (str sym) :qualified]) |
| 98 | + syms)))) |
| 99 | + svs (concat from-current-ns from-aliased-nss all-namespaces fully-qualified-names) |
| 100 | + completions (keep (fn [entry] |
| 101 | + (match alias->ns ns->alias query entry)) |
| 102 | + svs) |
| 103 | + completions (concat completions from-imports) |
| 104 | + completions (->> (map (fn [[namespace name]] |
| 105 | + (cond-> {"candidate" (str name)} |
| 106 | + namespace (assoc "ns" (str namespace)))) |
| 107 | + completions) |
| 108 | + distinct vec)] |
| 109 | + {:completions completions |
| 110 | + :status ["done"]}) |
| 111 | + {:status ["done"]}))) |
| 112 | + (catch :default e |
| 113 | + (js/console.error "ERROR" e) |
| 114 | + {:completions [] |
| 115 | + :status ["done"]}))) |
| 116 | + |
| 117 | +(defn autocomplete [^js context] |
| 118 | + (let [node-before (.. (cm-lang/syntaxTree (.-state context)) (resolveInner (.-pos context) -1)) |
| 119 | + text-before (.. context -state (sliceDoc (.-from node-before) (.-pos context)))] |
| 120 | + #js {:from (.-from node-before) |
| 121 | + :options (clj->js (map |
| 122 | + (fn [{:strs [candidate]}] |
| 123 | + (doto {:label candidate} prn)) |
| 124 | + (:completions (completions {:ctx (sci.ctx-store/get-ctx) :ns "user" :symbol text-before}))))})) |
| 125 | + |
| 126 | +(def completion-source |
| 127 | + (cm-autocomplete/autocompletion #js {:override #js [autocomplete]})) |
0 commit comments