Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Multiple NRepls.cs improvements #380

Merged
merged 14 commits into from
Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 140 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,55 @@ 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":
// Debug.Log("Classpath op has been called");
string assetsPath = Path.Combine(Path.GetFullPath("."), "Assets");
string arcadiaSrcPath = Path.Combine(assetsPath, "Arcadia", "Source");
nasser marked this conversation as resolved.
Show resolved Hide resolved
// Debug.Log("assetsPath: " + assetsPath);
// Debug.Log("arcadiaSrcPath: " + arcadiaSrcPath);
SendMessage(new BDictionary
{
{"id", message["id"]},
{"session", session.ToString()},
{"status", new BList {"done"}},
{"classpath", new BList { assetsPath, arcadiaSrcPath }},
}, client);
break;
default:
SendMessage(
new BDictionary
Expand Down
68 changes: 68 additions & 0 deletions Source/arcadia/internal/nrepl_support.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
(ns arcadia.internal.nrepl-support
(:import [BList]
[BDictionary]))

(defn complete-symbol [text]
(let [[ns prefix-str] (as-> text <>
(symbol <>)
[(some-> <> namespace symbol) (name <>)])
ns-to-check (if ns
(or ((ns-aliases *ns*) ns) (find-ns ns))
*ns*)
fn-candidate-list (when ns-to-check
(if ns
(map str (keys (ns-publics ns-to-check)))
(map str (keys (ns-map ns-to-check)))))]
(into '() (comp (filter #(.StartsWith % prefix-str))
(map #(if ns (str ns "/" %) %))
(map #(-> {:candidate %
:type "function"})))
(concat
fn-candidate-list))))

(defn complete-namespace [text]
(let [[ns prefix-str] (as-> text <>
(symbol <>)
[(some-> <> namespace symbol) (name <>)])
ns-candidate-list (when-not ns
(map (comp str ns-name) (all-ns)))]
(into '() (comp (filter #(.StartsWith % prefix-str))
(map str)
(map #(-> {:candidate %
:type "namespace"})))
ns-candidate-list)))

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

(defn complete-keyword [text]
(let [keyword-candidate-list
;; NOTE: :_ is used here to get an instance to some keyword.
nasser marked this conversation as resolved.
Show resolved Hide resolved
(->> sym-key-map
(.Values)
(map #(str (.Target %))))]
(into '() (comp (filter #(.StartsWith % text))
(map #(-> {:candidate %
:type "keyword"})))
keyword-candidate-list)))

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

(defn complete [^String prefix]
(bencode-result
(cond
(.StartsWith prefix ":") (complete-keyword prefix)
(.Contains prefix "/") (complete-symbol prefix)
:else
(concat (complete-symbol prefix)
(complete-namespace 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
}