From 24b79f82eb46b9bd8ad324e1a3ad017e3e3a6945 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Fri, 26 Feb 2021 16:01:21 +0000 Subject: [PATCH 01/32] Add pkg.json file This tells `dylan-tool update` what the dependencies are. --- pkg.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 pkg.json diff --git a/pkg.json b/pkg.json new file mode 100644 index 0000000..8cdaec8 --- /dev/null +++ b/pkg.json @@ -0,0 +1,8 @@ +{ + "name": "lsp-dylan", + "deps": [ + "json head", + "vscode-dylan head" + ], + "location": "git@github.com:pedro-w/lsp-dylan" +} From a824d9c1c19764800f603ebc7d0a89ecee7023c3 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Fri, 26 Feb 2021 16:13:21 +0000 Subject: [PATCH 02/32] Updates for latest json API --- jsonrpc.dylan | 15 +++++++++++---- lsp-dylan.dylan | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/jsonrpc.dylan b/jsonrpc.dylan index ed5f044..1e02c37 100644 --- a/jsonrpc.dylan +++ b/jsonrpc.dylan @@ -6,6 +6,13 @@ Copyright: 2020 // Headers for the JSONRPC call define variable $content-length = "Content-Length"; +define function print-json-to-string + (object, #rest args) => (json :: ) + with-output-to-string (s) + apply(print-json, object, s, args) + end +end function; + // Read the header part from a stream and return a // table of the (key, value) pairs. // Returns #f on error. @@ -87,7 +94,7 @@ end method read-json-message; * We always assume the default encoding. */ define method write-json-message(stream :: , json :: ) => () - let str :: = encode-json-to-string(json); + let str :: = print-json-to-string(json); let content-length = size(str); write(stream, $content-length); write(stream, ": "); @@ -274,7 +281,7 @@ define method send-request (session :: , end if; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send request: %s\n", encode-json-to-string(message)); + local-log("Server: send request: %s\n", print-json-to-string(message)); end if; end method; @@ -286,7 +293,7 @@ define method send-response(session :: , message["result"] := result; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send response %s\n", encode-json-to-string(message)); + local-log("Server: send response %s\n", print-json-to-string(message)); end if; end method; @@ -306,7 +313,7 @@ define method send-error-response(session :: , message["error"] := params; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send error response: %s\n", encode-json-to-string(message)); + local-log("Server: send error response: %s\n", print-json-to-string(message)); end; end method; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 3a1f27e..706091c 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -214,7 +214,7 @@ define function handle-workspace/didChangeConfiguration(session :: , // emacs does not, so we need to ask for config items ourselves and // not wait to be told. local-log("Did change configuration\n"); - local-log("Settings: %s\n", encode-json-to-string(params)); + local-log("Settings: %s\n", print-json-to-string(params)); // TODO do something with this info. let settings = params["settings"]; let dylan-settings = settings["dylan"]; From 673914c0da5c2556e6a07045ed55e1e1f2f6e3ed Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 27 Feb 2021 15:58:28 +0000 Subject: [PATCH 03/32] Whitespace cleanup --- compiler.dylan | 4 ++-- jsonrpc.dylan | 18 +++++++++--------- lsp-dylan.dylan | 19 +++++++++---------- vscode-dylan | 2 +- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/compiler.dylan b/compiler.dylan index ff046d0..601e4be 100644 --- a/compiler.dylan +++ b/compiler.dylan @@ -143,7 +143,7 @@ define function get-environment-object (symbol-name, #key module = #f) local-log("%s -> module is %s\n", symbol-name, n(module)); find-environment-object(*project*, symbol-name, - library: library, - module: module); + library: library, + module: module); end; diff --git a/jsonrpc.dylan b/jsonrpc.dylan index 1e02c37..875dd72 100644 --- a/jsonrpc.dylan +++ b/jsonrpc.dylan @@ -174,7 +174,7 @@ define generic send-error-response error-data :: = #f) => (); -/* +/* * Send an LSP notification-type message. * This has a method name but no ID because it isn't replied to */ @@ -183,12 +183,12 @@ define generic send-notification(session :: , params :: ) => (); -/* +/* * Get the next message. * If the message is a notification or request, return it * for processing. If it is a response to a request sent * by the server, look up the reponse callback and call it. -*/ +*/ define generic receive-message (session :: ) => (method-name :: , id :: , params :: ); @@ -224,7 +224,7 @@ define method send-notification(session :: , send-raw-message(session, message); if (*trace-messages*) local-log("Server: send notification '%s'\n", method-name); - end; + end; end method; /** receive a request or response. @@ -247,14 +247,14 @@ define method receive-message (session :: ) else local-log("Server: receive notification '%s'\n", method-name); end if; - end; + end; // Received a request or notification return (method-name, id, params); else // Received a response if (*trace-messages*) local-log("Server: receive response (%s)\n", id); - end; + end; let func = element(session.callbacks, id, default: #f); if (func) remove-key!(session.callbacks, id); @@ -282,7 +282,7 @@ define method send-request (session :: , send-raw-message(session, message); if (*trace-messages*) local-log("Server: send request: %s\n", print-json-to-string(message)); - end if; + end if; end method; define method send-response(session :: , @@ -314,10 +314,10 @@ define method send-error-response(session :: , send-raw-message(session, message); if (*trace-messages*) local-log("Server: send error response: %s\n", print-json-to-string(message)); - end; + end; end method; -/* +/* * A session communicating over standard in/out. * This is the only one implemented for now. */ diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 706091c..8025dd8 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -196,7 +196,7 @@ define function handle-textDocument/definition(session :: , end; let symbol = symbol-at-position(doc, l, c); let (target, line, char) = lookup-symbol(session, symbol, module: doc.document-module); - if (target) + if (target) local-log("Lookup %s and got target=%s, line=%d, char=%d\n", symbol, target, line, char); let uri = make-file-uri(target); // TODO location := make-location(as(, uri), line, char); @@ -234,7 +234,7 @@ end; /* Handler for 'initialized' message. * Here we will register the dynamic capabilities of the server with the client. - * Note we don't do this yet, any capabilities are registered statically in the + * Note we don't do this yet, any capabilities are registered statically in the * 'initialize' message. * Here also we will start the compiler session. */ @@ -267,7 +267,7 @@ define function handle-initialized(session :: , test-open-project(session); end function handle-initialized; -define function test-open-project(session) => () +define function test-open-project(session) => () // TODO don't hard-code the project name and module name. local-log("Select project %=\n", find-project-name()); @@ -288,12 +288,11 @@ define function test-open-project(session) => () local-log("Test, listing sources:\n"); for (s in project-sources(*project*)) let rl = source-record-location(s); - local-log("Source: %=, a %= in %= \n", - s, - object-class(s), - as(, rl)); - - end; + local-log("Source: %=, a %= in %=", + s, + object-class(s), + as(, rl)); + end; local-log("Test, listing project file libraries\n"); do-project-file-libraries(method(l, r) local-log("Lib:%= Rec:%=\n", l, r); @@ -484,7 +483,7 @@ define function find-project-name() *project-name*; else // Guess based on there being one .lid file in the workspace root - block(return) + block(return) local method return-lid(dir, name, type) local-log("Project scan %s\n", name); if (type = #"file") diff --git a/vscode-dylan b/vscode-dylan index 7be4ff4..e11e228 160000 --- a/vscode-dylan +++ b/vscode-dylan @@ -1 +1 @@ -Subproject commit 7be4ff489d0ecd89bc0af8315ad21109b1f6d398 +Subproject commit e11e22873749045da2e55b99be4d09fec15f71c5 From 72f5f5e14010190633ae4a68b51d5771f94add9c Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 27 Feb 2021 16:59:06 +0000 Subject: [PATCH 04/32] Log all messages sent/received verbatim A couple of minor cleanups also. --- jsonrpc.dylan | 48 ++++++++++++++++++++++-------------------------- lsp-dylan.dylan | 8 +++++--- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/jsonrpc.dylan b/jsonrpc.dylan index 875dd72..0e8afee 100644 --- a/jsonrpc.dylan +++ b/jsonrpc.dylan @@ -16,7 +16,7 @@ end function; // Read the header part from a stream and return a // table of the (key, value) pairs. // Returns #f on error. -define function headers(stm) +define function read-headers(stm) // First read lines up to the blank line let lines = block(return) @@ -51,6 +51,7 @@ define function headers(stm) #f end if end function; + /** * Make a string-table from a sequence of key value pairs. * This is just for convenience. @@ -69,20 +70,20 @@ end function; define function dump(t :: ) => () format(*standard-error*, "Table Dump\n==========\n"); - for (v keyed-by k in key-sequence(t)) - format(*standard-error*, - "%s-->%s(%s)\n", - k, - v, object-class(v)); + for (v keyed-by k in t) + format(*standard-error*, "%s-->%s(%s)\n", k, v, object-class(v)); end for; end; define method read-json-message(stream :: ) => (json :: ) - let hdrs = headers(stream); + let hdrs = read-headers(stream); if (hdrs) let content-length = element(hdrs, $content-length, default: "0"); let content-length = string-to-integer(content-length); let data = read(stream, content-length); + if (*trace-messages*) + local-log("received message: %s\n", data); + end; parse-json(data); else #f @@ -93,14 +94,14 @@ end method read-json-message; * See: https://microsoft.github.io/language-server-protocol/specification#headerPart * We always assume the default encoding. */ -define method write-json-message(stream :: , json :: ) => () - let str :: = print-json-to-string(json); - let content-length = size(str); +define method write-json-message + (stream :: , json :: ) => () + let content-length = size(json); write(stream, $content-length); write(stream, ": "); write(stream, integer-to-string(content-length)); write(stream, "\r\n\r\n"); - write(stream, str); + write(stream, json); end method; // This is just a table that uses \= to compare @@ -200,10 +201,9 @@ define generic flush(session :: ) /* * Make the 'skeleton' of a JSONRPC 2.0 message. -*/ -define function make-message(#key method-name = #f, id = #f) - let msg = make(); - msg["jsonrpc"] := "2.0"; + */ +define function make-message(#key method-name, id) + let msg = json("jsonrpc", "2.0"); if (method-name) msg["method"] := method-name; end; @@ -241,13 +241,6 @@ define method receive-message (session :: ) let id = element(message, "id", default: #f); let params = element(message, "params", default: #f); if (method-name) - if (*trace-messages*) - if (id) - local-log("Server: receive request '%s - (%s)'\n", method-name, id); - else - local-log("Server: receive notification '%s'\n", method-name); - end if; - end; // Received a request or notification return (method-name, id, params); else @@ -304,9 +297,8 @@ define method send-error-response(session :: , error-data :: = #f) => () let message = make-message(id: id); - let params = make(); - params["code"] := error-code; - params["message"] := error-message | default-error-message(error-code); + let params = json("code", error-code, + "message", error-message | default-error-message(error-code)); if (error-data) params["data"] := error-data; end if; @@ -327,7 +319,11 @@ end class; define method send-raw-message(session :: , message :: ) => () - write-json-message(*standard-output*, message); + let str :: = print-json-to-string(message); + if (*trace-messages*) + local-log("sending message: %s\n", str); + end; + write-json-message(*standard-output*, str); end method; define method receive-raw-message(session :: ) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 8025dd8..bca15ae 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -233,14 +233,16 @@ define function trailing-slash(s :: ) => (s-with-slash :: ) end; /* Handler for 'initialized' message. + * + * Example: {"jsonrpc":"2.0","method":"initialized","params":{}} + * * Here we will register the dynamic capabilities of the server with the client. * Note we don't do this yet, any capabilities are registered statically in the * 'initialize' message. * Here also we will start the compiler session. */ -define function handle-initialized(session :: , - id :: , - params :: ) => () +define function handle-initialized + (session :: , id :: , params :: ) => () /* Commented out because we don't need to do this (yet) let hregistration = json("id", "dylan-reg-hover", "method", "textDocument/hover"); From 95dd6137ad87b16c663e06422e5159689a1e90ef Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 27 Feb 2021 19:20:05 +0000 Subject: [PATCH 05/32] Remove newline from end of log messages --- compiler.dylan | 24 ++++++++--------- jsonrpc.dylan | 14 +++++----- lsp-dylan.dylan | 71 ++++++++++++++++++++++++++----------------------- 3 files changed, 57 insertions(+), 52 deletions(-) diff --git a/compiler.dylan b/compiler.dylan index 601e4be..5bfee7f 100644 --- a/compiler.dylan +++ b/compiler.dylan @@ -35,8 +35,8 @@ define function open-project(server, name :: ) server: server.server-context, file: as(, name)); let project = execute-command(command); - local-log("Result of opening %s is %=\n", name, project); - local-log("Result of find %s is %=\n", project-name(project), + local-log("Result of opening %s is %=", name, project); + local-log("Result of find %s is %=", project-name(project), find-project(project-name(project))); project end function; @@ -52,7 +52,7 @@ define function symbol-location (symbol-name, #key module = #f) if (env) environment-object-source-location(*project*, env) else - local-log("No environment object for %s in module %s\n", symbol-name, module); + local-log("No environment object for %s in module %s", symbol-name, module); end end function; @@ -61,7 +61,7 @@ define function list-all-package-names () (dir :: , filename :: , type :: ) if (type == #"file") if (last(filename) ~= '~') - local-log("%s\n", filename); + local-log("%s", filename); end; end; end; @@ -80,7 +80,7 @@ define function one-off-debug() let out-stream = make(, direction: #"output"); let srv = start-compiler(in-stream, out-stream); let project = open-project(srv, "testproject"); - local-log("Database: %=\n", project-compiler-database(project)); + local-log("Database: %=", project-compiler-database(project)); *project* := project; *server* := srv; @@ -92,7 +92,7 @@ let loc = environment-object-source-location(project, module).source-location-so symbol-name, library: library, module: module); - local-log("one-off-debug:\nfind-environment-object(%s, %s, library:%s, module:%s) => %=\n", + local-log("one-off-debug:\n find-environment-object(%s, %s, library:%s, module:%s) => %=", n(project), n(symbol-name), n(library), @@ -103,18 +103,18 @@ let loc = environment-object-source-location(project, module).source-location-so // fp := "testproject.dylan"; // fp := pfl; let (m, l) = file-module(project, fp); - local-log("one-off-debug:\n1. %=\n2. %=\n3. %=\n", n(fp), n(m), n(l)); + local-log("one-off-debug:\n 1. %=\n 2. %=\n3. %=", n(fp), n(m), n(l)); let same? = pfl = fp; - local-log("one-off-debug:\n1. %=\n2. %=\n", + local-log("one-off-debug:\n 1. %=\n 2. %=\n", locator-relative?(fp), locator-relative?(pfl)); - local-log("one-off-debug:\n1. %=\n2. %=\n", + local-log("one-off-debug:\n 1. %=\n 2. %=\n", locator-base(fp), locator-base(pfl)); - local-log("one-off-debug:\n1. %=\n2. %=\n", + local-log("one-off-debug:\n 1. %=\n 2. %=\n", locator-extension(fp), locator-extension(pfl)); - local-log("one-off-debug:\n1. %=\n2. %=\n3.%=\n", + local-log("one-off-debug:\n 1. %=\n 2. %=\n 3.%=\n", locator-path(fp), locator-path(pfl), same?); @@ -140,7 +140,7 @@ define function get-environment-object (symbol-name, #key module = #f) // TODO not hard code module := find-module(*project*, "testproject", library: library); end; - local-log("%s -> module is %s\n", symbol-name, n(module)); + local-log("%s -> module is %s", symbol-name, n(module)); find-environment-object(*project*, symbol-name, library: library, diff --git a/jsonrpc.dylan b/jsonrpc.dylan index 0e8afee..112b28e 100644 --- a/jsonrpc.dylan +++ b/jsonrpc.dylan @@ -82,7 +82,7 @@ define method read-json-message(stream :: ) => (json :: ) let content-length = string-to-integer(content-length); let data = read(stream, content-length); if (*trace-messages*) - local-log("received message: %s\n", data); + local-log("received message: %s", data); end; parse-json(data); else @@ -223,7 +223,7 @@ define method send-notification(session :: , end; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send notification '%s'\n", method-name); + local-log("Server: send notification '%s'", method-name); end; end method; @@ -246,7 +246,7 @@ define method receive-message (session :: ) else // Received a response if (*trace-messages*) - local-log("Server: receive response (%s)\n", id); + local-log("Server: receive response (%s)", id); end; let func = element(session.callbacks, id, default: #f); if (func) @@ -274,7 +274,7 @@ define method send-request (session :: , end if; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send request: %s\n", print-json-to-string(message)); + local-log("Server: send request: %s", print-json-to-string(message)); end if; end method; @@ -286,7 +286,7 @@ define method send-response(session :: , message["result"] := result; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send response %s\n", print-json-to-string(message)); + local-log("Server: send response %s", print-json-to-string(message)); end if; end method; @@ -305,7 +305,7 @@ define method send-error-response(session :: , message["error"] := params; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send error response: %s\n", print-json-to-string(message)); + local-log("Server: send error response: %s", print-json-to-string(message)); end; end method; @@ -321,7 +321,7 @@ define method send-raw-message(session :: , => () let str :: = print-json-to-string(message); if (*trace-messages*) - local-log("sending message: %s\n", str); + local-log("sending message: %s", str); end; write-json-message(*standard-output*, str); end method; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index bca15ae..23f0ac6 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -51,7 +51,7 @@ define inline method show-log(session :: , end method; define function local-log(m :: , #rest params) => () - apply(format, *standard-error*, m, params); + apply(format, *standard-error*, concatenate("local-log: ", m, "\n"), params); force-output(*standard-error*); end function; @@ -108,7 +108,7 @@ define function handle-workspace/symbol (session :: , => () // TODO this is only a dummy let query = params["query"]; - local-log("Query: %s\n", query); + local-log("Query: %s", query); let range = make-range(make-position(0, 0), make-position(0,5)); let symbols = list(json("name", "a-name", "kind", 13, @@ -151,7 +151,7 @@ define function handle-textDocument/didOpen(session :: , let languageId = textDocument["languageId"]; let version = textDocument["version"]; let text = textDocument["text"]; - local-log("textDocument/didOpen: File %s of type %s, version %s, length %d\n", + local-log("textDocument/didOpen: File %s of type %s, version %s, length %d", uri, languageId, version, size(text)); // Only bother about dylan files for now. if (languageId = "dylan") @@ -164,7 +164,7 @@ define function handle-textDocument/didOpen(session :: , let u = as(, uri); let f = make-file-locator(u); let (m, l) = file-module(*project*, f); - local-log("File: %=\nModule: %=, Library: %=\n", + local-log("File: %= Module: %=, Library: %=", as(, f), if (m) environment-object-primitive-name(*project*, m) else "?" end, if (l) environment-object-primitive-name(*project*, l) else "?" end); @@ -188,20 +188,20 @@ define function handle-textDocument/definition(session :: , let local-dir = make(, path: locator-path(doc.document-uri)); let local-file = make(, directory: local-dir, name: locator-name(doc.document-uri)); - local-log("local-dir=%s\nlocal-file=%s\n", as(, local-dir), - as(, local-file)); + local-log("local-dir=%s", as(, local-dir)); + local-log("local-file=%s", as(, local-file)); let (mod, lib) = file-module(*project*, local-file); - local-log("module=%s\nlibrary=%s\n", mod, lib); + local-log("module=%s, library=%s", mod, lib); doc.document-module := mod; end; let symbol = symbol-at-position(doc, l, c); let (target, line, char) = lookup-symbol(session, symbol, module: doc.document-module); if (target) - local-log("Lookup %s and got target=%s, line=%d, char=%d\n", symbol, target, line, char); + local-log("Lookup %s and got target=%s, line=%d, char=%d", symbol, target, line, char); let uri = make-file-uri(target); // TODO location := make-location(as(, uri), line, char); else - local-log("Lookup %s, not found\n", symbol); + local-log("Lookup %s, not found", symbol); end; end; send-response(session, id, location); @@ -213,8 +213,8 @@ define function handle-workspace/didChangeConfiguration(session :: , // NOTE: vscode always sends this just after initialized, whereas // emacs does not, so we need to ask for config items ourselves and // not wait to be told. - local-log("Did change configuration\n"); - local-log("Settings: %s\n", print-json-to-string(params)); + local-log("Did change configuration"); + local-log("Settings: %s", print-json-to-string(params)); // TODO do something with this info. let settings = params["settings"]; let dylan-settings = settings["dylan"]; @@ -251,17 +251,17 @@ define function handle-initialized send-request(session, "client/registerCapability", json("registrations", list(hregistration, oregistration)), callback: method(session, params) - local-log("Callback called back..%s\n", session); + local-log("Callback called back..%s", session); show-info(session, "Thanks la") end); */ show-info(session, "Dylan LSP server started."); - local-log("debug: %s, messages: %s, verbose: %s\n", *debug-mode*, *trace-messages*, *trace-verbose*); + local-log("debug: %s, messages: %s, verbose: %s", *debug-mode*, *trace-messages*, *trace-verbose*); let in-stream = make(); let out-stream = make(, direction: #"output"); // Test code - local-log("Env O-D-R=%s, PATH=%s\n", + local-log("Env O-D-R=%s, PATH=%s", environment-variable("OPEN_DYLAN_RELEASE"), environment-variable("PATH")); send-request(session, "workspace/workspaceFolders", #f, callback: handle-workspace/workspaceFolders); @@ -271,12 +271,12 @@ end function handle-initialized; define function test-open-project(session) => () // TODO don't hard-code the project name and module name. - local-log("Select project %=\n", find-project-name()); + local-log("Select project %=", find-project-name()); *project* := open-project(*server*, find-project-name()); - // Let's see if we can find a module + // Let's see if we can find a module let (m, l) = file-module(*project*, "library.dylan"); - local-log("Try\nModule: %=, Library: %=\n", + local-log("Try Module: %=, Library: %=", if (m) environment-object-primitive-name(*project*, m) else "?" end, if (l) environment-object-primitive-name(*project*, l) else "?" end); @@ -286,8 +286,8 @@ define function test-open-project(session) => () local-log("Warn: %=\n", w); end; let db = open-project-compiler-database(*project*, warning-callback: wrn); - local-log("Test, Database: %=\n", db); - local-log("Test, listing sources:\n"); + local-log("Test, Database: %=", db); + local-log("Test, listing sources:"); for (s in project-sources(*project*)) let rl = source-record-location(s); local-log("Source: %=, a %= in %=", @@ -295,9 +295,9 @@ define function test-open-project(session) => () object-class(s), as(, rl)); end; - local-log("Test, listing project file libraries\n"); - do-project-file-libraries(method(l, r) - local-log("Lib:%= Rec:%=\n", l, r); + local-log("Test, listing project file libraries"); + do-project-file-libraries(method (l, r) + local-log("Lib:%= Rec:%=", l, r); end, *project*, as(, "library.dylan")); @@ -305,8 +305,8 @@ define function test-open-project(session) => () local-log("Test, Project did't open\n"); end if; // local-log("dylan-sources:%=\n", project-dylan-sources(*project*)); - local-log("Compiler started:%=\nProject %=\n", *server*, *project*); - local-log("Database: %=\n", project-compiler-database(*project*)); + local-log("Compiler started:%=, Project %=", *server*, *project*); + local-log("Database: %=", project-compiler-database(*project*)); end function; define function add-trailing-slash(s :: ) => (s-slash :: ) @@ -340,7 +340,7 @@ define function handle-initialize (session :: , *trace-messages* := #t; *trace-verbose* := #t; end; - otherwise => local-log("trace must be \"off\", \"messages\" or \"verbose\", not %s\n", trace); + otherwise => local-log("trace must be \"off\", \"messages\" or \"verbose\", not %s", trace); end select; // Save the workspace root (if provided) for later. // TODO: can root-uri be something that's not a file:// URL? @@ -359,7 +359,7 @@ define function handle-initialize (session :: , if (session.root) working-directory() := session.root; end; - local-log("Working directory is now:%s\n", as(, working-directory())); + local-log("Working directory is now:%s", as(, working-directory())); // Return the capabilities of this server let capabilities = json("hoverProvider", #f, "textDocumentSync", 1, @@ -375,10 +375,12 @@ define function handle-workspace/workspaceFolders (session :: , params :: ) => () // TODO: handle multi-folder workspaces. - local-log("Workspace folders were received\n"); + local-log("Workspace folders were received"); end; + /* Document Management */ define constant $documents = make(); + // Represents one open file (given to us by textDocument/didOpen) define class () constant slot document-uri, required-init-keyword: uri:; @@ -388,7 +390,7 @@ end class; define function register-file (uri, contents) let lines = split-lines(contents); - local-log("register-file: %s(%s), lines: %d\n", uri, object-class(uri), size(lines)); + local-log("register-file: %s(%s), lines: %d", uri, object-class(uri), size(lines)); let doc = make(, uri: as(, uri), lines: lines); $documents[uri] := doc; end function; @@ -465,7 +467,7 @@ define function lookup-symbol (session, symbol, #key module = #f) => (doc, line, let column = loc.source-location-start-column; values(absolute-path, line - 1, column) else - local-log("Looking up %s, not found\n", symbol); + local-log("Looking up %s, not found", symbol); #f end end; @@ -481,13 +483,13 @@ define function find-project-name() => (name :: false-or()) if (*project-name*) // We've set it explicitly - local-log("Project name explicitly:%s\n", *project-name*); + local-log("Project name explicitly:%s", *project-name*); *project-name*; else // Guess based on there being one .lid file in the workspace root block(return) local method return-lid(dir, name, type) - local-log("Project scan %s\n", name); + local-log("Project scan %s", name); if (type = #"file") let file = as(, name); if (locator-extension(file) = "lid") @@ -496,7 +498,7 @@ define function find-project-name() end if; end method; do-directory(return-lid, working-directory()); - local-log("Project name, got nothing\n"); + local-log("Project name, got nothing"); #f end block; end if; @@ -516,6 +518,7 @@ define function main let session = make(); // Pre-init state while (session.state == $session-preinit) + local-log("entered pre-init state"); let (meth, id, params) = receive-message(session); select (meth by =) "initialize" => handle-initialize(session, id, params); @@ -530,6 +533,7 @@ define function main end while; // Active state while (session.state == $session-active) + local-log("entered active state"); let (meth, id, params) = receive-message(session); select (meth by =) "initialize" => @@ -552,7 +556,7 @@ define function main // Respond to any other request with an not-implemented error. // Drop any other notifications begin - local-log("%s '%s' is not implemented\n", + local-log("%s '%s' is not implemented", if (id) "Request" else @@ -568,6 +572,7 @@ define function main end while; // Shutdown state while (session.state == $session-shutdown) + local-log("entered shutdown state"); let (meth, id, params) = receive-message(session); select (meth by =) "exit" => From 89e085ac890542bde1fa94f15e38cce0be25d5a4 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 27 Feb 2021 19:21:43 +0000 Subject: [PATCH 06/32] Alphabetize imports, easier to scan --- library.dylan | 62 +++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/library.dylan b/library.dylan index 8c97b5b..2f8bc28 100644 --- a/library.dylan +++ b/library.dylan @@ -1,52 +1,52 @@ Module: dylan-user define library lsp-dylan + use build-system; + use commands; use common-dylan; - use io; - use network; - use lisp-reader; - use json; - use system; + use dfmc-back-end-implementations; + use dfmc-reader; use dylan; - use strings; use environment-commands; - use environment-protocols; - use build-system; - use commands; use environment-internal-commands; - use dfmc-reader; - use source-records; + use environment-protocols; use file-source-records; - use system; + use io; + use json; + use lisp-reader; + use network; use registry-projects; - //use stack-walker; use release-info; - use dfmc-back-end-implementations; + use source-records; + //use stack-walker; + use strings; + use system; + use system; end library; define module lsp-dylan + use build-system; + use command-lines; + use commands; use common-dylan; + use dfmc-reader; + use environment-commands; + use environment-protocols, + exclude: { open-project, + application-filename, + application-arguments, + run-application}; + use file-source-records; + use file-system; use format-out; use format; - use standard-io; - use streams; - use file-system; - use locators; use json; - use threads; + use locators; use operating-system; - use strings; - use command-lines; - use environment-commands; - use environment-protocols, exclude: { - open-project, - application-filename, - application-arguments, - run-application}; - use commands; use registry-projects; - use build-system; - use dfmc-reader; use source-records; - use file-source-records; + use standard-io; + use streams; + use strings; + use threads; end module lsp-dylan; From 917c7e2dd82ba08579e0aa0c2cfe3b02a5affc2a Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 27 Feb 2021 22:06:18 +0000 Subject: [PATCH 07/32] Use logging library to log messages With the time display (just millis for now) this makes it easy to see which messages are relevant for a particular request/response. --- library.dylan | 2 ++ lsp-dylan.dylan | 21 ++++++++++----------- pkg.json | 1 + 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/library.dylan b/library.dylan index 2f8bc28..e687f4f 100644 --- a/library.dylan +++ b/library.dylan @@ -14,6 +14,7 @@ define library lsp-dylan use io; use json; use lisp-reader; + use logging; use network; use registry-projects; use release-info; @@ -42,6 +43,7 @@ define module lsp-dylan use format; use json; use locators; + use logging; use operating-system; use registry-projects; use source-records; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 23f0ac6..98bd9fc 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -8,6 +8,7 @@ define constant $message-type-error = 1; define constant $message-type-warning = 2; define constant $message-type-info = 3; define constant $message-type-log = 4; + define method show-message (session :: , msg-type :: , m :: ) @@ -17,15 +18,6 @@ define method show-message (session :: , send-notification(session, "window/showMessage", show-message-params); end method; -define method log-message (session :: , - msg-type :: , - m :: ) - => () - let show-message-params = json("type", msg-type, - "message", m); - send-notification(session, "window/logMessage", show-message-params); -end method; - define inline method show-error (session :: , m :: ) => () @@ -50,9 +42,16 @@ define inline method show-log(session :: , show-message(session, $message-type-log, m); end method; +define constant $log + = make(, + name: "lsp-dylan", + targets: list($stderr-log-target), + // For now just displaying millis is a good way to identify all the + // messages that belong to a given call/response. + formatter: "%{millis} %{level} [%{thread}] - %{message}"); + define function local-log(m :: , #rest params) => () - apply(format, *standard-error*, concatenate("local-log: ", m, "\n"), params); - force-output(*standard-error*); + apply(log-debug, $log, m, params); end function; define function make-range(start, endp) diff --git a/pkg.json b/pkg.json index 8cdaec8..4113a08 100644 --- a/pkg.json +++ b/pkg.json @@ -2,6 +2,7 @@ "name": "lsp-dylan", "deps": [ "json head", + "logging head", "vscode-dylan head" ], "location": "git@github.com:pedro-w/lsp-dylan" From eaee886e18f53a626692128a90d0ea63e999dd8f Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 28 Feb 2021 05:07:04 +0000 Subject: [PATCH 08/32] Comments, logging, nothing major --- compiler.dylan | 2 +- lsp-dylan.dylan | 64 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/compiler.dylan b/compiler.dylan index 5bfee7f..81ce235 100644 --- a/compiler.dylan +++ b/compiler.dylan @@ -26,7 +26,7 @@ end function run-compiler; /* Ask the command line compiler to open a project. * Param: server - the command line server - * Param: name - the project name (either a registry name or a lid file) + * Param: name - the project name (either a library name or a lid file) * Returns: the project object. (instance of ) */ define function open-project(server, name :: ) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 98bd9fc..950fa8c 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -170,22 +170,34 @@ define function handle-textDocument/didOpen(session :: , end if; end function; -// Go to definition: +// Go to definition. +// Sent by M-. (emacs), ??? (VSCode). // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_definition - +// Example JSON: +// { "jsonrpc": "2.0", +// "method": "textDocument/definition", +// "params": { +// "textDocument": { +// "uri": "file:///home/cgay/dylan/workspaces/lsp/lsp-dylan/testproject/testproject.dylan" +// }, +// "position": { "line": 9, "character": 16} +// }, +// "id": 2 +// } define function handle-textDocument/definition(session :: , id :: , params :: ) => () let text-document = params["textDocument"]; let uri = text-document["uri"]; let position = params["position"]; - let (l, c) = decode-position(position); + let (line, character) = decode-position(position); let doc = element($documents, uri, default: #f); let location = $null; if (doc) unless (doc.document-module) let local-dir = make(, path: locator-path(doc.document-uri)); - let local-file = make(, directory: local-dir, + let local-file = make(, + directory: local-dir, name: locator-name(doc.document-uri)); local-log("local-dir=%s", as(, local-dir)); local-log("local-file=%s", as(, local-file)); @@ -193,10 +205,12 @@ define function handle-textDocument/definition(session :: , local-log("module=%s, library=%s", mod, lib); doc.document-module := mod; end; - let symbol = symbol-at-position(doc, l, c); - let (target, line, char) = lookup-symbol(session, symbol, module: doc.document-module); + let symbol = symbol-at-position(doc, line, character); + let (target, line, char) = lookup-symbol(session, symbol, + module: doc.document-module); if (target) - local-log("Lookup %s and got target=%s, line=%d, char=%d", symbol, target, line, char); + local-log("Lookup %s and got target=%s, line=%d, char=%d", + symbol, target, line, char); let uri = make-file-uri(target); // TODO location := make-location(as(, uri), line, char); else @@ -255,7 +269,6 @@ define function handle-initialized end); */ show-info(session, "Dylan LSP server started."); - local-log("debug: %s, messages: %s, verbose: %s", *debug-mode*, *trace-messages*, *trace-verbose*); let in-stream = make(); let out-stream = make(, direction: #"output"); @@ -263,7 +276,8 @@ define function handle-initialized local-log("Env O-D-R=%s, PATH=%s", environment-variable("OPEN_DYLAN_RELEASE"), environment-variable("PATH")); - send-request(session, "workspace/workspaceFolders", #f, callback: handle-workspace/workspaceFolders); + send-request(session, "workspace/workspaceFolders", #f, + callback: handle-workspace/workspaceFolders); *server* := start-compiler(in-stream, out-stream); test-open-project(session); end function handle-initialized; @@ -308,8 +322,8 @@ define function test-open-project(session) => () local-log("Database: %=", project-compiler-database(*project*)); end function; -define function add-trailing-slash(s :: ) => (s-slash :: ) - if (last(s) = '/') +define function ensure-trailing-slash(s :: ) => (s-slash :: ) + if (ends-with?(s, "/")) s else concatenate(s, "/") @@ -321,10 +335,19 @@ end function; * Here we return the 'static capabilities' of this server. * In the future we can register capabilities dynamically by sending messages * back to the client; this seems to be the preferred 'new' way to do things. + * https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#initialize */ define function handle-initialize (session :: , id :: , params :: ) => () + // The very first received message is "initialize" (I think), and it seems + // that for some reason it doesn't get logged, so log params here. The params + // for this method are copious, so we log them with pretty printing. + local-log("handle-initialize(%=, %=, %s)", + session, id, + with-output-to-string (s) + print-json(params, s, indent: 2) + end); let trace = element(params, "trace", default: "off"); select (trace by \=) "off" => begin @@ -339,13 +362,18 @@ define function handle-initialize (session :: , *trace-messages* := #t; *trace-verbose* := #t; end; - otherwise => local-log("trace must be \"off\", \"messages\" or \"verbose\", not %s", trace); + otherwise => + local-log("trace must be \"off\", \"messages\" or \"verbose\", not %s", trace); end select; + local-log("debug: %s, messages: %s, verbose: %s", + *debug-mode*, *trace-messages*, *trace-verbose*); + // Save the workspace root (if provided) for later. + // rootUri takes precedence over rootPath if both are provided. // TODO: can root-uri be something that's not a file:// URL? let root-uri = element(params, "rootUri", default: #f); if (root-uri) - let url = as(, trailing-slash(root-uri)); + let url = as(, ensure-trailing-slash(root-uri)); let dir = make(, path: locator-path(url)); session.root := dir; else @@ -354,6 +382,7 @@ define function handle-initialize (session :: , session.root := as(, root-path); end; end; + // Set CWD if (session.root) working-directory() := session.root; @@ -477,6 +506,8 @@ end; * If there is more than one lid file, that's an error, don't return * any project. * Returns: the name of a project + * TODO(cgay): This is a question the "workspaces" library should be able to answer + * but currently can't. It just needs a concept of "primary project". */ define function find-project-name() => (name :: false-or()) @@ -517,7 +548,7 @@ define function main let session = make(); // Pre-init state while (session.state == $session-preinit) - local-log("entered pre-init state"); + local-log("state = pre-init"); let (meth, id, params) = receive-message(session); select (meth by =) "initialize" => handle-initialize(session, id, params); @@ -532,7 +563,7 @@ define function main end while; // Active state while (session.state == $session-active) - local-log("entered active state"); + local-log("state = active"); let (meth, id, params) = receive-message(session); select (meth by =) "initialize" => @@ -571,7 +602,7 @@ define function main end while; // Shutdown state while (session.state == $session-shutdown) - local-log("entered shutdown state"); + local-log("state = shutdown"); let (meth, id, params) = receive-message(session); select (meth by =) "exit" => @@ -600,5 +631,4 @@ main(application-name(), application-arguments()); // Local Variables: // indent-tabs-mode: nil -// compile-command: "dylan-compiler -build lsp-dylan" // End: From 172b35a2ca8e674a9113f35bc357feb00f5c0577 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 6 Mar 2021 15:29:50 +0000 Subject: [PATCH 09/32] Add basic support for dylan-tool workspaces --- README.md | 27 ++++++++++++++++++++----- compiler.dylan | 6 +++--- library.dylan | 3 ++- lsp-dylan.dylan | 52 ++++++++++++++++++++++++++++++++++++------------- setup.el | 24 +++++++++++++++++------ 5 files changed, 84 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 06792c1..f4ca4af 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,21 @@ We are currently using version [3.15 of the LSP protocol](https://microsoft.gith Testing with Emacs [lsp-mode](https://github.com/emacs-lsp/lsp-mode). 1. Install lsp-mode (see github project page for details) -2. Start emacs with `emacs --load=setup.el` in this directory -3. Open a Dylan file -4. Type `M-x lsp` to start the client, which will connect to the server +2. Start emacs with `emacs --load=setup.el testproject/testproject.dylan` in + this directory. (For now "testproject" is the single, hard-coded project + name, soon to be fixed.) +3. Type `M-x lsp` to start the client, which will connect to the server The file `setup.el` is used just to avoid making any changes to the user's `.emacs`. -You must set `OPEN_DYLAN_RELEASE` and `OPEN_DYLAN_USER_REGISTRIES` appropriately. -Currently the only function is `lsp-find-definition` which will jump to the definition of the symbol under the cursor. Unfortunately it is still not reliable and depends on some hard-coded defaults. +If you are **not** using [dylan-tool](https://github.com/cgay/dylan-tool) then +you must set `OPEN_DYLAN_RELEASE` to wherever your "opendylan" directory is and +`OPEN_DYLAN_USER_REGISTRIES` to the appropriate "registry" directory. + +Currently the only function is `lsp-find-definition` which will jump to the +definition of the symbol under the cursor. Unfortunately it is still not +reliable and depends on some hard-coded defaults. Testing with VS Code (1.45.0 on macos) @@ -34,5 +40,16 @@ Testing with VS Code (1.45.0 on macos) 4. A new VS Code window will open with the extension running. +## References + +* [Intro to LSP from + Microsoft](https://docs.microsoft.com/en-us/visualstudio/extensibility/language-server-protocol) + Besides being a quick introduction, this has links to some other tools that + would help in developing VS Code integration for Dylan. +* [LSP v3.15 + Specification](https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/) + This is the version we are currently coding to. +* [langserver.org](https://langserver.org/) lists LSP implementations that + support at least one of the six major LSP features. diff --git a/compiler.dylan b/compiler.dylan index 81ce235..b48ba52 100644 --- a/compiler.dylan +++ b/compiler.dylan @@ -25,9 +25,9 @@ define function run-compiler(server, string :: ) => () end function run-compiler; /* Ask the command line compiler to open a project. - * Param: server - the command line server - * Param: name - the project name (either a library name or a lid file) - * Returns: the project object. (instance of ) + * Param: server - the command line server. + * Param: name - either a library name or a lid file. + * Returns: an instance of */ define function open-project(server, name :: ) => (project :: ) diff --git a/library.dylan b/library.dylan index e687f4f..fa3eba3 100644 --- a/library.dylan +++ b/library.dylan @@ -22,7 +22,7 @@ define library lsp-dylan //use stack-walker; use strings; use system; - use system; + use workspaces; end library; define module lsp-dylan @@ -51,4 +51,5 @@ define module lsp-dylan use streams; use strings; use threads; + use workspaces; end module lsp-dylan; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 950fa8c..376b189 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -47,7 +47,7 @@ define constant $log name: "lsp-dylan", targets: list($stderr-log-target), // For now just displaying millis is a good way to identify all the - // messages that belong to a given call/response. + // messages that belong to a given call/response, and it's terse. formatter: "%{millis} %{level} [%{thread}] - %{message}"); define function local-log(m :: , #rest params) => () @@ -372,22 +372,13 @@ define function handle-initialize (session :: , // rootUri takes precedence over rootPath if both are provided. // TODO: can root-uri be something that's not a file:// URL? let root-uri = element(params, "rootUri", default: #f); - if (root-uri) - let url = as(, ensure-trailing-slash(root-uri)); - let dir = make(, path: locator-path(url)); - session.root := dir; - else - let root-path = element(params, "rootPath", default: #f); - if (root-path) - session.root := as(, root-path); - end; - end; - - // Set CWD + let root-path = element(params, "rootPath", default: #f); + session.root := find-workspace-root(root-uri, root-path); if (session.root) working-directory() := session.root; end; local-log("Working directory is now:%s", as(, working-directory())); + // Return the capabilities of this server let capabilities = json("hoverProvider", #f, "textDocumentSync", 1, @@ -399,6 +390,39 @@ define function handle-initialize (session :: , session.state := $session-active; end function; +// Find the workspace root. The "rootUri" LSP parameter takes precedence over +// the deprecated "rootPath" LSP parameter. We first look for a `dylan-tool` +// workspace root containing the file and then fall back to the nearest +// directory containing a `registry` directory. This should work for +// `dylan-tool` users and others equally well. +define function find-workspace-root + (root-uri, root-path) => (root :: false-or()) + let directory + = if (root-uri) + let url = as(, ensure-trailing-slash(root-uri)); + make(, path: locator-path(url)) + elseif (root-path) + as(, root-path) + end; + let file = workspace-file(directory: directory); + if (file) + file.locator-directory + else + // Search up from `directory` to find the directory containing the + // "registry" directory. + iterate loop (dir = directory) + if (dir) + let registry-dir = subdirectory-locator(dir, "registry"); + if (file-exists?(registry-dir)) + dir + else + loop(dir.locator-directory) + end + end + end + end +end function; + define function handle-workspace/workspaceFolders (session :: , params :: ) => () @@ -508,6 +532,8 @@ end; * Returns: the name of a project * TODO(cgay): This is a question the "workspaces" library should be able to answer * but currently can't. It just needs a concept of "primary project". + * But really, we need to search the LID files to find the file in the original + * client request with rootUri, maybe?? */ define function find-project-name() => (name :: false-or()) diff --git a/setup.el b/setup.el index dffb5f3..1deec0d 100644 --- a/setup.el +++ b/setup.el @@ -5,9 +5,21 @@ (setq lsp-server-trace "verbose") (add-to-list 'lsp-language-id-configuration '(dylan-mode . "dylan")) -(let ((server (list (expand-file-name "_build/bin/lsp-dylan" - (file-name-directory load-file-name)) "--debug"))) - (lsp-register-client - (make-lsp-client :new-connection (lsp-stdio-connection server) - :major-modes '(dylan-mode) - :server-id 'dylan-lsp))) + +(defun lsp-dylan-start () + (let* ((dylan-root (getenv "DYLAN")) + (lsp-dylan-relative-path "_build/bin/lsp-dylan") + (lsp-dylan-full-path + (if dylan-root + ;; Assume using dylan-tool if $DYLAN is set. + ;; Also assume $DYLAN/workspaces/lsp as the workspace directory. + (format "%s/workspaces/lsp/_build/bin/lsp-dylan" dylan-root) + ;; Otherwise assume using git submodules in the lsp-dylan directory. + (expand-file-name lsp-dylan-relative-path load-file-name))) + (server (list lsp-dylan-full-path "--debug"))) + (lsp-register-client + (make-lsp-client :new-connection (lsp-stdio-connection server) + :major-modes '(dylan-mode) + :server-id 'dylan-lsp)))) + +(lsp-dylan-start) From d6238c6c400ab7e11582931a49e81b1ec04510a9 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 6 Mar 2021 16:12:23 +0000 Subject: [PATCH 10/32] ignore() some unused definitions --- lsp-dylan.dylan | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 376b189..abe6032 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -651,6 +651,10 @@ define function main exit-application(retcode); end function main; +ignore(*library*, run-compiler, describe-symbol, list-all-package-names, + document-lines-setter, trailing-slash, unregister-file, + one-off-debug, dump, show-warning, show-log, show-error); + main(application-name(), application-arguments()); From f60244864106f31eecd436418256f58f1dcf7f1b Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 6 Mar 2021 23:26:26 +0000 Subject: [PATCH 11/32] Add support for default-project-name If the containing workspace has the "default-project-name" setting, use it. Otherwise fall back to looking for a LID file. Depends on https://github.com/cgay/workspaces/commit/86cfc7ee6f81d28511bdf82bca291d7a616dde4f --- library.dylan | 3 ++- lsp-dylan.dylan | 44 +++++++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/library.dylan b/library.dylan index fa3eba3..b12e6fe 100644 --- a/library.dylan +++ b/library.dylan @@ -51,5 +51,6 @@ define module lsp-dylan use streams; use strings; use threads; - use workspaces; + use workspaces, + prefix: "ws/"; end module lsp-dylan; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index abe6032..78b7fc4 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -377,7 +377,7 @@ define function handle-initialize (session :: , if (session.root) working-directory() := session.root; end; - local-log("Working directory is now:%s", as(, working-directory())); + local-log("Working directory is now %s", working-directory()); // Return the capabilities of this server let capabilities = json("hoverProvider", #f, @@ -404,9 +404,9 @@ define function find-workspace-root elseif (root-path) as(, root-path) end; - let file = workspace-file(directory: directory); - if (file) - file.locator-directory + let workspace = ws/find-workspace(directory: directory); + if (workspace) + ws/workspace-directory(workspace) else // Search up from `directory` to find the directory containing the // "registry" directory. @@ -542,22 +542,28 @@ define function find-project-name() local-log("Project name explicitly:%s", *project-name*); *project-name*; else - // Guess based on there being one .lid file in the workspace root - block(return) - local method return-lid(dir, name, type) - local-log("Project scan %s", name); - if (type = #"file") - let file = as(, name); - if (locator-extension(file) = "lid") - return (name); + let workspace = ws/find-workspace(); + let project-name = workspace & ws/workspace-default-project-name(workspace); + if (project-name) + project-name + else + // Guess based on there being one .lid file in the workspace root + block(return) + local method return-lid(dir, name, type) + local-log("Project scan %s", name); + if (type = #"file") + let file = as(, name); + if (locator-extension(file) = "lid") + return(name); + end if; end if; - end if; - end method; - do-directory(return-lid, working-directory()); - local-log("Project name, got nothing"); - #f - end block; - end if; + end method; + do-directory(return-lid, working-directory()); + local-log("find-project-name found nothing"); + #f + end block + end if + end if end function; define function main From f7e151f3b5d7812eec914b757cad8db9e2d51897 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 7 Mar 2021 17:56:21 +0000 Subject: [PATCH 12/32] Make symbol-at-position when pos follows symbol If the cursor is on, for example, the open paren in foo() we still want symbol-at-position to work. --- lsp-dylan.dylan | 87 +++++++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 78b7fc4..172aec3 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -184,9 +184,8 @@ end function; // }, // "id": 2 // } -define function handle-textDocument/definition(session :: , - id :: , - params :: ) => () +define function handle-textDocument/definition + (session :: , id :: , params :: ) => () let text-document = params["textDocument"]; let uri = text-document["uri"]; let position = params["position"]; @@ -206,15 +205,19 @@ define function handle-textDocument/definition(session :: , doc.document-module := mod; end; let symbol = symbol-at-position(doc, line, character); - let (target, line, char) = lookup-symbol(session, symbol, - module: doc.document-module); - if (target) - local-log("Lookup %s and got target=%s, line=%d, char=%d", - symbol, target, line, char); - let uri = make-file-uri(target); // TODO - location := make-location(as(, uri), line, char); + if (symbol) + let (target, line, char) + = lookup-symbol(session, symbol, module: doc.document-module); + if (target) + local-log("Lookup %s and got target=%s, line=%d, char=%d", + symbol, target, line, char); + let uri = make-file-uri(target); // TODO + location := make-location(as(, uri), line, char); + else + local-log("Symbol %=, not found", symbol); + end; else - local-log("Lookup %s, not found", symbol); + show-message(session, $message-type-info, "No symbol found at current position."); end; end; send-response(session, id, location); @@ -430,14 +433,17 @@ define function handle-workspace/workspaceFolders (session :: , local-log("Workspace folders were received"); end; -/* Document Management */ +// Maps URI strings to objects. define constant $documents = make(); // Represents one open file (given to us by textDocument/didOpen) define class () - constant slot document-uri, required-init-keyword: uri:; - slot document-module, init-value: #f; // Module if we know it. - slot document-lines, required-init-keyword: lines:; + constant slot document-uri :: , + required-init-keyword: uri:; + slot document-module :: false-or() = #f, + init-keyword: module:; + slot document-lines :: , + required-init-keyword: lines:; end class; define function register-file (uri, contents) @@ -447,31 +453,37 @@ define function register-file (uri, contents) $documents[uri] := doc; end function; -/* Given a document and a position, find the symbol that this position is within -If the position not on a symbol, return #f -*/ -define function symbol-at-position (doc :: , line, column) => (symbol :: false-or()) - if (line >=0 & line < size(doc.document-lines) & column >=0 & column < size(doc.document-lines[line])) +// Characters that are part of the Dylan "name" BNF. +define constant $dylan-name-characters + = "abcdefghijklmnopqrstuvwxyzABCDEFGHIHJLKMNOPQRSTUVWXYZ0123456789!&*<>|^$%@_-+~?/="; + +// Given a document and a position, find the Dylan name (identifier) that is at +// (or immediately precedes) this position. If the position is, for example, +// the open paren following a function name, we should still find the name. If +// there is no name at position, return #f. +define function symbol-at-position + (doc :: , line, column) => (symbol :: false-or()) + if (line >= 0 + & line < size(doc.document-lines) + & column >= 0 + & column < size(doc.document-lines[line])) let line = doc.document-lines[line]; - local method any-character?(c) => (well? :: ) - member?(c, "abcdefghijklmnopqrstuvwxyzABCDEFGHIHJLKMNOPQRSTUVWXYZ0123456789!&*<>|^$%@_-+~?/=") + local method name-character?(c) => (well? :: ) + member?(c, $dylan-name-characters) end; - if (any-character?(line[column])) - let symbol-start = column; - let symbol-end = column; - while (symbol-start >= 0 & any-character?(line[symbol-start])) + let symbol-start = column; + let symbol-end = column; + while (symbol-start > 0 & name-character?(line[symbol-start - 1])) symbol-start := symbol-start - 1; - end while; - while (symbol-end < size(line) & any-character?(line[symbol-end])) - symbol-end := symbol-end + 1; - end while; - copy-sequence(line, start: symbol-start + 1, end: symbol-end) - else - // Hovered over some 'punctuation' - #f - end if + end; + while (symbol-end < size(line) & name-character?(line[symbol-end])) + symbol-end := symbol-end + 1; + end while; + let name = copy-sequence(line, start: symbol-start, end: symbol-end); + ~empty?(name) & name else - // Not in range + local-log("line %d column %d not in range for document %s", + line, column, doc.document-uri); #f end; end function; @@ -598,6 +610,9 @@ define function main local-log("state = active"); let (meth, id, params) = receive-message(session); select (meth by =) + // TODO(cgay): It would be nice to turn params into a set of keyword/value + // pairs and apply(the-method, session, id, params) so that the parameters + // to each method are clear from the #key parameters. "initialize" => send-error-response(session, id, $invalid-request); "initialized" => handle-initialized(session, id, params); From 9774b177e4fe869c0fbcba5bc5997cfe43979d99 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 7 Mar 2021 20:29:15 +0000 Subject: [PATCH 13/32] Added a few `:: ` declarations where applicable --- compiler.dylan | 14 ++++++-------- lsp-dylan.dylan | 14 ++++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/compiler.dylan b/compiler.dylan index b48ba52..e7e0998 100644 --- a/compiler.dylan +++ b/compiler.dylan @@ -47,8 +47,9 @@ define function describe-symbol (symbol-name) environment-object-description(*project*, env, *module*) end; -define function symbol-location (symbol-name, #key module = #f) - let env = symbol-name & get-environment-object(symbol-name, module: module); +define function symbol-location + (symbol-name :: , #key module) + let env = get-environment-object(symbol-name, module: module); if (env) environment-object-source-location(*project*, env) else @@ -134,14 +135,11 @@ end; define method n ( x == #f) "#f" end; -define function get-environment-object (symbol-name, #key module = #f) + +define function get-environment-object + (symbol-name :: , #key module) => (o :: false-or()) let library = project-library(*project*); - unless (module) - // TODO not hard code - module := find-module(*project*, "testproject", library: library); - end; local-log("%s -> module is %s", symbol-name, n(module)); - find-environment-object(*project*, symbol-name, library: library, module: module); diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 172aec3..a8baf5c 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -521,8 +521,9 @@ end; // Look up a symbol. Return the containing doc, // the line and column -define function lookup-symbol (session, symbol, #key module = #f) => (doc, line, column) - let loc = symbol-location (symbol, module: module); +define function lookup-symbol + (session, symbol :: , #key module) => (doc, line, column) + let loc = symbol-location(symbol, module: module); if (loc) let source-record = loc.source-location-source-record; let absolute-path = source-record.source-record-location; @@ -542,10 +543,10 @@ end; * If there is more than one lid file, that's an error, don't return * any project. * Returns: the name of a project - * TODO(cgay): This is a question the "workspaces" library should be able to answer - * but currently can't. It just needs a concept of "primary project". - * But really, we need to search the LID files to find the file in the original - * client request with rootUri, maybe?? + * + * TODO(cgay): Really we need to search the LID files to find the file in the + * textDocument/didOpen message so we can figure out which library's project + * to open. */ define function find-project-name() => (name :: false-or()) @@ -557,6 +558,7 @@ define function find-project-name() let workspace = ws/find-workspace(); let project-name = workspace & ws/workspace-default-project-name(workspace); if (project-name) + local-log("found dylan-tool workspace default project name %=", project-name); project-name else // Guess based on there being one .lid file in the workspace root From 1c5eff6b90ec779700cb6f49559a5ac20e1aaf5e Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Mon, 8 Mar 2021 03:48:40 +0000 Subject: [PATCH 14/32] Update for changes in workspaces library --- lsp-dylan.dylan | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index a8baf5c..cc71678 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -556,10 +556,10 @@ define function find-project-name() *project-name*; else let workspace = ws/find-workspace(); - let project-name = workspace & ws/workspace-default-project-name(workspace); - if (project-name) - local-log("found dylan-tool workspace default project name %=", project-name); - project-name + let library-name = workspace & ws/workspace-default-library-name(workspace); + if (library-name) + local-log("found dylan-tool workspace default library name %=", library-name); + library-name else // Guess based on there being one .lid file in the workspace root block(return) From 45dbc597b896b2f96c29bb5173fcc799f6b099b0 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Mon, 8 Mar 2021 16:06:25 +0000 Subject: [PATCH 15/32] Remove json submodule that has commits not pushed to master --- .gitmodules | 4 ---- json | 1 - 2 files changed, 5 deletions(-) delete mode 160000 json diff --git a/.gitmodules b/.gitmodules index 9019e6e..e56b159 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "json"] - path = json - url = git@github.com:pedro-w/json.git - branch = updates-for-lsp [submodule "vscode-dylan"] path = vscode-dylan url = git@github.com:pedro-w/vscode-dylan.git diff --git a/json b/json deleted file mode 160000 index 83fd4e2..0000000 --- a/json +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 83fd4e25f80f173f47722d7f887d1916ee138569 From 70fa5771910e7a6d3a13f4f0736fdbd7bf2f750f Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Mon, 8 Mar 2021 16:21:59 +0000 Subject: [PATCH 16/32] Update all submodule except vscode-dylan Added the `workspaces` repo and its dependencies. Moved them to the ext/ directory, as is the custom. vscode-dylan should be moved there at some point too, but I don't have VS Code installed to test it. --- .gitmodules | 12 ++++++++++++ ext/json | 1 + ext/pacman | 1 + ext/uncommon-dylan | 1 + ext/workspaces | 1 + registry/generic/json | 2 +- registry/generic/pacman | 1 + registry/generic/uncommon-dylan | 1 + registry/generic/workspaces | 1 + 9 files changed, 20 insertions(+), 1 deletion(-) create mode 160000 ext/json create mode 160000 ext/pacman create mode 160000 ext/uncommon-dylan create mode 160000 ext/workspaces create mode 100644 registry/generic/pacman create mode 100644 registry/generic/uncommon-dylan create mode 100644 registry/generic/workspaces diff --git a/.gitmodules b/.gitmodules index e56b159..73242d7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,15 @@ [submodule "vscode-dylan"] path = vscode-dylan url = git@github.com:pedro-w/vscode-dylan.git +[submodule "ext/json"] + path = ext/json + url = git@github.com:dylan-lang/json +[submodule "ext/workspaces"] + path = ext/workspaces + url = git@github.com:cgay/workspaces +[submodule "ext/uncommon-dylan"] + path = ext/uncommon-dylan + url = git@github.com:cgay/uncommon-dylan +[submodule "ext/pacman"] + path = ext/pacman + url = git@github.com:cgay/pacman diff --git a/ext/json b/ext/json new file mode 160000 index 0000000..91c4174 --- /dev/null +++ b/ext/json @@ -0,0 +1 @@ +Subproject commit 91c4174c9bee2c97b3909c48f1bcda9524793d1c diff --git a/ext/pacman b/ext/pacman new file mode 160000 index 0000000..bfe4990 --- /dev/null +++ b/ext/pacman @@ -0,0 +1 @@ +Subproject commit bfe49902bb1ab449d1090bb5e817b6323cc6c41f diff --git a/ext/uncommon-dylan b/ext/uncommon-dylan new file mode 160000 index 0000000..06eddc4 --- /dev/null +++ b/ext/uncommon-dylan @@ -0,0 +1 @@ +Subproject commit 06eddc482d33e2faeab88706804ebe9022c3a854 diff --git a/ext/workspaces b/ext/workspaces new file mode 160000 index 0000000..359ef9d --- /dev/null +++ b/ext/workspaces @@ -0,0 +1 @@ +Subproject commit 359ef9df58d3ade48f0c3fbfee6ee3e59a9f1f6b diff --git a/registry/generic/json b/registry/generic/json index 00873d6..0b78401 100644 --- a/registry/generic/json +++ b/registry/generic/json @@ -1 +1 @@ -abstract://dylan/json/json.lid +abstract://dylan/ext/json/json.lid diff --git a/registry/generic/pacman b/registry/generic/pacman new file mode 100644 index 0000000..e172857 --- /dev/null +++ b/registry/generic/pacman @@ -0,0 +1 @@ +abstract://dylan/ext/pacman/pacman.lid \ No newline at end of file diff --git a/registry/generic/uncommon-dylan b/registry/generic/uncommon-dylan new file mode 100644 index 0000000..9086d1d --- /dev/null +++ b/registry/generic/uncommon-dylan @@ -0,0 +1 @@ +abstract://dylan/ext/uncommon-dylan/uncommon-dylan.lid \ No newline at end of file diff --git a/registry/generic/workspaces b/registry/generic/workspaces new file mode 100644 index 0000000..bcc1125 --- /dev/null +++ b/registry/generic/workspaces @@ -0,0 +1 @@ +abstract://dylan/ext/workspaces/workspaces.lid \ No newline at end of file From e5ea33239ab3b9c988dc02d0bd0d969ead41d752 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 13 Mar 2021 15:13:39 +0000 Subject: [PATCH 17/32] find-project-name: err if workspace has no default library and some minor cleanups --- lsp-dylan.dylan | 102 +++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 54 deletions(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index cc71678..1665d33 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -9,37 +9,30 @@ define constant $message-type-warning = 2; define constant $message-type-info = 3; define constant $message-type-log = 4; -define method show-message (session :: , - msg-type :: , - m :: ) - => () - let show-message-params = json("type", msg-type, - "message", m); - send-notification(session, "window/showMessage", show-message-params); +define method window/show-message + (session :: , msg-type :: , msg :: ) => () + let params = json("type", msg-type, "message", msg); + send-notification(session, "window/showMessage", params); end method; -define inline method show-error (session :: , - m :: ) - => () - show-message(session, $message-type-error, m); +define method show-error + (session :: , msg :: ) => () + window/show-message(session, $message-type-error, msg); end method; -define inline method show-warning (session :: , - m :: ) - => () - show-message(session, $message-type-warning, m); +define inline method show-warning + (session :: , msg :: ) => () + window/show-message(session, $message-type-warning, msg); end method; -define inline method show-info (session :: , - m :: ) - => () - show-message(session, $message-type-info, m); +define inline method show-info + (session :: , msg :: ) => () + window/show-message(session, $message-type-info, msg); end method; -define inline method show-log(session :: , - m :: ) - => () - show-message(session, $message-type-log, m); +define inline method show-log + (session :: , msg :: ) => () + window/show-message(session, $message-type-log, msg); end method; define constant $log @@ -51,7 +44,7 @@ define constant $log formatter: "%{millis} %{level} [%{thread}] - %{message}"); define function local-log(m :: , #rest params) => () - apply(log-debug, $log, m, params); + apply(local-log, m, params); end function; define function make-range(start, endp) @@ -217,7 +210,7 @@ define function handle-textDocument/definition local-log("Symbol %=, not found", symbol); end; else - show-message(session, $message-type-info, "No symbol found at current position."); + show-info(session, "No symbol found at current position."); end; end; send-response(session, id, location); @@ -286,22 +279,21 @@ define function handle-initialized end function handle-initialized; define function test-open-project(session) => () - // TODO don't hard-code the project name and module name. - local-log("Select project %=", find-project-name()); + let project-name = find-project-name(); + local-log("Found project name %=", project-name); + *project* := open-project(*server*, project-name); + local-log("Project opened"); - *project* := open-project(*server*, find-project-name()); // Let's see if we can find a module let (m, l) = file-module(*project*, "library.dylan"); local-log("Try Module: %=, Library: %=", - if (m) environment-object-primitive-name(*project*, m) else "?" end, - if (l) environment-object-primitive-name(*project*, l) else "?" end); + m & environment-object-primitive-name(*project*, m), + l & environment-object-primitive-name(*project*, l)); *module* := m; if (*project*) - local method wrn(w) - local-log("Warn: %=\n", w); - end; - let db = open-project-compiler-database(*project*, warning-callback: wrn); + let warn = curry(log-warning, $log, "Warn: %="); + let db = open-project-compiler-database(*project*, warning-callback: warn); local-log("Test, Database: %=", db); local-log("Test, listing sources:"); for (s in project-sources(*project*)) @@ -407,7 +399,7 @@ define function find-workspace-root elseif (root-path) as(, root-path) end; - let workspace = ws/find-workspace(directory: directory); + let workspace = ws/workspace-file() & ws/find-workspace(directory: directory); if (workspace) ws/workspace-directory(workspace) else @@ -548,35 +540,37 @@ end; * textDocument/didOpen message so we can figure out which library's project * to open. */ -define function find-project-name() - => (name :: false-or()) +define function find-project-name () => (name :: false-or()) if (*project-name*) // We've set it explicitly local-log("Project name explicitly:%s", *project-name*); - *project-name*; - else + *project-name* + elseif (ws/workspace-file()) + // There's a dylan-tool workspace. let workspace = ws/find-workspace(); let library-name = workspace & ws/workspace-default-library-name(workspace); if (library-name) local-log("found dylan-tool workspace default library name %=", library-name); library-name else - // Guess based on there being one .lid file in the workspace root - block(return) - local method return-lid(dir, name, type) - local-log("Project scan %s", name); - if (type = #"file") - let file = as(, name); - if (locator-extension(file) = "lid") - return(name); - end if; + local-log("dylan-tool workspace has no default library configured."); + #f + end; + else + // Guess based on there being one .lid file in the workspace root + block(return) + local method return-lid(dir, name, type) + if (type = #"file") + let file = as(, name); + if (locator-extension(file) = "lid") + return(name); end if; - end method; - do-directory(return-lid, working-directory()); - local-log("find-project-name found nothing"); - #f - end block - end if + end if; + end method; + do-directory(return-lid, working-directory()); + local-log("find-project-name found no LID files"); + #f + end block end if end function; From 2239162926339245d4782d3da4a0d4ee7901b961 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 13 Mar 2021 17:32:50 +0000 Subject: [PATCH 18/32] Improve logging a bit --- jsonrpc.dylan | 18 ++++---- lsp-dylan.dylan | 113 ++++++++++++++++++++++++++++-------------------- 2 files changed, 75 insertions(+), 56 deletions(-) diff --git a/jsonrpc.dylan b/jsonrpc.dylan index 112b28e..ada52d9 100644 --- a/jsonrpc.dylan +++ b/jsonrpc.dylan @@ -69,9 +69,9 @@ define function json (#rest kvs) => (table :: ) end function; define function dump(t ::
) => () - format(*standard-error*, "Table Dump\n==========\n"); + local-log("========== Table Dump =========="); for (v keyed-by k in t) - format(*standard-error*, "%s-->%s(%s)\n", k, v, object-class(v)); + local-log("dump: %s-->%s (%s)", k, v, object-class(v)); end for; end; @@ -82,7 +82,7 @@ define method read-json-message(stream :: ) => (json :: ) let content-length = string-to-integer(content-length); let data = read(stream, content-length); if (*trace-messages*) - local-log("received message: %s", data); + local-log("read-json-message: received %=", data); end; parse-json(data); else @@ -223,7 +223,7 @@ define method send-notification(session :: , end; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send notification '%s'", method-name); + local-log("send-notification: %=", method-name); end; end method; @@ -246,7 +246,7 @@ define method receive-message (session :: ) else // Received a response if (*trace-messages*) - local-log("Server: receive response (%s)", id); + local-log("receive-message: got id %=", id); end; let func = element(session.callbacks, id, default: #f); if (func) @@ -274,7 +274,7 @@ define method send-request (session :: , end if; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send request: %s", print-json-to-string(message)); + local-log("send-request: %s", print-json-to-string(message)); end if; end method; @@ -286,7 +286,7 @@ define method send-response(session :: , message["result"] := result; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send response %s", print-json-to-string(message)); + local-log("send-response: %s", print-json-to-string(message)); end if; end method; @@ -305,7 +305,7 @@ define method send-error-response(session :: , message["error"] := params; send-raw-message(session, message); if (*trace-messages*) - local-log("Server: send error response: %s", print-json-to-string(message)); + local-log("send-error-response: %s", print-json-to-string(message)); end; end method; @@ -321,7 +321,7 @@ define method send-raw-message(session :: , => () let str :: = print-json-to-string(message); if (*trace-messages*) - local-log("sending message: %s", str); + local-log("send-raw-message: %s", str); end; write-json-message(*standard-output*, str); end method; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 1665d33..199123f 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -4,6 +4,19 @@ Author: Peter Copyright: 2019 +define constant $log + = make(, + name: "lsp-dylan", + targets: list($stderr-log-target), + // For now just displaying millis is a good way to identify all the + // messages that belong to a given call/response, and it's terse. + formatter: "%{millis} %{level} [%{thread}] - %{message}"); + +define function local-log(m :: , #rest params) => () + apply(log-debug, $log, m, params); +end function; + + define constant $message-type-error = 1; define constant $message-type-warning = 2; define constant $message-type-info = 3; @@ -35,18 +48,6 @@ define inline method show-log window/show-message(session, $message-type-log, msg); end method; -define constant $log - = make(, - name: "lsp-dylan", - targets: list($stderr-log-target), - // For now just displaying millis is a good way to identify all the - // messages that belong to a given call/response, and it's terse. - formatter: "%{millis} %{level} [%{thread}] - %{message}"); - -define function local-log(m :: , #rest params) => () - apply(local-log, m, params); -end function; - define function make-range(start, endp) json("start", start, "end", endp); end function; @@ -149,17 +150,18 @@ define function handle-textDocument/didOpen(session :: , if (languageId = "dylan") register-file(uri, text); end if; - show-info(session, "handle-textDocument/didOpen"); if (*project*) // This is just test code. // Let's see if we can find a module let u = as(, uri); let f = make-file-locator(u); let (m, l) = file-module(*project*, f); - local-log("File: %= Module: %=, Library: %=", + local-log("textDocument/didOpen: File: %= Module: %=, Library: %=", as(, f), - if (m) environment-object-primitive-name(*project*, m) else "?" end, - if (l) environment-object-primitive-name(*project*, l) else "?" end); + if (m) environment-object-primitive-name(*project*, m) end, + if (l) environment-object-primitive-name(*project*, l) end); + else + local-log("textDocument/didOpen: no project found"); end if; end function; @@ -185,16 +187,16 @@ define function handle-textDocument/definition let (line, character) = decode-position(position); let doc = element($documents, uri, default: #f); let location = $null; - if (doc) + if (~doc) + local-log("textDocument/definition: document not found: %=", uri); + else unless (doc.document-module) let local-dir = make(, path: locator-path(doc.document-uri)); let local-file = make(, directory: local-dir, name: locator-name(doc.document-uri)); - local-log("local-dir=%s", as(, local-dir)); - local-log("local-file=%s", as(, local-file)); let (mod, lib) = file-module(*project*, local-file); - local-log("module=%s, library=%s", mod, lib); + local-log("textDocument/definition: module=%s, library=%s", mod, lib); doc.document-module := mod; end; let symbol = symbol-at-position(doc, line, character); @@ -202,14 +204,15 @@ define function handle-textDocument/definition let (target, line, char) = lookup-symbol(session, symbol, module: doc.document-module); if (target) - local-log("Lookup %s and got target=%s, line=%d, char=%d", + local-log("textDocument/definition: Lookup %s and got target=%s, line=%d, char=%d", symbol, target, line, char); let uri = make-file-uri(target); // TODO location := make-location(as(, uri), line, char); else - local-log("Symbol %=, not found", symbol); + local-log("textDocument/definition: symbol %=, not found", symbol); end; else + local-log("textDocument/definition: symbol is #f, nothing to lookup", symbol); show-info(session, "No symbol found at current position."); end; end; @@ -269,9 +272,18 @@ define function handle-initialized let out-stream = make(, direction: #"output"); // Test code - local-log("Env O-D-R=%s, PATH=%s", - environment-variable("OPEN_DYLAN_RELEASE"), - environment-variable("PATH")); + for (var in list("OPEN_DYLAN_RELEASE", + "OPEN_DYLAN_RELEASE_BUILD", + "OPEN_DYLAN_RELEASE_INSTALL", + "OPEN_DYLAN_RELEASE_REGISTRIES", + "OPEN_DYLAN_USER_BUILD", + "OPEN_DYLAN_USER_INSTALL", + "OPEN_DYLAN_USER_PROJECTS", + "OPEN_DYLAN_USER_REGISTRIES", + "OPEN_DYLAN_USER_ROOT", + "PATH")) + local-log("handle-initialized: %s=%s", var, environment-variable(var)); + end; send-request(session, "workspace/workspaceFolders", #f, callback: handle-workspace/workspaceFolders); *server* := start-compiler(in-stream, out-stream); @@ -280,41 +292,48 @@ end function handle-initialized; define function test-open-project(session) => () let project-name = find-project-name(); - local-log("Found project name %=", project-name); + local-log("test-open-project: Found project name %=", project-name); *project* := open-project(*server*, project-name); - local-log("Project opened"); + local-log("test-open-project: Project opened"); + + // Let's see if we can find a module. - // Let's see if we can find a module + // TODO(cgay): file-module is returning #f because (I believe) + // project-compiler-database(*project*) returns #f and hence file-module + // punts. Not sure who's responsible for opening the db and setting that slot + // or why it has worked at all in the past. let (m, l) = file-module(*project*, "library.dylan"); - local-log("Try Module: %=, Library: %=", + local-log("test-open-project: m = %=, l = %=", m, l); + local-log("test-open-project: Try Module: %=, Library: %=", m & environment-object-primitive-name(*project*, m), l & environment-object-primitive-name(*project*, l)); + local-log("test-open-project: project-library = %=", project-library(*project*)); + local-log("test-open-project: project db = %=", project-compiler-database(*project*)); + *module* := m; if (*project*) - let warn = curry(log-warning, $log, "Warn: %="); + let warn = curry(log-warning, $log, "open-project-compiler-database: %="); let db = open-project-compiler-database(*project*, warning-callback: warn); - local-log("Test, Database: %=", db); - local-log("Test, listing sources:"); + local-log("test-open-project: db = %=", db); for (s in project-sources(*project*)) let rl = source-record-location(s); - local-log("Source: %=, a %= in %=", + local-log("test-open-project: Source: %=, a %= in %=", s, object-class(s), as(, rl)); end; - local-log("Test, listing project file libraries"); + local-log("test-open-project: listing project file libraries:"); do-project-file-libraries(method (l, r) - local-log("Lib:%= Rec:%=", l, r); + local-log("test-open-project: Lib: %= Rec: %=", l, r); end, *project*, as(, "library.dylan")); else - local-log("Test, Project did't open\n"); + local-log("test-open-project: project did't open"); end if; -// local-log("dylan-sources:%=\n", project-dylan-sources(*project*)); - local-log("Compiler started:%=, Project %=", *server*, *project*); - local-log("Database: %=", project-compiler-database(*project*)); + local-log("test-open-project: Compiler started: %=, Project %=", *server*, *project*); + local-log("test-open-project: Database: %=", project-compiler-database(*project*)); end function; define function ensure-trailing-slash(s :: ) => (s-slash :: ) @@ -358,9 +377,10 @@ define function handle-initialize (session :: , *trace-verbose* := #t; end; otherwise => - local-log("trace must be \"off\", \"messages\" or \"verbose\", not %s", trace); + log-error($log, "handle-initialize: trace must be" + " \"off\", \"messages\" or \"verbose\", not %=", trace); end select; - local-log("debug: %s, messages: %s, verbose: %s", + local-log("handle-initialize: debug: %s, messages: %s, verbose: %s", *debug-mode*, *trace-messages*, *trace-verbose*); // Save the workspace root (if provided) for later. @@ -372,7 +392,7 @@ define function handle-initialize (session :: , if (session.root) working-directory() := session.root; end; - local-log("Working directory is now %s", working-directory()); + local-log("handle-initialize: Working directory is now %s", working-directory()); // Return the capabilities of this server let capabilities = json("hoverProvider", #f, @@ -440,7 +460,6 @@ end class; define function register-file (uri, contents) let lines = split-lines(contents); - local-log("register-file: %s(%s), lines: %d", uri, object-class(uri), size(lines)); let doc = make(, uri: as(, uri), lines: lines); $documents[uri] := doc; end function; @@ -588,7 +607,7 @@ define function main let session = make(); // Pre-init state while (session.state == $session-preinit) - local-log("state = pre-init"); + local-log("main: state = pre-init"); let (meth, id, params) = receive-message(session); select (meth by =) "initialize" => handle-initialize(session, id, params); @@ -603,7 +622,7 @@ define function main end while; // Active state while (session.state == $session-active) - local-log("state = active"); + local-log("main: state = active"); let (meth, id, params) = receive-message(session); select (meth by =) // TODO(cgay): It would be nice to turn params into a set of keyword/value @@ -629,7 +648,7 @@ define function main // Respond to any other request with an not-implemented error. // Drop any other notifications begin - local-log("%s '%s' is not implemented", + local-log("main: %s '%s' is not implemented", if (id) "Request" else @@ -645,7 +664,7 @@ define function main end while; // Shutdown state while (session.state == $session-shutdown) - local-log("state = shutdown"); + local-log("main: state = shutdown"); let (meth, id, params) = receive-message(session); select (meth by =) "exit" => From 00e4636fac942dab876a721fbd3860e70d5c1b9a Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 13 Mar 2021 19:59:38 +0000 Subject: [PATCH 19/32] Minor refactor of lsp-dylan-start --- setup.el | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/setup.el b/setup.el index 1deec0d..12c2d92 100644 --- a/setup.el +++ b/setup.el @@ -7,16 +7,20 @@ (add-to-list 'lsp-language-id-configuration '(dylan-mode . "dylan")) (defun lsp-dylan-start () - (let* ((dylan-root (getenv "DYLAN")) - (lsp-dylan-relative-path "_build/bin/lsp-dylan") - (lsp-dylan-full-path - (if dylan-root - ;; Assume using dylan-tool if $DYLAN is set. - ;; Also assume $DYLAN/workspaces/lsp as the workspace directory. - (format "%s/workspaces/lsp/_build/bin/lsp-dylan" dylan-root) - ;; Otherwise assume using git submodules in the lsp-dylan directory. - (expand-file-name lsp-dylan-relative-path load-file-name))) - (server (list lsp-dylan-full-path "--debug"))) + (let* ((relative-path "_build/bin/lsp-dylan") + (chosen-path + (cond ((file-exists-p relative-path) + ;; If current directory has a _build directory, prefer that. + relative-path) + ((getenv "DYLAN") + ;; Assume using dylan-tool and $DYLAN/workspaces/lsp as the + ;; workspace directory. + ;; TODO(cgay): needs better solution. this works for me. + (concat (getenv "DYLAN") "/workspaces/lsp/" relative-path)) + (t + (error "Couldn't find the lsp-dylan executable")))) + (full-path (expand-file-name chosen-path (file-name-directory load-file-name))) + (server (list full-path "--debug"))) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection server) :major-modes '(dylan-mode) From 840f6d649830a1f83db79ea246754a66e3f37801 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 13 Mar 2021 20:03:31 +0000 Subject: [PATCH 20/32] Use OPEN_DYLAN_RELEASE_INSTALL OPEN_DYLAN_RELEASE seems to be something of a hack for an ancient beta release. --- README.md | 3 ++- env.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f4ca4af..c664c1f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ The file `setup.el` is used just to avoid making any changes to the user's `.emacs`. If you are **not** using [dylan-tool](https://github.com/cgay/dylan-tool) then -you must set `OPEN_DYLAN_RELEASE` to wherever your "opendylan" directory is and +you must set `OPEN_DYLAN_RELEASE_INSTALL` to wherever your "opendylan" +directory is (so that it can find the Jam build scripts) and `OPEN_DYLAN_USER_REGISTRIES` to the appropriate "registry" directory. Currently the only function is `lsp-find-definition` which will jump to the diff --git a/env.sh b/env.sh index 1f411f9..6e134df 100644 --- a/env.sh +++ b/env.sh @@ -1,5 +1,5 @@ #!/bin/sh export OPEN_DYLAN_USER_REGISTRIES=/Users/peterhull/registry/ -export OPEN_DYLAN_RELEASE=/opt/local/2020.1pre/ +export OPEN_DYLAN_RELEASE_INSTALL=/opt/local/2020.1pre/ From de2616879fd27d6711e448af0536b1264d734a08 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 14 Mar 2021 04:28:21 +0000 Subject: [PATCH 21/32] Enable OD's debug-out logging in our logs --- compiler.dylan | 3 ++- library.dylan | 1 + lsp-dylan.dylan | 24 +++++++++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/compiler.dylan b/compiler.dylan index e7e0998..48700da 100644 --- a/compiler.dylan +++ b/compiler.dylan @@ -14,7 +14,8 @@ define variable *module* = #f; define variable *library* = #f; define variable *project-name* = #f; -define function start-compiler(input-stream, output-stream) +define function start-compiler + (input-stream, output-stream) => (server :: ) make-environment-command-line-server(input-stream: input-stream, output-stream: output-stream) end function; diff --git a/library.dylan b/library.dylan index b12e6fe..2ae727f 100644 --- a/library.dylan +++ b/library.dylan @@ -46,6 +46,7 @@ define module lsp-dylan use logging; use operating-system; use registry-projects; + use simple-debugging; use source-records; use standard-io; use streams; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 199123f..bcd42fc 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -16,7 +16,6 @@ define function local-log(m :: , #rest params) => () apply(log-debug, $log, m, params); end function; - define constant $message-type-error = 1; define constant $message-type-warning = 2; define constant $message-type-info = 3; @@ -593,6 +592,28 @@ define function find-project-name () => (name :: false-or()) end if end function; +define function enable-od-environment-debug-logging () + // For simple-debugging's debug-out. This makes it possible to modify the OD + // environment sources with debug-out messages and see them in our local logs. + debugging?() := #t; + // Added most of the sources/environment/ debug-out categories here. --cgay + debug-parts() := #(#"dfmc-environment-application", + #"dfmc-environment-database", + #"dfmc-environment-projects", + #"environment-debugger", + #"environment-profiler", + #"environment-protocols", + #"lsp"); // our own temp category. debug-out(#"lsp", ...) + local method lsp-debug-out (fn :: ) + let (fmt, #rest args) = apply(values, fn()); + // I wish we could log the "part" here, but debug-out drops it. + apply(local-log, concatenate("debug-out: ", fmt), args) + end; + debug-out-function() := lsp-debug-out; + // Not yet... + //*dfmc-debug-out* := #(#"whatever"); // For dfmc-common's debug-out. +end function; + define function main (name :: , arguments :: ) //one-off-debug(); @@ -600,6 +621,7 @@ define function main // Command line processing if (member?("--debug", arguments, test: \=)) *debug-mode* := #t; + enable-od-environment-debug-logging(); end if; // Set up. let msg = #f; From 87d40c3e2792160813a0ba115b5e07415e3d137f Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 14 Mar 2021 18:32:39 +0000 Subject: [PATCH 22/32] Always open projects via registry This is a work-around for the fact that opening via the .lid file doesn't work. See comment in this commit. This makes projects open correctly both with and without dylan-tool workspaces. --- lsp-dylan.dylan | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index bcd42fc..b2bb924 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -581,7 +581,12 @@ define function find-project-name () => (name :: false-or()) if (type = #"file") let file = as(, name); if (locator-extension(file) = "lid") - return(name); + // TODO(cgay): This strips the extension so that the project will be + // opened via the registry because when it's opened via the .lid file + // directly the database doesn't get opened. Note that when opened by + // .lid file it opens a whereas when opened + // via the registry it opens a . + return(locator-base(file)); end if; end if; end method; From 786d176b5682fd82c3ad989ad80e65de2ebcea38 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 14 Mar 2021 21:10:16 +0000 Subject: [PATCH 23/32] Standardize the code style a little bit Rather than change things piecemeal as they come up... * Switched from /* */ to // comments simply because dylan-mode deals with them better. * Changed parameter lists to the prevailing style: both params and values on the following line if they'll fit. --- compiler.dylan | 37 +++++------ lsp-dylan.dylan | 167 +++++++++++++++++++++--------------------------- 2 files changed, 91 insertions(+), 113 deletions(-) diff --git a/compiler.dylan b/compiler.dylan index 48700da..6581a8b 100644 --- a/compiler.dylan +++ b/compiler.dylan @@ -3,10 +3,10 @@ Synopsis: Communicaton with the Dylan command-line compiler Author: Peter Copyright: 2019 -/* The basis of this code is taken from the dswank module -* author: Andreas Bogk and Hannes Mehnert -* copyright: Original Code is Copyright (c) 2008-2012 Dylan Hackers; All rights reversed. -*/ +// The basis of this code is taken from the dswank module. +// Author: Andreas Bogk and Hannes Mehnert +// Copyright: Original Code is Copyright (c) 2008-2012 Dylan Hackers; All rights reversed. + define variable *server* = #f; define variable *project* = #f; @@ -25,13 +25,12 @@ define function run-compiler(server, string :: ) => () execute-command-line(server, string); end function run-compiler; -/* Ask the command line compiler to open a project. - * Param: server - the command line server. - * Param: name - either a library name or a lid file. - * Returns: an instance of -*/ -define function open-project(server, name :: ) - => (project :: ) +// Ask the command line compiler to open a project. +// Param: server - the command line server. +// Param: name - either a library name or a lid file. +// Returns: an instance of +define function open-project + (server, name :: ) => (project :: ) let command = make-command(, server: server.server-context, file: as(, name)); @@ -74,9 +73,9 @@ define function list-all-package-names () do-directory(collect-project, reg-path); end; end; -end; +end function; -define function one-off-debug() +define function one-off-debug () //list-all-package-names(); let in-stream = make(); let out-stream = make(, direction: #"output"); @@ -89,7 +88,7 @@ define function one-off-debug() let symbol-name = "zeor"; let library = project-library(project); let module = find-module(project, "testproject", library: library); -let loc = environment-object-source-location(project, module).source-location-source-record; + let loc = environment-object-source-location(project, module).source-location-source-record; let env = find-environment-object(project, symbol-name, library: library, @@ -120,20 +119,23 @@ let loc = environment-object-source-location(project, module).source-location-so locator-path(fp), locator-path(pfl), same?); -end; +end function; define method n (x :: ) // for debugging! let s = print-environment-object-to-string(*project*, x); format-to-string("%s%s", object-class(x), s); end; + define method n (x :: ) format-to-string("\"%s\"", x) end; + define method n (x :: ) format-to-string("locator:\"%s\"", as(, x)) end; -define method n ( x == #f) + +define method n (x == #f) "#f" end; @@ -144,5 +146,4 @@ define function get-environment-object find-environment-object(*project*, symbol-name, library: library, module: module); -end; - +end function; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index b2bb924..b65cb23 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -51,47 +51,39 @@ define function make-range(start, endp) json("start", start, "end", endp); end function; -/* - * Make a Position object - * See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#position - */ -define function make-position(line, character) - json("line", line, "character", character); +// Make json for a Position object. +// See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#position +define function make-position (line, character) + json("line", line, "character", character) end function; -/* - * Make a Location that's 'zero size' range - * See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#location - */ -define function make-location(doc, line, character) + +// Make json for a Location that's a 'zero size' range. +// See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#location +define function make-location (doc, line, character) let pos = make-position(line, character); json("uri", doc, "range", make-range(pos, pos)) -end; +end function; -/* - * Decode a Position object. - * Note line and character are zero-based. - * See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#position - */ -define function decode-position(position) +// Decode a Position json object. Note line and character are zero-based. +// See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#position +define function decode-position (position) => (line :: , character :: ) - let line = as(, position["line"]); - let character = as(, position["character"]); + let line = string-to-integer(position["line"]); + let character = string-to-integer(position["character"]); values(line, character) end function; -/* - * Create a MarkupContent object. - * See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#markupContent - */ -define function make-markup(txt, #key markdown = #f) +// Create a MarkupContent json object. +// See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#markupContent +define function make-markup (txt, #key markdown) let kind = if (markdown) "markdown" else "plaintext" end; json("value", txt, - "kind", kind); + "kind", kind) end function; define function handle-workspace/symbol (session :: , @@ -109,14 +101,12 @@ define function handle-workspace/symbol (session :: , send-response(session, id, symbols); end function; -/* Show information about a symbol when we hover the cursor over it - * See: https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_hover - * Parameters: textDocument, position, (optional) workDoneToken - * Returns: contents, (optional) range - */ -define function handle-textDocument/hover(session :: , - id :: , - params :: ) => () +// Show information about a symbol when we hover the cursor over it +// See: https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_hover +// Parameters: textDocument, position, (optional) workDoneToken +// Returns: contents, (optional) range +define function handle-textDocument/hover + (session :: , id :: , params :: ) => () // TODO this is only a dummy let text-document = params["textDocument"]; let uri = text-document["uri"]; @@ -134,9 +124,8 @@ define function handle-textDocument/hover(session :: , end; end function; -define function handle-textDocument/didOpen(session :: , - id :: , - params :: ) => () +define function handle-textDocument/didOpen + (session :: , id :: , params :: ) => () // TODO this is only a dummy let textDocument = params["textDocument"]; let uri = textDocument["uri"]; @@ -218,9 +207,8 @@ define function handle-textDocument/definition send-response(session, id, location); end function; -define function handle-workspace/didChangeConfiguration(session :: , - id :: , - params :: ) => () +define function handle-workspace/didChangeConfiguration + (session :: , id :: , params :: ) => () // NOTE: vscode always sends this just after initialized, whereas // emacs does not, so we need to ask for config items ourselves and // not wait to be told. @@ -235,14 +223,6 @@ define function handle-workspace/didChangeConfiguration(session :: , test-open-project(session); end function; -define function trailing-slash(s :: ) => (s-with-slash :: ) - if (s[s.size - 1] = '/') - s - else - concatenate(s, "/") - end -end; - /* Handler for 'initialized' message. * * Example: {"jsonrpc":"2.0","method":"initialized","params":{}} @@ -335,24 +315,23 @@ define function test-open-project(session) => () local-log("test-open-project: Database: %=", project-compiler-database(*project*)); end function; -define function ensure-trailing-slash(s :: ) => (s-slash :: ) +define function ensure-trailing-slash + (s :: ) => (s-slash :: ) if (ends-with?(s, "/")) s else concatenate(s, "/") - end; + end end function; -/* Handle the 'initialize' message. - * Here we initialize logging/tracing and store the workspace root for later. - * Here we return the 'static capabilities' of this server. - * In the future we can register capabilities dynamically by sending messages - * back to the client; this seems to be the preferred 'new' way to do things. - * https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#initialize -*/ -define function handle-initialize (session :: , - id :: , - params :: ) => () +// Handle the 'initialize' message. +// Here we initialize logging/tracing and store the workspace root for later. +// Here we return the 'static capabilities' of this server. +// In the future we can register capabilities dynamically by sending messages +// back to the client; this seems to be the preferred 'new' way to do things. +// https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#initialize +define function handle-initialize + (session :: , id :: , params :: ) => () // The very first received message is "initialize" (I think), and it seems // that for some reason it doesn't get logged, so log params here. The params // for this method are copious, so we log them with pretty printing. @@ -437,11 +416,10 @@ define function find-workspace-root end end function; -define function handle-workspace/workspaceFolders (session :: , - params :: ) - => () -// TODO: handle multi-folder workspaces. - local-log("Workspace folders were received"); +define function handle-workspace/workspaceFolders + (session :: , params :: ) => () + // TODO: handle multi-folder workspaces. + local-log("Workspace folders were received: %=", params); end; // Maps URI strings to objects. @@ -495,21 +473,19 @@ define function symbol-at-position local-log("line %d column %d not in range for document %s", line, column, doc.document-uri); #f - end; + end end function; -define function unregister-file(uri) +define function unregister-file (uri) // TODO remove-key!($documents, uri) end function; -/* - * Make a file:// URI from a local file path. - * This is supposed to follow RFC 8089 - * (locators library not v. helpful here) - */ -define function make-file-uri (f :: ) - => (uri :: ) +// Make a file:// URI from a local file path. +// This is supposed to follow RFC 8089 +// (locators library not v. helpful here) +define function make-file-uri + (f :: ) => (uri :: ) if (f.locator-relative?) f := merge-locators(f, working-directory()); end; @@ -520,14 +496,14 @@ define function make-file-uri (f :: ) make(, directory: directory, name: locator-name(f)) -end; +end function; -define function make-file-locator (f :: ) - => (loc :: ) - /* TODO - what if it isnt a file:/, etc etc */ +define function make-file-locator + (f :: ) => (loc :: ) + // TODO - what if it isnt a file:/, etc etc let d = make(, path: locator-path(f)); make(, directory: d, name: locator-name(f)) -end; +end function; // Look up a symbol. Return the containing doc, // the line and column @@ -545,20 +521,20 @@ define function lookup-symbol local-log("Looking up %s, not found", symbol); #f end -end; +end function; -/* Find the project name to open. - * Either it is set in the per-directory config (passed in from the client) - * or we'll guess it is the only lid file in the workspace root. - * If there is more than one lid file, that's an error, don't return - * any project. - * Returns: the name of a project - * - * TODO(cgay): Really we need to search the LID files to find the file in the - * textDocument/didOpen message so we can figure out which library's project - * to open. - */ -define function find-project-name () => (name :: false-or()) +// Find the project name to open. +// Either it is set in the per-directory config (passed in from the client) +// or we'll guess it is the only lid file in the workspace root. +// If there is more than one lid file, that's an error, don't return +// any project. +// Returns: the name of a project +// +// TODO(cgay): Really we need to search the LID files to find the file in the +// textDocument/didOpen message so we can figure out which library's project +// to open. +define function find-project-name + () => (name :: false-or()) if (*project-name*) // We've set it explicitly local-log("Project name explicitly:%s", *project-name*); @@ -597,9 +573,10 @@ define function find-project-name () => (name :: false-or()) end if end function; +// This makes it possible to modify the OD environment sources with debug-out +// messages and see them in our local logs. debug-out et al are from the +// simple-debugging:dylan module. define function enable-od-environment-debug-logging () - // For simple-debugging's debug-out. This makes it possible to modify the OD - // environment sources with debug-out messages and see them in our local logs. debugging?() := #t; // Added most of the sources/environment/ debug-out categories here. --cgay debug-parts() := #(#"dfmc-environment-application", @@ -715,7 +692,7 @@ define function main end function main; ignore(*library*, run-compiler, describe-symbol, list-all-package-names, - document-lines-setter, trailing-slash, unregister-file, + document-lines-setter, unregister-file, one-off-debug, dump, show-warning, show-log, show-error); main(application-name(), application-arguments()); From 1b48cc8966f24578314132ed4f4805f4c176d8d8 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Mon, 15 Mar 2021 03:00:11 +0000 Subject: [PATCH 24/32] Use command-line-parser to parse lsp-dylan command line Changed --debug to --debug-server and added --debug-opendylan to enable OD's debug-out going into our log. --- .gitmodules | 3 + ext/command-line-parser | 1 + library.dylan | 3 + lsp-dylan.dylan | 102 ++++++++++++++++----------- registry/generic/command-line-parser | 1 + setup.el | 2 +- 6 files changed, 68 insertions(+), 44 deletions(-) create mode 160000 ext/command-line-parser create mode 100644 registry/generic/command-line-parser diff --git a/.gitmodules b/.gitmodules index 73242d7..a38e66d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "ext/pacman"] path = ext/pacman url = git@github.com:cgay/pacman +[submodule "ext/command-line-parser"] + path = ext/command-line-parser + url = git@github.com:dylan-lang/command-line-parser diff --git a/ext/command-line-parser b/ext/command-line-parser new file mode 160000 index 0000000..014c500 --- /dev/null +++ b/ext/command-line-parser @@ -0,0 +1 @@ +Subproject commit 014c5009fef0f2cc3e9c5c9e42ffc16c0d4ca239 diff --git a/library.dylan b/library.dylan index 2ae727f..f2fc900 100644 --- a/library.dylan +++ b/library.dylan @@ -2,6 +2,7 @@ Module: dylan-user define library lsp-dylan use build-system; + use command-line-parser; use commands; use common-dylan; use dfmc-back-end-implementations; @@ -27,6 +28,8 @@ end library; define module lsp-dylan use build-system; + use command-line-parser, + prefix: "clp/"; use command-lines; use commands; use common-dylan; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index b65cb23..6b88d34 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -69,8 +69,8 @@ end function; // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#position define function decode-position (position) => (line :: , character :: ) - let line = string-to-integer(position["line"]); - let character = string-to-integer(position["character"]); + let line = position["line"]; + let character = position["character"]; values(line, character) end function; @@ -573,41 +573,17 @@ define function find-project-name end if end function; -// This makes it possible to modify the OD environment sources with debug-out -// messages and see them in our local logs. debug-out et al are from the -// simple-debugging:dylan module. -define function enable-od-environment-debug-logging () - debugging?() := #t; - // Added most of the sources/environment/ debug-out categories here. --cgay - debug-parts() := #(#"dfmc-environment-application", - #"dfmc-environment-database", - #"dfmc-environment-projects", - #"environment-debugger", - #"environment-profiler", - #"environment-protocols", - #"lsp"); // our own temp category. debug-out(#"lsp", ...) - local method lsp-debug-out (fn :: ) - let (fmt, #rest args) = apply(values, fn()); - // I wish we could log the "part" here, but debug-out drops it. - apply(local-log, concatenate("debug-out: ", fmt), args) - end; - debug-out-function() := lsp-debug-out; - // Not yet... - //*dfmc-debug-out* := #(#"whatever"); // For dfmc-common's debug-out. -end function; +define function lsp-server-top-level + (command :: ) => () + *debug-mode* := command.debug-server?; + if (command.debug-opendylan?) + enable-od-environment-debug-logging(); + end; -define function main - (name :: , arguments :: ) //one-off-debug(); - // Command line processing - if (member?("--debug", arguments, test: \=)) - *debug-mode* := #t; - enable-od-environment-debug-logging(); - end if; // Set up. let msg = #f; - let retcode = 1; let session = make(); // Pre-init state while (session.state == $session-preinit) @@ -669,27 +645,67 @@ define function main // Shutdown state while (session.state == $session-shutdown) local-log("main: state = shutdown"); - let (meth, id, params) = receive-message(session); + let (meth, id, params) = receive-message(session); select (meth by =) "exit" => - begin - retcode := 0; - session.state := $session-killed; - end; + local-log("Dylan LSP server exiting"); + clp/abort-command(0); otherwise => // Respond to any request with an invalid error, // Drop any notifications - begin - if (id) - send-error-response(session, id, $invalid-request); - end if; + if (id) + send-error-response(session, id, $invalid-request); end; end select; flush(session); end while; +end function lsp-server-top-level; + +// This makes it possible to modify the OD environment sources with debug-out +// messages and see them in our local logs. debug-out et al are from the +// simple-debugging:dylan module. +define function enable-od-environment-debug-logging () + debugging?() := #t; + // Added most of the sources/environment/ debug-out categories here. --cgay + debug-parts() := #(#"dfmc-environment-application", + #"dfmc-environment-database", + #"dfmc-environment-projects", + #"environment-debugger", + #"environment-profiler", + #"environment-protocols", + #"lsp"); // our own temp category. debug-out(#"lsp", ...) + local method lsp-debug-out (fn :: ) + let (fmt, #rest args) = apply(values, fn()); + // I wish we could log the "part" here, but debug-out drops it. + apply(local-log, concatenate("debug-out: ", fmt), args) + end; + debug-out-function() := lsp-debug-out; + // Not yet... + //*dfmc-debug-out* := #(#"whatever"); // For dfmc-common's debug-out. +end function; + +define clp/command-line () + option debug-server? :: = #t, // default to #f eventually + names: #("debug-server"), + kind: clp/, + help: "Turn on debugging for the LSP server."; + option debug-opendylan? :: = #t, // default to #f eventually + names: #("debug-opendylan"), + kind: clp/, + help: "Turn on debugging for Open Dylan."; +end clp/command-line; - exit-application(retcode); -end function main; +define function main + (name :: , arguments :: ) + let command = make(, + help: "Dylan LSP server"); + block () + clp/parse-command-line(command, application-arguments()); + lsp-server-top-level(command); + exception (err :: clp/) + exit-application(clp/exit-status(err)); + end; +end function; ignore(*library*, run-compiler, describe-symbol, list-all-package-names, document-lines-setter, unregister-file, diff --git a/registry/generic/command-line-parser b/registry/generic/command-line-parser new file mode 100644 index 0000000..1b3d3ea --- /dev/null +++ b/registry/generic/command-line-parser @@ -0,0 +1 @@ +abstract://dylan/ext/command-line-parser/command-line-parser.lid \ No newline at end of file diff --git a/setup.el b/setup.el index 12c2d92..f25d0d1 100644 --- a/setup.el +++ b/setup.el @@ -20,7 +20,7 @@ (t (error "Couldn't find the lsp-dylan executable")))) (full-path (expand-file-name chosen-path (file-name-directory load-file-name))) - (server (list full-path "--debug"))) + (server (list full-path "--debug-server" "--debug-opendylan"))) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection server) :major-modes '(dylan-mode) From 453253aeaaa5dce2d4d85b299124e98f3d6c3a02 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Mon, 15 Mar 2021 03:01:43 +0000 Subject: [PATCH 25/32] Bring README.md up to date with current status --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c664c1f..76ff377 100644 --- a/README.md +++ b/README.md @@ -2,37 +2,79 @@ This is an implementation of the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) for -Dylan. At the moment it will respond to the initialize/ shutdown -sequence but does not actually implement any of the methods defined in -the LSP. Note that this project includes a git submodule for -json. This adds `null` handling and a method to output json as a -string to the open-dylan/json project. +Dylan. -We are currently using version [3.15 of the LSP protocol](https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/) -## Usage +## Current Status + +As of 2021.03.14, the only function fully implemented is "jump to definition" +and (at least in Emacs) when you jump to another `.dylan` file, that file does +not in automatically have LSP enabled so you must use `M-x lsp` again. + +We are currently using version [3.15 of the LSP protocol](https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/). + + +## Opening Projects + +The LSP server needs to be able to open a project (essentially a Dylan library) +associated with the file you're editing when you turn on LSP in your editor. It +makes two attempts, in the following order: + +1. Search up in the directory structure until it finds a `workspace.json` file, + which indicates a [dylan-tool](https://github.com/cgay/dylan-tool) + workspace. In this case it looks for the "default-library" setting in the + workspace file and opens that library. If there is no default library set + and there is only one "active" library, it uses that. Otherwise it fails. + +2. Search up in the directory structure for a `registry` directory and open the + library associated with the first `*.lid` file it finds there. (Note that it + currently removes the `.lid` suffix and assumes that a library by the same + name as the basename of the file can be opened via the registry. This should + eventually be fixed.) + + +## Emacs Usage Testing with Emacs [lsp-mode](https://github.com/emacs-lsp/lsp-mode). 1. Install lsp-mode (see github project page for details) -2. Start emacs with `emacs --load=setup.el testproject/testproject.dylan` in - this directory. (For now "testproject" is the single, hard-coded project - name, soon to be fixed.) -3. Type `M-x lsp` to start the client, which will connect to the server -The file `setup.el` is used just to avoid making any changes to the -user's `.emacs`. +2. Set environment variables. + + a. Currently the LSP server only opens projects via the Dylan registry so + it's important to either start emacs in the directory containing your + "registry" directory or set `OPEN_DYLAN_USER_REGISTRIES` to contain that + registry directory. + + If you are developing the lsp-dylan code itself and also modifying Open + Dylan at the same time, you may want to include + `.../opendylan/sources/registry` in the list as well. Otherwise, when you + use `M-.` etc to jump to definitions for used libraries, files in the + Open Dylan install directory (which is not under source control) will be + opened. For example: + + export OPEN_DYLAN_USER_REGISTRIES=/home/you/lsp-dylan/registry:/home/you/opendylan/sources/registry + + b. Point `OPEN_DYLAN_RELEASE_INSTALL` at the Open Dylan installation + directory. This is necessary so that it can find the Jam build scripts, + and core libraries. For example: + + export OPEN_DYLAN_RELEASE_INSTALL=/home/you/opendylan-2021.1 + +3. Start emacs and make sure that `setup.el` is loaded. For example: + + `emacs --load=/home/you/lsp-dylan/setup.el` + + Obviously you may modify your Emacs init file instead, if you prefer. -If you are **not** using [dylan-tool](https://github.com/cgay/dylan-tool) then -you must set `OPEN_DYLAN_RELEASE_INSTALL` to wherever your "opendylan" -directory is (so that it can find the Jam build scripts) and -`OPEN_DYLAN_USER_REGISTRIES` to the appropriate "registry" directory. +4. Open a Dylan source file and type `M-x lsp` to start the client. The client + starts the LSP server (the `lsp-dylan` executable) and connects to it. -Currently the only function is `lsp-find-definition` which will jump to the -definition of the symbol under the cursor. Unfortunately it is still not -reliable and depends on some hard-coded defaults. + Currently `lsp-dylan` must be in `./_build/bin/lsp-dylan` or + `${DYLAN}/workspaces/lsp/_build/bin/lsp-dylan`. (TODO: search for it on + `PATH`.) -Testing with VS Code (1.45.0 on macos) +## VS Code Usage (1.45.0 on macos) 1. Open the `vscode` folder in VS Code 1. First time only, `npm install` to get the dependencies From 69ba3bacb063f6cecd18326008d3cb1bf91c3456 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Wed, 17 Mar 2021 01:06:59 +0000 Subject: [PATCH 26/32] Make M-. work when at end-of-line --- lsp-dylan.dylan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 6b88d34..9977322 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -454,7 +454,7 @@ define function symbol-at-position if (line >= 0 & line < size(doc.document-lines) & column >= 0 - & column < size(doc.document-lines[line])) + & column <= size(doc.document-lines[line])) let line = doc.document-lines[line]; local method name-character?(c) => (well? :: ) member?(c, $dylan-name-characters) From 514170b1b1fe21c3aa71585863c34ea67b4b717e Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 20 Mar 2021 20:03:49 +0000 Subject: [PATCH 27/32] Log to a temp file as well as stderr Log to a rolling temp file so we have a history and because I've seen the Emacs LSP client's `*dylan-lsp::stderr*` buffer not be kept up to date when the process restarts. --- lsp-dylan.dylan | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 9977322..dabbd69 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -4,10 +4,20 @@ Author: Peter Copyright: 2019 +define constant $lsp-log-target + = make(, + pathname: merge-locators(as(,"lsp.log"), + temp-directory())); + define constant $log = make(, - name: "lsp-dylan", - targets: list($stderr-log-target), + name: "lsp", + // Log to stderr so it shows up in the *dylan-lsp::stderr* buffer. + // Log to a rolling temp file so we have a history and because I've + // seen the Emacs LSP client's *dylan-lsp::stderr* buffer not be kept + // up to date when the process restarts. + targets: list($stderr-log-target, + $lsp-log-target), // For now just displaying millis is a good way to identify all the // messages that belong to a given call/response, and it's terse. formatter: "%{millis} %{level} [%{thread}] - %{message}"); From 72605e53a3e6c8a9027f671ecc44621725d19642 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 20 Mar 2021 22:39:19 +0000 Subject: [PATCH 28/32] Add more logging, use default log formatter --- lsp-dylan.dylan | 9 ++++----- setup.el | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index dabbd69..18a0032 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -17,10 +17,7 @@ define constant $log // seen the Emacs LSP client's *dylan-lsp::stderr* buffer not be kept // up to date when the process restarts. targets: list($stderr-log-target, - $lsp-log-target), - // For now just displaying millis is a good way to identify all the - // messages that belong to a given call/response, and it's terse. - formatter: "%{millis} %{level} [%{thread}] - %{message}"); + $lsp-log-target)); define function local-log(m :: , #rest params) => () apply(log-debug, $log, m, params); @@ -446,6 +443,7 @@ define class () end class; define function register-file (uri, contents) + local-log("register-file(%=)", uri); let lines = split-lines(contents); let doc = make(, uri: as(, uri), lines: lines); $documents[uri] := doc; @@ -561,6 +559,7 @@ define function find-project-name #f end; else + local-log("no workspace file found starting in %s", working-directory()); // Guess based on there being one .lid file in the workspace root block(return) local method return-lid(dir, name, type) @@ -577,7 +576,7 @@ define function find-project-name end if; end method; do-directory(return-lid, working-directory()); - local-log("find-project-name found no LID files"); + local-log("find-project-name found no LID files in %s", working-directory()); #f end block end if diff --git a/setup.el b/setup.el index f25d0d1..7e951b1 100644 --- a/setup.el +++ b/setup.el @@ -19,7 +19,8 @@ (concat (getenv "DYLAN") "/workspaces/lsp/" relative-path)) (t (error "Couldn't find the lsp-dylan executable")))) - (full-path (expand-file-name chosen-path (file-name-directory load-file-name))) + (full-path (expand-file-name chosen-path + (file-name-directory (or load-file-name "")))) (server (list full-path "--debug-server" "--debug-opendylan"))) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection server) From f5c16104ed649b784b125e782ca7a5b7af962622 Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sat, 20 Mar 2021 23:00:27 +0000 Subject: [PATCH 29/32] Break lsp-server-top-level apart --- lsp-dylan.dylan | 50 ++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 18a0032..18b38f9 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -582,21 +582,10 @@ define function find-project-name end if end function; -define function lsp-server-top-level - (command :: ) => () - *debug-mode* := command.debug-server?; - if (command.debug-opendylan?) - enable-od-environment-debug-logging(); - end; - - //one-off-debug(); - - // Set up. - let msg = #f; - let session = make(); - // Pre-init state +define function lsp-pre-init-state-loop + (session :: ) => () while (session.state == $session-preinit) - local-log("main: state = pre-init"); + local-log("lsp-pre-init-state-loop: waiting for message"); let (meth, id, params) = receive-message(session); select (meth by =) "initialize" => handle-initialize(session, id, params); @@ -609,9 +598,12 @@ define function lsp-server-top-level end select; flush(session); end while; - // Active state +end function; + +define function lsp-active-state-loop + (session :: ) => () while (session.state == $session-active) - local-log("main: state = active"); + local-log("lsp-active-state-loop: waiting for message"); let (meth, id, params) = receive-message(session); select (meth by =) // TODO(cgay): It would be nice to turn params into a set of keyword/value @@ -651,9 +643,12 @@ define function lsp-server-top-level end select; flush(session); end while; - // Shutdown state +end function; + +define function lsp-shutdown-state-loop + (session :: ) => () while (session.state == $session-shutdown) - local-log("main: state = shutdown"); + local-log("lsp-shutdown-state-loop: waiting for message"); let (meth, id, params) = receive-message(session); select (meth by =) "exit" => @@ -668,7 +663,24 @@ define function lsp-server-top-level end select; flush(session); end while; -end function lsp-server-top-level; +end function; + +define function lsp-server-top-level + (command :: ) => () + *debug-mode* := command.debug-server?; + if (command.debug-opendylan?) + enable-od-environment-debug-logging(); + end; + + let session = make(); + block () + lsp-pre-init-state-loop(session); + lsp-active-state-loop(session); + lsp-shutdown-state-loop(session); + cleanup + local-log("lsp-server-top-level exiting: bye!"); + end; +end function; // This makes it possible to modify the OD environment sources with debug-out // messages and see them in our local logs. debug-out et al are from the From 1cca3939ac32a86f9ea66e7e53bc63000c94501e Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 21 Mar 2021 16:33:13 +0000 Subject: [PATCH 30/32] Basic implementations for td/didSave and td/didChange --- compiler.dylan | 3 +- lsp-dylan.dylan | 125 +++++++++++++++++++++++++++++++----------------- 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/compiler.dylan b/compiler.dylan index 6581a8b..b4f1337 100644 --- a/compiler.dylan +++ b/compiler.dylan @@ -36,7 +36,8 @@ define function open-project file: as(, name)); let project = execute-command(command); local-log("Result of opening %s is %=", name, project); - local-log("Result of find %s is %=", project-name(project), + local-log("Result of find %s is %=", + project-name(project), find-project(project-name(project))); project end function; diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 18b38f9..9ce0e53 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -1,5 +1,5 @@ Module: lsp-dylan -Synopsis: Test stuff for language server protocol +Synopsis: Language Server Protocol (LSP) server for Dylan Author: Peter Copyright: 2019 @@ -160,20 +160,67 @@ define function handle-textDocument/didOpen end if; end function; -// Go to definition. -// Sent by M-. (emacs), ??? (VSCode). +define function foob () 5 end; + +// A document was saved. For Emacs, this is called when M-x lsp is executed on +// a new file. For now we don't care about the message at all, we just trigger +// a compilation of the associated project (if any) unconditionally. +// https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_didSave +define function handle-textDocument/didSave + (session :: , id :: , params :: ) => () + let textDocument = params["textDocument"]; + let uri = textDocument["uri"]; + let project = find-project-name(); + local-log("textDocument/didSave: File %s, project %=", uri, project); + foob(); + if (project) + let project-object = find-project(project); + local-log("textDocument/didSave: project = %=", project-object); + if (project-object) + build-project(project-object); + local-log("textDocument/didSave: done building %=", project); + else + show-error("Project %s not found.", project); + end; + else + local-log("handle-textDocument/didSave: project not found for %=", uri); + end; +end function; + +// https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_didChange +define function handle-textDocument/didChange + (session :: , id :: , params :: ) => () + let text-document = params["textDocument"]; + let uri = text-document["uri"]; + let document = element($documents, uri, default: #f); + if (document) + let changes = params["contentChanges"]; + for (change in changes) + apply-change(session, document, change); + end; + else + show-error(session, format-to-string("Document not found on server: %s", uri)); + end; +end function; + +// Apply a sequence of changes to a document. Each change is a +// TextDocumentContentChangeEvent json object that has a "text" attribute and optional +// "range" attribute. If there is no range then text contains the entire new document. +define function apply-change + (session :: , document :: , change :: ) => () + let text = change["text"]; + let range = element(change, "range", default: #f); + if (range) + show-error(session, "didChange doesn't support ranges yet"); + else + local-log("document replaced: %s", document.document-uri); + show-info(session, "Document content replaced"); + document-lines(document) := split-lines(text); + end; +end function; + +// Jump to definition. // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_definition -// Example JSON: -// { "jsonrpc": "2.0", -// "method": "textDocument/definition", -// "params": { -// "textDocument": { -// "uri": "file:///home/cgay/dylan/workspaces/lsp/lsp-dylan/testproject/testproject.dylan" -// }, -// "position": { "line": 9, "character": 16} -// }, -// "id": 2 -// } define function handle-textDocument/definition (session :: , id :: , params :: ) => () let text-document = params["textDocument"]; @@ -184,6 +231,7 @@ define function handle-textDocument/definition let location = $null; if (~doc) local-log("textDocument/definition: document not found: %=", uri); + show-error(session, format-to-string("Document not found: %s", uri)); else unless (doc.document-module) let local-dir = make(, path: locator-path(doc.document-uri)); @@ -541,6 +589,8 @@ end function; // TODO(cgay): Really we need to search the LID files to find the file in the // textDocument/didOpen message so we can figure out which library's project // to open. +// TODO(cgay): accept a locator argument so we know where to start, rather than +// using working-directory(). Also better for testing. define function find-project-name () => (name :: false-or()) if (*project-name*) @@ -570,7 +620,7 @@ define function find-project-name // opened via the registry because when it's opened via the .lid file // directly the database doesn't get opened. Note that when opened by // .lid file it opens a whereas when opened - // via the registry it opens a . + // via the registry it opens a . Go figure. return(locator-base(file)); end if; end if; @@ -606,39 +656,26 @@ define function lsp-active-state-loop local-log("lsp-active-state-loop: waiting for message"); let (meth, id, params) = receive-message(session); select (meth by =) - // TODO(cgay): It would be nice to turn params into a set of keyword/value - // pairs and apply(the-method, session, id, params) so that the parameters - // to each method are clear from the #key parameters. - "initialize" => - send-error-response(session, id, $invalid-request); + "exit" => session.state := $session-killed; + "initialize" => send-error-response(session, id, $invalid-request); "initialized" => handle-initialized(session, id, params); - "workspace/symbol" => handle-workspace/symbol(session, id, params); - "textDocument/hover" => handle-textDocument/hover(session, id, params); - "textDocument/didOpen" => handle-textDocument/didOpen(session, id, params); + "shutdown" => + send-response(session, id, $null); + session.state := $session-shutdown; "textDocument/definition" => handle-textDocument/definition(session, id, params); + "textDocument/didChange" => handle-textDocument/didChange(session, id, params); + "textDocument/didOpen" => handle-textDocument/didOpen(session, id, params); + "textDocument/didSave" => handle-textDocument/didSave(session, id, params); + "textDocument/hover" => handle-textDocument/hover(session, id, params); "workspace/didChangeConfiguration" => handle-workspace/didChangeConfiguration(session, id, params); - // TODO handle all other messages here - "shutdown" => - begin - // TODO shutdown everything - send-response(session, id, $null); - session.state := $session-shutdown; - end; - "exit" => session.state := $session-killed; + "workspace/symbol" => handle-workspace/symbol(session, id, params); otherwise => - // Respond to any other request with an not-implemented error. - // Drop any other notifications - begin - local-log("main: %s '%s' is not implemented", - if (id) - "Request" - else - "Notification" - end, - meth); - if (id) - send-error-response(session, id, $method-not-found); - end if; + // Respond to any other request with an not-implemented error. + // Drop any other notifications + local-log("lsp-active-state-loop: %s method '%s' is not yet implemented.", + if (id) "Request" else "Notification" end, meth); + if (id) + send-error-response(session, id, $method-not-found); end; end select; flush(session); From da3531c24bd3d1872ba0e6536075ddd01de523eb Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 21 Mar 2021 16:48:44 +0000 Subject: [PATCH 31/32] Make show-info et al more concise --- lsp-dylan.dylan | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 9ce0e53..9d42419 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -29,30 +29,16 @@ define constant $message-type-info = 3; define constant $message-type-log = 4; define method window/show-message - (session :: , msg-type :: , msg :: ) => () + (msg-type :: , session :: , fmt :: , #rest args) => () + let msg = apply(format-to-string, fmt, args); let params = json("type", msg-type, "message", msg); send-notification(session, "window/showMessage", params); end method; -define method show-error - (session :: , msg :: ) => () - window/show-message(session, $message-type-error, msg); -end method; - -define inline method show-warning - (session :: , msg :: ) => () - window/show-message(session, $message-type-warning, msg); -end method; - -define inline method show-info - (session :: , msg :: ) => () - window/show-message(session, $message-type-info, msg); -end method; - -define inline method show-log - (session :: , msg :: ) => () - window/show-message(session, $message-type-log, msg); -end method; +define constant show-error = curry(window/show-message, $message-type-error); +define constant show-warning = curry(window/show-message, $message-type-warning); +define constant show-info = curry(window/show-message, $message-type-info); +define constant show-log = curry(window/show-message, $message-type-log); define function make-range(start, endp) json("start", start, "end", endp); From 73641cb2cbb5de752f1ab9532195d31562124eba Mon Sep 17 00:00:00 2001 From: Carl Gay Date: Sun, 21 Mar 2021 17:07:12 +0000 Subject: [PATCH 32/32] Don't link. Note warning count. Delete debug code. --- lsp-dylan.dylan | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lsp-dylan.dylan b/lsp-dylan.dylan index 9d42419..3893710 100644 --- a/lsp-dylan.dylan +++ b/lsp-dylan.dylan @@ -50,7 +50,6 @@ define function make-position (line, character) json("line", line, "character", character) end function; - // Make json for a Location that's a 'zero size' range. // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#location define function make-location (doc, line, character) @@ -146,8 +145,6 @@ define function handle-textDocument/didOpen end if; end function; -define function foob () 5 end; - // A document was saved. For Emacs, this is called when M-x lsp is executed on // a new file. For now we don't care about the message at all, we just trigger // a compilation of the associated project (if any) unconditionally. @@ -158,18 +155,29 @@ define function handle-textDocument/didSave let uri = textDocument["uri"]; let project = find-project-name(); local-log("textDocument/didSave: File %s, project %=", uri, project); - foob(); if (project) let project-object = find-project(project); local-log("textDocument/didSave: project = %=", project-object); if (project-object) - build-project(project-object); + let warnings = make(); + local method note-warning (#rest args) + add!(warnings, args); + end; + // TODO(cgay): do we want `save-databases?: #t` here? + // TODO(cgay): how to display warnings on client side. I assume there's a message + // we should be sending. + build-project(project-object, + link?: #f, + warning-callback: note-warning); local-log("textDocument/didSave: done building %=", project); + show-info(session, "Build complete, %s warnings", + if (empty?(warnings)) "no" else warnings.size end); else show-error("Project %s not found.", project); end; else local-log("handle-textDocument/didSave: project not found for %=", uri); + show-error("Project %s not found.", project); end; end function;