Skip to content

Commit

Permalink
Merge pull request #380 from setzer22/master
Browse files Browse the repository at this point in the history
Multiple NRepls.cs improvements
  • Loading branch information
nasser authored Apr 13, 2020
2 parents 268bed0 + 04921e4 commit a0e20da
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 20 deletions.
162 changes: 142 additions & 20 deletions Editor/NRepl.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if NET_4_6
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
Expand Down Expand Up @@ -97,6 +98,9 @@ public override void Flush ()
private static Var nsResolveVar;
private static Var findNsVar;
private static Var symbolVar;
private static Var concatVar;
private static Var completeVar;
private static Var configVar;

private static Namespace shimsNS;

Expand All @@ -123,6 +127,13 @@ static NRepl ()
nsResolveVar = RT.var("clojure.core", "ns-resolve");
findNsVar = RT.var("clojure.core", "find-ns");
symbolVar = RT.var("clojure.core", "symbol");
concatVar = RT.var("clojure.core", "concat");

Util.require("arcadia.internal.nrepl-support");
completeVar = RT.var("arcadia.internal.nrepl-support", "complete");

Util.require("arcadia.internal.config");
configVar = RT.var("arcadia.internal.config", "config");

readStringOptions = PersistentHashMap.EMPTY.assoc(Keyword.intern("read-cond"), Keyword.intern("allow"));

Expand Down Expand Up @@ -194,10 +205,42 @@ public override object invoke ()
var sessionBindings = _sessions[session];
var outWriter = new Writer("out", _request, _client);
var errWriter = new Writer("err", _request, _client);

// Split the path, and try to infer the ns from the filename. If the ns exists, then change the current ns before evaluating
List<String> nsList = new List<String>();
Namespace fileNs = null;
try
{
var path = _request["file"].ToString();
string current = null;
while (path != null && current != "Assets")
{
current = Path.GetFileNameWithoutExtension(path);
nsList.Add(current);
path = Directory.GetParent(path).FullName;
}
nsList.Reverse();
nsList.RemoveAt(0);
// Debug.Log("Trying to find: " + string.Join(".", nsList.ToArray()));
fileNs = Namespace.find(Symbol.create(string.Join(".", nsList.ToArray())));
// Debug.Log("Found: " + string.Join(".", nsList.ToArray()));
}
catch (Exception e)
{
/* Whatever sent in :file was not a path. Ignore it */
// Debug.Log(":file was not a valid ns");
}

Var.pushThreadBindings(sessionBindings
.assoc(RT.OutVar, outWriter)
.assoc(RT.ErrVar, errWriter));
var evalBindings = sessionBindings
.assoc(RT.OutVar, outWriter)
.assoc(RT.ErrVar, errWriter);
if (fileNs != null)
{
// Debug.Log("Current ns: " + fileNs.ToString());
evalBindings = evalBindings.assoc(RT.CurrentNSVar, fileNs);
}

