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 all 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
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
}