diff --git a/Editor/ArcadiaProjectInitialization.cs b/Editor/ArcadiaProjectInitialization.cs index 603a7b2f..c66ed35b 100644 --- a/Editor/ArcadiaProjectInitialization.cs +++ b/Editor/ArcadiaProjectInitialization.cs @@ -14,11 +14,12 @@ static ArcadiaProjectInitialization() public static void CheckSettings() { - Debug.Log("Checking Unity Settings..."); - if (!Application.unityVersion.StartsWith("2018")) // gross string comparison, not sure we can avoid - { - Debug.LogWarningFormat("Expected Unity version 2018.x, got {0}. This might cause issues.", Application.unityVersion); - } + + //Debug.Log("Checking Unity Settings..."); + //if (!Application.unityVersion.StartsWith("2018")) // gross string comparison, not sure we can avoid + //{ + //Debug.LogWarningFormat("Expected Unity version 2018.x, got {0}. This might cause issues.", Application.unityVersion); + //} if (PlayerSettings.GetApiCompatibilityLevel(BuildTargetGroup.Standalone) != ApiCompatibilityLevel.NET_4_6) { diff --git a/Editor/Initialization.cs b/Editor/Initialization.cs index 4d66a120..19c6baeb 100644 --- a/Editor/Initialization.cs +++ b/Editor/Initialization.cs @@ -98,7 +98,7 @@ public static void Initialize() NRepl.StartServer(); #endif StartNudge(); - Debug.Log("Arcadia Started!"); + //Debug.Log("Arcadia Started!"); initialized = true; } diff --git a/Editor/NRepl.cs b/Editor/NRepl.cs index dbead855..3368de97 100644 --- a/Editor/NRepl.cs +++ b/Editor/NRepl.cs @@ -1,5 +1,6 @@ #if NET_4_6 using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Concurrent; using System.IO; @@ -11,6 +12,7 @@ using BencodeNET.Objects; using UnityEngine; using clojure.lang; +using Microsoft.Scripting.Utils; // shim for atom proto-repl namespace java.io @@ -39,7 +41,6 @@ public override object invoke (object property) } } - namespace Arcadia { public class NRepl @@ -101,6 +102,7 @@ public override void Flush () private static Var concatVar; private static Var completeVar; private static Var configVar; + private static Var eldocMethodsVar; private static Namespace shimsNS; @@ -131,6 +133,7 @@ static NRepl () Util.require("arcadia.internal.nrepl-support"); completeVar = RT.var("arcadia.internal.nrepl-support", "complete"); + eldocMethodsVar = RT.var("arcadia.internal.nrepl-support", "eldoc-methods"); Util.require("arcadia.internal.config"); configVar = RT.var("arcadia.internal.config", "config"); @@ -205,7 +208,8 @@ public override object invoke () var sessionBindings = _sessions[session]; var outWriter = new Writer("out", _request, _client); var errWriter = new Writer("err", _request, _client); - + var isPprint = _request.TryGetValue("nrepl.middleware.print/print", out _); + // Split the path, and try to infer the ns from the filename. If the ns exists, then change the current ns before evaluating List nsList = new List(); Namespace fileNs = null; @@ -224,9 +228,9 @@ public override object invoke () // 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"); } @@ -244,7 +248,20 @@ public override object invoke () try { var form = readStringVar.invoke(readStringOptions, code); var result = evalVar.invoke(form); - var value = (string)prStrVar.invoke(result); + string value; + + // you get ultra rect if you'd eval something enormous or infinite + + if (isPprint) { + Util.require("clojure.pprint"); + var writer = new StringWriter(); + var prVar = RT.var("clojure.pprint", "pprint"); + prVar.invoke(result, writer); + value = writer.ToString(); + writer.Dispose(); + } else { + value = (string)prStrVar.invoke(result); + } star3Var.set(star2Var.deref()); star2Var.set(star1Var.deref()); @@ -313,8 +330,27 @@ public static void SendMessage (BDictionary message, TcpClient client) client.GetStream().Write(bytes, 0, bytes.Length); } + + static string getSymbolStr(BDictionary message) + { + String symbolStr = ""; + if (message.TryGetValue("symbol", out var s)) + { + symbolStr = s.ToString(); + } else if (message.TryGetValue("sym", out var s1)) { + symbolStr = s1.ToString(); + } else if (message.TryGetValue("prefix", out var s2)) { + symbolStr = s2.ToString(); + } + return symbolStr; + } + 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"))); @@ -332,7 +368,7 @@ static void HandleMessage (BDictionary message, TcpClient client) }, client); break; case "describe": - // TODO include arcadia version + // TODO include arcadia version var clojureVersion = (IPersistentMap)RT.var("clojure.core", "*clojure-version*").deref(); var clojureMajor = (int)clojureVersion.valAt(Keyword.intern("major")); var clojureMinor = (int)clojureVersion.valAt(Keyword.intern("minor")); @@ -395,23 +431,26 @@ static void HandleMessage (BDictionary message, TcpClient client) case "eldoc": case "info": - 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; + var symbolStr = NRepl.getSymbolStr(message); + // Editors like Calva that support doc-on-hover sometimes will ask about empty strings or spaces + if (symbolStr == "" || symbolStr == " ") break; IPersistentMap symbolMetadata = null; try { - symbolMetadata = (IPersistentMap)metaVar.invoke(nsResolveVar.invoke( - findNsVar.invoke(symbolVar.invoke(message["ns"].ToString())), - symbolVar.invoke(symbolStr))); - } catch (TypeNotFoundException) { + 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. + // 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) { + symbolMetadata = (IPersistentMap)eldocMethodsVar.invoke(symbolStr); + } + if (symbolMetadata != null) { var resultMessage = new BDictionary { @@ -424,15 +463,23 @@ static void HandleMessage (BDictionary message, TcpClient client) if (entry.val() != null) { String keyStr = entry.key().ToString().Substring(1); String keyVal = entry.val().ToString(); + if (keyStr == "arglists") { - keyStr = "arglists-str"; - } - if (keyStr == "forms") { - keyStr = "forms-str"; + // cider expects eldoc here in this format [["(foo)"]] + resultMessage["eldoc"] = new BList(((IEnumerable)entry.val()).Select(lst => new BList(((IEnumerable)lst).Select(o => o.ToString())))); + resultMessage["arglists-str"] = new BString(keyVal); + } else if (keyStr == "forms") { + resultMessage[keyStr] = new BList(new [] { keyVal}); + resultMessage["forms-str"] = new BString(keyVal); + } else if (keyStr == "doc") { + resultMessage[keyStr] = new BString(keyVal); + resultMessage["docstring"] = new BString(keyVal); + } else { + resultMessage[keyStr] = new BString(keyVal); } - resultMessage[keyStr] = new BString(keyVal); - } - } + + } + } SendMessage(resultMessage, client); } else { SendMessage( @@ -446,7 +493,7 @@ static void HandleMessage (BDictionary message, TcpClient client) break; case "complete": - // When autoCompletionSupportEnabled is false, we don't advertise auto-completion support. + // 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( @@ -468,7 +515,7 @@ static void HandleMessage (BDictionary message, TcpClient client) // Make sure to eval this in the right namespace Var.pushThreadBindings(completeBindings); - BList completions = (BList) completeVar.invoke(message["symbol"].ToString()); + BList completions = (BList) completeVar.invoke(getSymbolStr(message)); Var.popThreadBindings(); SendMessage(new BDictionary @@ -476,7 +523,7 @@ static void HandleMessage (BDictionary message, TcpClient client) {"id", message["id"]}, {"session", session.ToString()}, {"status", new BList {"done"}}, - {"completions", completions} + {"completions", completions} }, client); break; case "classpath": @@ -492,168 +539,169 @@ static void HandleMessage (BDictionary message, TcpClient client) {"id", message["id"]}, {"session", session.ToString()}, {"status", new BList {"done"}}, - {"classpath", classpath}, + {"classpath", classpath}, }, client); - break; - default: - SendMessage( - new BDictionary - { - {"id", message["id"]}, - {"session", session.ToString()}, - {"status", new BList {"done", "error", "unknown-op"}} - }, client); - break; - } - } - } - - private const int Port = 3722; - - public static void StopServer () - { - running = false; - } - - public static void StartServer () - { - Debug.Log("nrepl: starting"); - - running = true; - new Thread(() => { - var listener = new TcpListener(IPAddress.Loopback, Port); - try - { - // TODO make IPAddress.Loopback configurable to allow remote connections - listener.Start(); - Debug.LogFormat("nrepl: listening on port {0}", Port); - while (running) - { - if (!listener.Pending()) - { - Thread.Sleep(100); - continue; - } - - var client = listener.AcceptTcpClient(); - new Thread(() => - { - Debug.LogFormat("nrepl: connected to client {0}", client.Client.RemoteEndPoint); - var parser = new BencodeNET.Parsing.BencodeParser(); - var clientRunning = true; - var buffer = new byte[1024 * 8]; // 8k buffer - while (running && clientRunning) - { - // bencode needs a seekable stream to parse, so each - // message gets its own MemoryStream (MemoryStreams are - // seekable, NetworkStreams e.g. client.GetStream() are not) - try - { - using (var ms = new MemoryStream()) - { - // message might be bigger than our buffer - // loop till we have the whole thing - var parsedMessage = false; - while (!parsedMessage) - { - // copy from network stream into memory stream - var total = client.GetStream().Read(buffer, 0, buffer.Length); - if (total == 0) - { - // reading zero bytes after blocking means the other end has hung up - clientRunning = false; - break; - } - ms.Write(buffer, 0, total); - // bencode parsing expects stream position to be 0 - ms.Position = 0; - try - { - // try and parse the message and handle it - var obj = parser.Parse(ms); - parsedMessage = true; - var message = obj as BDictionary; - if (message != null) - { - try - { - HandleMessage(message, client); - } - catch (Exception e) - { - Debug.LogException(e); - } - } - } - catch (InvalidBencodeException e) - { - if (Encoding.UTF8.GetString(ms.GetBuffer()) - .Contains("2:op13:init-debugger")) - { - // hack to deal with cider sending us packets with duplicate keys - // BencodeNET cannot deal with duplicate keys, hence the string check - // the real solution is to switch to the bencode implementation that - // nrepl itself uses - parsedMessage = true; - } - else - { - // most likely an incomplete message. i kind - // of wish this was an EOF exception... we cannot - // actually tell the difference between an incomplete - // message and an invalid one as it stands - - // seek to the end of the MemoryStream to take on more bytes - ms.Seek(0, SeekOrigin.End); - } - } - } - } - } - catch (SocketException e) - { - // the other end has disconnected, gracefully shutdown - clientRunning = false; - } - catch (IOException e) - { - // the other end has disconnected, gracefully shutdown - clientRunning = false; - } - catch (ObjectDisposedException e) - { - // the other end has disconnected, gracefully shutdown - clientRunning = false; - } - catch (Exception e) - { - Debug.LogWarningFormat("nrepl: {0}", e); - clientRunning = false; - } - } - Debug.LogFormat("nrepl: disconnected from client {0}", client.Client.RemoteEndPoint); - client.Close(); - client.Dispose(); - }).Start(); - } - } - catch (ThreadAbortException) - { - // do nothing. this probably means the VM is being reset. - } - catch (Exception e) - { - Debug.LogException(e); - } - finally - { - Debug.LogFormat("nrepl: closing port {0}", Port); - listener.Stop(); - } - - - }).Start(); - } - } + break; + default: + SendMessage( + new BDictionary + { + {"id", message["id"]}, + {"session", session.ToString()}, + {"status", new BList {"done", "error", "unknown-op"}} + }, client); + break; + } + } + } + + + public static void StopServer () + { + running = false; + } + + public static void StartServer () { + + + var port = unchecked((int)(long)((IPersistentMap)configVar.invoke()).valAt(Keyword.intern("nrepl"))); + Debug.Log("nrepl: starting " + port); + + running = true; + new Thread(() => { + var listener = new TcpListener(IPAddress.Loopback, port); + try + { + // TODO make IPAddress.Loopback configurable to allow remote connections + listener.Start(); + Debug.LogFormat("nrepl: listening on port {0}", port); + while (running) + { + if (!listener.Pending()) + { + Thread.Sleep(100); + continue; + } + + var client = listener.AcceptTcpClient(); + new Thread(() => + { + Debug.LogFormat("nrepl: connected to client {0}", client.Client.RemoteEndPoint); + var parser = new BencodeNET.Parsing.BencodeParser(); + var clientRunning = true; + var buffer = new byte[1024 * 8]; // 8k buffer + while (running && clientRunning) + { + // bencode needs a seekable stream to parse, so each + // message gets its own MemoryStream (MemoryStreams are + // seekable, NetworkStreams e.g. client.GetStream() are not) + try + { + using (var ms = new MemoryStream()) + { + // message might be bigger than our buffer + // loop till we have the whole thing + var parsedMessage = false; + while (!parsedMessage) + { + // copy from network stream into memory stream + var total = client.GetStream().Read(buffer, 0, buffer.Length); + if (total == 0) + { + // reading zero bytes after blocking means the other end has hung up + clientRunning = false; + break; + } + ms.Write(buffer, 0, total); + // bencode parsing expects stream position to be 0 + ms.Position = 0; + try + { + // try and parse the message and handle it + var obj = parser.Parse(ms); + parsedMessage = true; + var message = obj as BDictionary; + if (message != null) + { + try + { + HandleMessage(message, client); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + } + catch (InvalidBencodeException e) + { + if (Encoding.UTF8.GetString(ms.GetBuffer()) + .Contains("2:op13:init-debugger")) + { + // hack to deal with cider sending us packets with duplicate keys + // BencodeNET cannot deal with duplicate keys, hence the string check + // the real solution is to switch to the bencode implementation that + // nrepl itself uses + parsedMessage = true; + } + else + { + // most likely an incomplete message. i kind + // of wish this was an EOF exception... we cannot + // actually tell the difference between an incomplete + // message and an invalid one as it stands + + // seek to the end of the MemoryStream to take on more bytes + ms.Seek(0, SeekOrigin.End); + } + } + } + } + } + catch (SocketException e) + { + // the other end has disconnected, gracefully shutdown + clientRunning = false; + } + catch (IOException e) + { + // the other end has disconnected, gracefully shutdown + clientRunning = false; + } + catch (ObjectDisposedException e) + { + // the other end has disconnected, gracefully shutdown + clientRunning = false; + } + catch (Exception e) + { + Debug.LogWarningFormat("nrepl: {0}", e); + clientRunning = false; + } + } + Debug.LogFormat("nrepl: disconnected from client {0}", client.Client.RemoteEndPoint); + client.Close(); + client.Dispose(); + }).Start(); + } + } + catch (ThreadAbortException) + { + // do nothing. this probably means the VM is being reset. + } + catch (Exception e) + { + Debug.LogException(e); + } + finally + { + Debug.LogFormat("nrepl: closing port {0}", port); + listener.Stop(); + } + + + }).Start(); + } + } } -#endif \ No newline at end of file +#endif diff --git a/Helpers/SocketREPLBootstrap.cs b/Helpers/SocketREPLBootstrap.cs index fc3fce47..2d78e1b5 100644 --- a/Helpers/SocketREPLBootstrap.cs +++ b/Helpers/SocketREPLBootstrap.cs @@ -47,12 +47,15 @@ static void DoInit() } } - public int port = 37221; private void Awake() { DoInit(); var addCallbackIFn = new AddCallbackFn(WorkQueue); + Var configVar = RT.var("arcadia.internal.config", "config"); + var port = unchecked((int)((IPersistentMap)configVar.invoke()).valAt(Keyword.intern("socket-repl"))); System.Console.WriteLine("[socket-repl] bootstrap awake, callback fn: {0} port: {1}, addr: {2}", addCallbackIFn, port, IPAddress.Any); + Debug.Log("socket: starting " + port); + var optionsMap = RT.mapUniqueKeys( portKeyword, port, argsKeyword, RT.vector(addCallbackIFn) diff --git a/Source/arcadia/internal/nrepl_support.clj b/Source/arcadia/internal/nrepl_support.clj index 52e66bdb..f0eb94f5 100644 --- a/Source/arcadia/internal/nrepl_support.clj +++ b/Source/arcadia/internal/nrepl_support.clj @@ -1,5 +1,7 @@ (ns arcadia.internal.nrepl-support - (:require [arcadia.internal.autocompletion :as ac]) + (:require + [arcadia.internal.autocompletion :as ac] + [arcadia.introspection :as i]) (:import [BList] [BDictionary])) @@ -16,3 +18,31 @@ (bencode-completion-result (ac/completions prefix))) + +(defn eldoc-methods + "Returns eldoc info data for static class members." + [symbol-str] + (when-let + [sym (symbol symbol-str)] + (when-let + [ns (namespace sym)]) + (let [methods + (i/methods-report + (resolve (symbol (namespace sym))) + (re-pattern (format "^%s$" (name sym))))] + (when (seq methods) + {:name + (:name (first methods)) + :arglists + (mapv + (fn [m] + (into [(:return-type m)] (:parameters m))) methods)})))) + +;; and now provide the doc string +;; by checking the assembly xml + +(comment + (eldoc-methods "fo") + (symbol "fo") + (namespace nil) + ) diff --git a/benjamin-todo.org b/benjamin-todo.org new file mode 100644 index 00000000..9322129b --- /dev/null +++ b/benjamin-todo.org @@ -0,0 +1,32 @@ + +When I connect with cider + +#+begin_src shell +TypeNotFoundException: Unable to find type: cljs.core +clojure.lang.RT.classForNameE (System.String p) (at :0) +clojure.lang.CljCompiler.Ast.HostExpr.MaybeType (System.Object form, System.Boolean stringOk) (at :0) +clojure.lang.Compiler.AnalyzeSymbol (clojure.lang.Symbol symbol) (at :0) +clojure.lang.Compiler.Analyze (clojure.lang.CljCompiler.Ast.ParserContext pcontext, System.Object form, System.String name) (at :0) +Rethrow as CompilerException: Unable to find type: cljs.core, compiling: (NO_SOURCE_PATH:0:0) +clojure.lang.Compiler.Analyze (clojure.lang.CljCompiler.Ast.ParserContext pcontext, System.Object form, System.String name) (at :0) +clojure.lang.Compiler.Analyze (clojure.lang.CljCompiler.Ast.ParserContext pcontext, System.Object form) (at :0) +clojure.lang.Compiler.eval (System.Object form) (at :0) +clojure$core$eval__22334.invokeStatic (System.Object ) (at <75aedcbf6ad14e4293bea1888c10ab1d>:0) +clojure$core$eval__22334.invoke (System.Object ) (at <75aedcbf6ad14e4293bea1888c10ab1d>:0) +clojure.lang.Var.invoke (System.Object arg1) (at :0) +Arcadia.NRepl+EvalFn.invoke () (at Assets/Arcadia/Editor/NRepl.cs:316) +arcadia$internal$callbacks$run_callbacks__4717.invokeStatic (System.Object , System.Object ) (at <20b9304908c1491892b347793d1cbf65>:0) +arcadia$internal$callbacks$run_callbacks__4717.invoke (System.Object , System.Object ) (at <20b9304908c1491892b347793d1cbf65>:0) +arcadia$internal$editor_callbacks$run_callbacks__4811.invokeStatic () (at <20b9304908c1491892b347793d1cbf65>:0) +arcadia$internal$editor_callbacks$run_callbacks__4811.invoke () (at <20b9304908c1491892b347793d1cbf65>:0) +clojure.lang.Var.invoke () (at :0) +Arcadia.EditorCallbacks.RunCallbacks () (at Assets/Arcadia/Editor/EditorCallbacks.cs:24) +UnityEditor.EditorApplication.Internal_CallUpdateFunctions () (at /home/bokken/buildslave/unity/build/Editor/Mono/EditorApplication.cs:200) +#+end_src + +this is because cider pokes the nrepl process to check if cljs is +available. +Nrepl should just reply with 'no'? + +This is toleratable right now. Does not seem to brake anything. But +sad for a newcommer using cider and being confused by an error. diff --git a/configuration.edn b/configuration.edn index 55f95bb9..a49254c8 100644 --- a/configuration.edn +++ b/configuration.edn @@ -8,8 +8,8 @@ ;; loading on change (see :reload-on-change below) and responding to ;; changes in configuration.edn without a restart :reactive true - - ;; map of https://clojuredocs.org/clojure.core/*compiler-options* + + ;; map of https://clojuredocs.org/clojure.core/*compiler-options* ;; Example: {:elide-meta [:doc :file :line :added :column :arglists]} :compiler-options {} @@ -44,17 +44,17 @@ ;; Value means: ;; ;; `true`: make default socket-repl (localhost, port 5555) - ;; `false`: stop ALL socket repls + ;; `false`: stop ALL socket repls ;; a map: opts for `arcadia.internal.socket-repl/server-socket`. Means at ;; least the specified server is running. - ;; + ;; ;; In the future, a non-map collection of maps (eg, a sequence of ;; maps) should specify several servers that should at least be ;; running. `true` as element should be default socket-repl. ;; ;; Only `false` stops servers; `nil` is ignored (means _at least_ no ;; servers are running). - + ;; :socket-repl true ;; see https://github.com/arcadia-unity/Arcadia/wiki/Stacktraces-and-Error-Reporting @@ -76,5 +76,7 @@ ;; - Autocomplete keywords: Any keywords used (i.e., interned) in the project ;; are also autocompleted. :nrepl-auto-completion true - } + + :nrepl 3722 + :socket-repl 37221}