Var.pushThreadBindings(evalBindings);
try {
var form = readStringVar.invoke(readStringOptions, code);
var result = evalVar.invoke(form);
Expand Down Expand Up @@ -274,6 +317,7 @@ static void HandleMessage (BDictionary message, TcpClient client)
{
var opValue = message["op"];
var opString = opValue as BString;
var autoCompletionSupportEnabled = RT.booleanCast(((IPersistentMap)configVar.invoke()).valAt(Keyword.intern("nrepl-auto-completion")));
if (opString != null) {
var session = GetSession(message);
switch (opString.ToString()) {
Expand All @@ -294,23 +338,26 @@ static void HandleMessage (BDictionary message, TcpClient client)
var clojureMinor = (int)clojureVersion.valAt(Keyword.intern("minor"));
var clojureIncremental = (int)clojureVersion.valAt(Keyword.intern("incremental"));
var clojureQualifier = (string)clojureVersion.valAt(Keyword.intern("qualifier"));
var supportedOps = new BDictionary {
{"eval", 1},
{"load-file", 1},
{"describe", 1},
{"clone", 1},
{"info", 1},
{"eldoc", 1},
{"classpath", 1},
};
// Debug.Log("Autocomplete support is enabled?: " + autoCompletionSupportEnabled);
if (autoCompletionSupportEnabled) {
supportedOps.Add("complete", 1);
}
SendMessage(
new BDictionary
{
{"id", message["id"]},
{"session", session.ToString()},
{"status", new BList {"done"}},
{
"ops",
new BDictionary
{
{"eval", 1},
{"load-file", 1},
{"describe", 1},
{"clone", 1},
{"info", 1},
}
},
{ "ops", supportedOps},
{
"versions",
new BDictionary
Expand Down Expand Up @@ -345,23 +392,47 @@ static void HandleMessage (BDictionary message, TcpClient client)
var loadFn = new EvalFn(message, client);
addCallbackVar.invoke(loadFn);
break;
case "eldoc":
case "info":
var symbolMetadata = (IPersistentMap)metaVar.invoke(nsResolveVar.invoke(
findNsVar.invoke(symbolVar.invoke(message["ns"].ToString())),
symbolVar.invoke(message["symbol"].ToString())));

String symbolStr = message["symbol"].ToString();

// Editors like Calva that support doc-on-hover sometimes will ask about empty strings or spaces
if (symbolStr == "" || symbolStr == null || symbolStr == " ") break;

IPersistentMap symbolMetadata = null;
try
{
symbolMetadata = (IPersistentMap)metaVar.invoke(nsResolveVar.invoke(
findNsVar.invoke(symbolVar.invoke(message["ns"].ToString())),
symbolVar.invoke(symbolStr)));
} catch (TypeNotFoundException) {
// We'll just ignore this call if the type cannot be found. This happens sometimes.
// TODO: One particular case when this happens is when querying info for a namespace.
// That case should be handled separately (e.g., via `find-ns`?)
}


if (symbolMetadata != null) {
var resultMessage = new BDictionary {
{"id", message["id"]},
{"session", session.ToString()},
{"status", new BList {"done"}}
};

foreach (var entry in symbolMetadata) {
if (entry.val() != null) {
resultMessage[entry.key().ToString().Substring(1)] =
new BString(entry.val().ToString());
String keyStr = entry.key().ToString().Substring(1);
String keyVal = entry.val().ToString();
if (keyStr == "arglists") {
keyStr = "arglists-str";
}
}
if (keyStr == "forms") {
keyStr = "forms-str";
}
resultMessage[keyStr] = new BString(keyVal);
}
}
SendMessage(resultMessage, client);
} else {
SendMessage(
Expand All @@ -373,6 +444,57 @@ static void HandleMessage (BDictionary message, TcpClient client)
}, client);
}
break;
case "complete":

// When autoCompletionSupportEnabled is false, we don't advertise auto-completion support.
// some editors seem to ignore this and request anyway, so we return an unknown op message.
if (!autoCompletionSupportEnabled) {
SendMessage(
new BDictionary
{
{"id", message["id"]},
{"session", session.ToString()},
{"status", new BList {"done", "error", "unknown-op"}}
}, client);
break;
}

Namespace ns = Namespace.find(Symbol.create(message["ns"].ToString()));
var sessionBindings = _sessions[session];
var completeBindings = sessionBindings;
if (ns != null) {
completeBindings = completeBindings.assoc(RT.CurrentNSVar, ns);
}

// Make sure to eval this in the right namespace
Var.pushThreadBindings(completeBindings);
BList completions = (BList) completeVar.invoke(message["symbol"].ToString());
Var.popThreadBindings();

SendMessage(new BDictionary
{
{"id", message["id"]},
{"session", session.ToString()},
{"status", new BList {"done"}},
{"completions", completions}
}, client);
break;
case "classpath":
BList classpath = new BList();
foreach (String p in Environment.GetEnvironmentVariable("CLOJURE_LOAD_PATH").Split(System.IO.Path.PathSeparator)) {
if (p != "") {
classpath.Add(Path.GetFullPath(p));
}
}

SendMessage(new BDictionary
{
{"id", message["id"]},
{"session", session.ToString()},
{"status", new BList {"done"}},
{"classpath", classpath},
}, client);
break;
default:
SendMessage(
new BDictionary
Expand Down
112 changes: 112 additions & 0 deletions Source/arcadia/internal/autocompletion.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
(ns arcadia.internal.autocompletion
(:require [clojure.main]))

;; This namespace has been adapted from a fork of the `clojure-complete` library by @sogaiu:
;; https://github.com/sogaiu/clojure-complete/blob/clr-support/src/complete/core.cljc
;; The original code was in turn adapted from swank-clojure (http://github.com/jochu/swank-clojure)
;; List of changes:
;; - Added support for keyword autocompletion
;; - Removed conditional reader tags

(defn namespaces
"Returns a list of potential namespace completions for a given namespace"
[ns]
(map name (concat (map ns-name (all-ns)) (keys (ns-aliases ns)))))

(defn ns-public-vars
"Returns a list of potential public var name completions for a given
namespace"
[ns]
(map name (keys (ns-publics ns))))

(defn ns-vars
"Returns a list of all potential var name completions for a given namespace"
[ns]
(for [[sym val] (ns-map ns) :when (var? val)]
(name sym)))

(defn ns-classes
"Returns a list of potential class name completions for a given namespace"
[ns]
(map name (keys (ns-imports ns))))

(def special-forms
(map name '[def if do let quote var fn loop recur throw try monitor-enter
monitor-exit dot new set!]))

(defn- static? [member]
(.IsStatic member))

(defn static-members
"Returns a list of potential static members for a given class"
[^System.RuntimeType class]
(for [member (concat (.GetMethods class)
(.GetFields class)
'()) :when (static? member)]
(.Name member)))

(defn resolve-class [sym]
(try (let [val (resolve sym)]
(when (class? val) val))
(catch Exception e
(when (not= clojure.lang.TypeNotFoundException
(class (clojure.main/repl-exception e)))
(throw e)))))

(defmulti potential-completions
(fn [^String prefix ns]
(cond (.StartsWith prefix ":") :keyword
(.Contains prefix "/") :scoped
(.Contains prefix ".") :class
:else :var)))

(defmethod potential-completions :scoped
[^String prefix ns]
(when-let [prefix-scope
(first (let [[x & _ :as pieces]
(.Split prefix (.ToCharArray "/"))]
(if (= x "")
'()
pieces)))]
(let [scope (symbol prefix-scope)]
(map #(str scope "/" %)
(if-let [class (resolve-class scope)]
(static-members class)
(when-let [ns (or (find-ns scope)
(scope (ns-aliases ns)))]
(ns-public-vars ns)))))))

(defmethod potential-completions :class
[^String prefix ns]
(concat (namespaces ns)))

(defmethod potential-completions :var
[_ ns]
(concat special-forms
(namespaces ns)
(ns-vars ns)
(ns-classes ns)))

(def sym-key-map
(-> clojure.lang.Keyword
(.GetField "_symKeyMap" (enum-or BindingFlags/NonPublic BindingFlags/Static))
(.GetValue nil)))

(defmethod potential-completions :keyword
[_ _]
(let [keyword-candidate-list
(->> sym-key-map
(.Values)
(map #(str (.Target %))))]
keyword-candidate-list))

(defn completions
"Return a sequence of matching completions given a prefix string and an
optional current namespace."
([prefix] (completions prefix *ns*))
([^String prefix ns]
(-> (for [^String completion (potential-completions prefix ns)
:when (.StartsWith completion prefix)]
completion)
distinct
sort)))
18 changes: 18 additions & 0 deletions Source/arcadia/internal/nrepl_support.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
(ns arcadia.internal.nrepl-support
(:require [arcadia.internal.autocompletion :as ac])
(:import [BList]
[BDictionary]))

(defn bencode-completion-result
"Converts a seq of completion maps into a BList of BDictionary"
[completions]
(let [blist (BList.)]
(doseq [candidate completions]
(.Add blist (doto (BDictionary.)
(.Add "candidate" candidate))))
blist))

(defn complete [^String prefix]
(bencode-completion-result
(ac/completions prefix)))

16 changes: 16 additions & 0 deletions configuration.edn
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,21 @@
;; see https://github.com/arcadia-unity/Arcadia/wiki/Stacktraces-and-Error-Reporting
;; for options on formatting thrown Exceptions
;; :error-options {:format true}


;; This boolean variable controls whether auto-completion support on an nrepl
;; connection is enabled. When enabled, the nREPL server will answer requests
;; to the "complete" message with the following information:
;;
;; - Autocomplete fns in current namespace: The server tries to complete all
;; the symbols available to the current context. That is, the public vars of
;; the current namespace, as well as any `use` or `:refer :all` imported symbols.
;; - Autocomplete fns from other namespaces: You can also autocomplete things
;; from other namespaces, both via alias, e.g. `str/split`, and fully
;; qualified name, e.g. `clojure.string/split`.
;; - Autocomplete namespaces: The name of namespaces is also autocompleted.
;; - Autocomplete keywords: Any keywords used (i.e., interned) in the project
;; are also autocompleted.
:nrepl-auto-completion true
}

0 comments on commit a0e20da

Please sign in to comment.