630 lines
22 KiB
Python
630 lines
22 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Talks to the server as an editor would, over stdin and stdout, and checks
|
||
|
|
what it answers. One server process for every case, so that a second check
|
||
|
|
in the same process is tested too.
|
||
|
|
|
||
|
|
test/smoke.py # builds nothing; run `dotnet build -c Release` first
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
|
||
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
SERVER = os.path.join(ROOT, "bin", "Release", "net10.0", "BjolangLsp.dll")
|
||
|
|
|
||
|
|
|
||
|
|
def frame(message):
|
||
|
|
body = json.dumps(message).encode()
|
||
|
|
return b"Content-Length: %d\r\n\r\n" % len(body) + body
|
||
|
|
|
||
|
|
|
||
|
|
def read(stream):
|
||
|
|
length = None
|
||
|
|
while True:
|
||
|
|
line = stream.readline()
|
||
|
|
if not line:
|
||
|
|
raise EOFError("server closed stdout")
|
||
|
|
line = line.strip()
|
||
|
|
if not line:
|
||
|
|
break
|
||
|
|
name, _, value = line.decode().partition(":")
|
||
|
|
if name.lower() == "content-length":
|
||
|
|
length = int(value)
|
||
|
|
return json.loads(stream.read(length))
|
||
|
|
|
||
|
|
|
||
|
|
class Server:
|
||
|
|
def __init__(self):
|
||
|
|
self.proc = subprocess.Popen(
|
||
|
|
["dotnet", SERVER], stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||
|
|
)
|
||
|
|
self.next_id = 0
|
||
|
|
# Notifications read while waiting for something else: a check that
|
||
|
|
# fell due can publish between a request and its answer.
|
||
|
|
self.pending = []
|
||
|
|
|
||
|
|
def send(self, method, params=None, request=False):
|
||
|
|
message = {"jsonrpc": "2.0", "method": method, "params": params or {}}
|
||
|
|
if request:
|
||
|
|
self.next_id += 1
|
||
|
|
message["id"] = self.next_id
|
||
|
|
self.proc.stdin.write(frame(message))
|
||
|
|
self.proc.stdin.flush()
|
||
|
|
if not request:
|
||
|
|
return None
|
||
|
|
while True:
|
||
|
|
answer = read(self.proc.stdout)
|
||
|
|
if answer.get("id") == self.next_id:
|
||
|
|
return answer
|
||
|
|
self.pending.append(answer)
|
||
|
|
|
||
|
|
def change(self, uri, text, version):
|
||
|
|
self.send(
|
||
|
|
"textDocument/didChange",
|
||
|
|
{"textDocument": {"uri": uri, "version": version}, "contentChanges": [{"text": text}]},
|
||
|
|
)
|
||
|
|
|
||
|
|
def published(self, uri):
|
||
|
|
"""The diagnostics for `uri` among the notifications already read, as
|
||
|
|
a list of their diagnostics lists, and takes them out."""
|
||
|
|
mine = [
|
||
|
|
m for m in self.pending
|
||
|
|
if m.get("method") == "textDocument/publishDiagnostics" and m["params"]["uri"] == uri
|
||
|
|
]
|
||
|
|
self.pending = [m for m in self.pending if m not in mine]
|
||
|
|
return [m["params"]["diagnostics"] for m in mine]
|
||
|
|
|
||
|
|
def next_diagnostics(self, uri):
|
||
|
|
"""The next diagnostics published for `uri`, waiting for them if
|
||
|
|
none has come yet."""
|
||
|
|
earlier = self.published(uri)
|
||
|
|
if earlier:
|
||
|
|
return earlier[0]
|
||
|
|
while True:
|
||
|
|
message = read(self.proc.stdout)
|
||
|
|
if message.get("method") == "textDocument/publishDiagnostics" and message["params"]["uri"] == uri:
|
||
|
|
return message["params"]["diagnostics"]
|
||
|
|
self.pending.append(message)
|
||
|
|
|
||
|
|
def diagnostics_after(self, method, uri, text=None):
|
||
|
|
doc = {"uri": uri}
|
||
|
|
if text is not None:
|
||
|
|
doc.update(languageId="bjolang", version=1, text=text)
|
||
|
|
self.published(uri) # anything older is not the answer to this
|
||
|
|
self.send(method, {"textDocument": doc})
|
||
|
|
return self.next_diagnostics(uri)
|
||
|
|
|
||
|
|
|
||
|
|
failures = 0
|
||
|
|
|
||
|
|
|
||
|
|
def expect(name, ok, detail=""):
|
||
|
|
global failures
|
||
|
|
print(("ok: " if ok else "FAIL: ") + name + ("" if ok else f"\n {detail}"))
|
||
|
|
if not ok:
|
||
|
|
failures += 1
|
||
|
|
|
||
|
|
|
||
|
|
BROKEN = """(import (std prelude))
|
||
|
|
|
||
|
|
(: main (-> (Vec string) int))
|
||
|
|
(defun (main args)
|
||
|
|
(+ 1 "two"))
|
||
|
|
"""
|
||
|
|
|
||
|
|
FIXED = """(import (std prelude))
|
||
|
|
|
||
|
|
(: main (-> (Vec string) int))
|
||
|
|
(defun (main args)
|
||
|
|
(+ 1 2))
|
||
|
|
"""
|
||
|
|
|
||
|
|
UNCLOSED = """(import (std prelude))
|
||
|
|
|
||
|
|
(defun (main args)
|
||
|
|
(+ 1 2)
|
||
|
|
"""
|
||
|
|
|
||
|
|
SYMBOLS = """(import (std prelude))
|
||
|
|
|
||
|
|
(type (: Shape (Union (Circle int) (Square int) Empty)))
|
||
|
|
(type (: Point (Record (: x int) (: y int))))
|
||
|
|
|
||
|
|
(def/trait (Area %a)
|
||
|
|
(: area (-> %a int)))
|
||
|
|
|
||
|
|
(impl (Area Shape)
|
||
|
|
(defun (area s)
|
||
|
|
(match s
|
||
|
|
((Circle r) (* 3 (* r r)))
|
||
|
|
((Square a) (* a a))
|
||
|
|
(Empty 0))))
|
||
|
|
|
||
|
|
(def unit-size 1)
|
||
|
|
|
||
|
|
(: double (-> int int))
|
||
|
|
(defun (double n) (* 2 n))
|
||
|
|
|
||
|
|
(: main (-> (Vec string) int))
|
||
|
|
(defun (main args)
|
||
|
|
(double (area (Circle unit-size)))
|
||
|
|
(list-map double (list 1 2 3))
|
||
|
|
0)
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def position_of(text, needle, nth=0, offset=1):
|
||
|
|
"""The LSP position `offset` characters into the `nth` occurrence of
|
||
|
|
`needle` in `text`."""
|
||
|
|
at = -1
|
||
|
|
for _ in range(nth + 1):
|
||
|
|
at = text.index(needle, at + 1)
|
||
|
|
at += offset
|
||
|
|
line = text.count("\n", 0, at)
|
||
|
|
return {"line": line, "character": at - (text.rfind("\n", 0, at) + 1)}
|
||
|
|
|
||
|
|
|
||
|
|
def line_of(text, needle):
|
||
|
|
return text.count("\n", 0, text.index(needle))
|
||
|
|
|
||
|
|
|
||
|
|
def names(symbols):
|
||
|
|
return [(s["name"], names(s.get("children", []))) for s in symbols]
|
||
|
|
|
||
|
|
|
||
|
|
def symbols_and_definitions(server, tmp):
|
||
|
|
path = os.path.join(tmp, "symbols.bjo")
|
||
|
|
uri = "file://" + path
|
||
|
|
with open(path, "w") as f:
|
||
|
|
f.write(SYMBOLS)
|
||
|
|
server.diagnostics_after("textDocument/didOpen", uri, SYMBOLS)
|
||
|
|
|
||
|
|
found = server.send("textDocument/documentSymbol", {"textDocument": {"uri": uri}}, request=True)["result"]
|
||
|
|
expect(
|
||
|
|
"the outline",
|
||
|
|
names(found)
|
||
|
|
== [
|
||
|
|
("Shape", [("Circle", []), ("Square", []), ("Empty", [])]),
|
||
|
|
("Point", [("x", []), ("y", [])]),
|
||
|
|
("Area", [("area", [])]),
|
||
|
|
("impl (Area Shape)", [("area", [])]),
|
||
|
|
("unit-size", []),
|
||
|
|
("double", []),
|
||
|
|
("main", []),
|
||
|
|
],
|
||
|
|
names(found),
|
||
|
|
)
|
||
|
|
|
||
|
|
def definition(text, needle, offset=1):
|
||
|
|
answer = server.send(
|
||
|
|
"textDocument/definition",
|
||
|
|
{"textDocument": {"uri": uri}, "position": position_of(text, needle, 0, offset)},
|
||
|
|
request=True,
|
||
|
|
)
|
||
|
|
return answer["result"]
|
||
|
|
|
||
|
|
d = definition(SYMBOLS, "(double (area")
|
||
|
|
expect(
|
||
|
|
"a name defined in the file",
|
||
|
|
d and d["uri"] == uri and d["range"]["start"]["line"] == line_of(SYMBOLS, "(defun (double"),
|
||
|
|
d,
|
||
|
|
)
|
||
|
|
d = definition(SYMBOLS, "(Circle unit-size")
|
||
|
|
expect(
|
||
|
|
"a constructor",
|
||
|
|
d and d["range"]["start"]["line"] == line_of(SYMBOLS, "(type (: Shape"),
|
||
|
|
d,
|
||
|
|
)
|
||
|
|
d = definition(SYMBOLS, "(list-map")
|
||
|
|
expect("a name from the prelude", d and d["uri"].endswith("/lib/std/prelude.bjo"), d)
|
||
|
|
# `n` is a parameter: it goes to where the parameter is written.
|
||
|
|
d = definition(SYMBOLS, "(* 2 n)", 5)
|
||
|
|
expect(
|
||
|
|
"a parameter goes to the parameter",
|
||
|
|
d and d["range"]["start"] == position_of(SYMBOLS, "(defun (double n)", 0, 15),
|
||
|
|
d,
|
||
|
|
)
|
||
|
|
|
||
|
|
def hover(text, needle, offset=1):
|
||
|
|
answer = server.send(
|
||
|
|
"textDocument/hover",
|
||
|
|
{"textDocument": {"uri": uri}, "position": position_of(text, needle, 0, offset)},
|
||
|
|
request=True,
|
||
|
|
)
|
||
|
|
result = answer["result"]
|
||
|
|
return result["contents"]["value"] if result else None
|
||
|
|
|
||
|
|
h = hover(SYMBOLS, "(double (area")
|
||
|
|
expect(
|
||
|
|
"hover: the declaration and the head",
|
||
|
|
h and "(: double (-> int int))" in h and "(defun (double n))" in h,
|
||
|
|
h,
|
||
|
|
)
|
||
|
|
h = hover(SYMBOLS, "(Circle unit-size")
|
||
|
|
expect("hover: a constructor's case and type", h and "(Circle int)" in h and "A case of `Shape`" in h, h)
|
||
|
|
h = hover(SYMBOLS, "(list-map")
|
||
|
|
expect(
|
||
|
|
"hover: the prelude's doc",
|
||
|
|
h and "Applies a function to each element of a list." in h and "**Parameters**" in h,
|
||
|
|
h,
|
||
|
|
)
|
||
|
|
h = hover(SYMBOLS, "(* 2 n)", 5)
|
||
|
|
expect("hover: a parameter says whose", h and "*A parameter of `double`.*" in h, h)
|
||
|
|
|
||
|
|
# Unsaved: the buffer gains a definition the file on disk does not have.
|
||
|
|
changed = SYMBOLS + "\n(defun (triple n) (* 3 n))\n(def nine (triple 3))\n"
|
||
|
|
server.send(
|
||
|
|
"textDocument/didChange",
|
||
|
|
{"textDocument": {"uri": uri, "version": 2}, "contentChanges": [{"text": changed}]},
|
||
|
|
)
|
||
|
|
d = definition(changed, "(triple 3)")
|
||
|
|
expect(
|
||
|
|
"the buffer, not the file",
|
||
|
|
d and d["range"]["start"]["line"] == line_of(changed, "(defun (triple"),
|
||
|
|
d,
|
||
|
|
)
|
||
|
|
found = server.send("textDocument/documentSymbol", {"textDocument": {"uri": uri}}, request=True)["result"]
|
||
|
|
expect("the outline follows the buffer", [n for n, _ in names(found)][-2:] == ["triple", "nine"], names(found))
|
||
|
|
|
||
|
|
# Half typed: the outline stays as it was rather than emptying.
|
||
|
|
server.send(
|
||
|
|
"textDocument/didChange",
|
||
|
|
{"textDocument": {"uri": uri, "version": 3}, "contentChanges": [{"text": changed + "(defun (half"}]},
|
||
|
|
)
|
||
|
|
found = server.send("textDocument/documentSymbol", {"textDocument": {"uri": uri}}, request=True)["result"]
|
||
|
|
expect(
|
||
|
|
"a half-typed form is read as if closed",
|
||
|
|
[n for n, _ in names(found)][-3:] == ["triple", "nine", "half"],
|
||
|
|
names(found),
|
||
|
|
)
|
||
|
|
|
||
|
|
# A stray closer cannot be mended by closing: the last outline stays.
|
||
|
|
server.send(
|
||
|
|
"textDocument/didChange",
|
||
|
|
{"textDocument": {"uri": uri, "version": 4}, "contentChanges": [{"text": changed + ")"}]},
|
||
|
|
)
|
||
|
|
found = server.send("textDocument/documentSymbol", {"textDocument": {"uri": uri}}, request=True)["result"]
|
||
|
|
expect(
|
||
|
|
"text that does not read keeps the last outline",
|
||
|
|
[n for n, _ in names(found)][-3:] == ["triple", "nine", "half"],
|
||
|
|
names(found),
|
||
|
|
)
|
||
|
|
|
||
|
|
server.diagnostics_after("textDocument/didClose", uri)
|
||
|
|
|
||
|
|
|
||
|
|
DOCUMENTED = """(import (std prelude))
|
||
|
|
|
||
|
|
(:doc #:file "documented.bjodoc")
|
||
|
|
|
||
|
|
(: squeeze (-> int int int int))
|
||
|
|
(defun (squeeze lo hi x) (max lo (min hi x)))
|
||
|
|
|
||
|
|
(: main (-> (Vec string) int))
|
||
|
|
(defun (main args) (squeeze 0 10 12))
|
||
|
|
"""
|
||
|
|
|
||
|
|
DOCUMENTED_DOC = '''(:doc squeeze
|
||
|
|
(summary "@code{x}, kept @strong{between} @em{lo} and hi.")
|
||
|
|
(arg lo "The least.")
|
||
|
|
(arg hi "The most.")
|
||
|
|
(arg x "The value.")
|
||
|
|
(returns "One of: @itemlist{@item{@code{lo}} @item{@code{hi}} @item{x}}")
|
||
|
|
(example """
|
||
|
|
(squeeze 0 10 12) ; 10
|
||
|
|
"""))
|
||
|
|
'''
|
||
|
|
|
||
|
|
|
||
|
|
def documented(server, tmp):
|
||
|
|
path = os.path.join(tmp, "documented.bjo")
|
||
|
|
uri = "file://" + path
|
||
|
|
with open(path, "w") as f:
|
||
|
|
f.write(DOCUMENTED)
|
||
|
|
with open(os.path.join(tmp, "documented.bjodoc"), "w") as f:
|
||
|
|
f.write(DOCUMENTED_DOC)
|
||
|
|
found = server.diagnostics_after("textDocument/didOpen", uri, DOCUMENTED)
|
||
|
|
expect("the documented file checks", found == [], found)
|
||
|
|
|
||
|
|
answer = server.send(
|
||
|
|
"textDocument/hover",
|
||
|
|
{"textDocument": {"uri": uri}, "position": position_of(DOCUMENTED, "(squeeze 0")},
|
||
|
|
request=True,
|
||
|
|
)
|
||
|
|
h = answer["result"]["contents"]["value"] if answer["result"] else None
|
||
|
|
expect(
|
||
|
|
"hover: a doc from a .bjodoc, its markup as Markdown",
|
||
|
|
h
|
||
|
|
and "`x`, kept **between** *lo* and hi." in h
|
||
|
|
and "- `lo`: The least." in h
|
||
|
|
and "**Returns** One of:\n- `lo`\n- `hi`\n- x" in h
|
||
|
|
and "(squeeze 0 10 12) ; 10" in h,
|
||
|
|
h,
|
||
|
|
)
|
||
|
|
server.diagnostics_after("textDocument/didClose", uri)
|
||
|
|
|
||
|
|
|
||
|
|
COMPLETING = """(import (std prelude))
|
||
|
|
|
||
|
|
(defun (list-unwind xs) xs)
|
||
|
|
|
||
|
|
(: main (-> (Vec string) int))
|
||
|
|
(defun (main args)
|
||
|
|
;; list-u
|
||
|
|
(list-u
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def completing(server, tmp):
|
||
|
|
path = os.path.join(tmp, "completing.bjo")
|
||
|
|
uri = "file://" + path
|
||
|
|
with open(path, "w") as f:
|
||
|
|
f.write(COMPLETING)
|
||
|
|
server.diagnostics_after("textDocument/didOpen", uri, COMPLETING)
|
||
|
|
|
||
|
|
def complete(needle, nth=0):
|
||
|
|
at = position_of(COMPLETING, needle, nth, len(needle))
|
||
|
|
answer = server.send(
|
||
|
|
"textDocument/completion", {"textDocument": {"uri": uri}, "position": at}, request=True
|
||
|
|
)
|
||
|
|
return at, answer["result"]["items"]
|
||
|
|
|
||
|
|
at, items = complete("(list-u", 0)
|
||
|
|
labels = {i["label"]: i for i in items}
|
||
|
|
expect(
|
||
|
|
"completion: the file's own name and the prelude's",
|
||
|
|
"list-unwind" in labels and "list-unfold" in labels,
|
||
|
|
sorted(labels)[:20],
|
||
|
|
)
|
||
|
|
expect(
|
||
|
|
"completion: only names starting with the word's first letter, in either case",
|
||
|
|
all(l[0] in "lL" for l in labels),
|
||
|
|
sorted(labels)[:20],
|
||
|
|
)
|
||
|
|
own = labels.get("list-unwind", {})
|
||
|
|
expect(
|
||
|
|
"completion: the item replaces the whole word",
|
||
|
|
own.get("textEdit", {}).get("range", {}).get("start") == {"line": at["line"], "character": at["character"] - 6},
|
||
|
|
own,
|
||
|
|
)
|
||
|
|
expect(
|
||
|
|
"completion: the file's own names sort first",
|
||
|
|
own.get("sortText", "").startswith("1") and labels["list-unfold"]["sortText"].startswith("2"),
|
||
|
|
(own.get("sortText"), labels.get("list-unfold", {}).get("sortText")),
|
||
|
|
)
|
||
|
|
|
||
|
|
resolved = server.send("completionItem/resolve", labels["list-unfold"], request=True)["result"]
|
||
|
|
doc = resolved.get("documentation", {}).get("value", "")
|
||
|
|
expect("completion: resolve adds the doc", "A list generated from a state." in doc, doc)
|
||
|
|
|
||
|
|
_, items = complete(";; list-u")
|
||
|
|
expect("completion: none in a comment", items == [], [i["label"] for i in items][:10])
|
||
|
|
|
||
|
|
# A builtin: no source to define it, but a type and a doc.
|
||
|
|
seq_text = COMPLETING.replace("(list-u\n", "(seq-unfold\n")
|
||
|
|
server.send(
|
||
|
|
"textDocument/didChange",
|
||
|
|
{"textDocument": {"uri": uri, "version": 2}, "contentChanges": [{"text": seq_text}]},
|
||
|
|
)
|
||
|
|
answer = server.send(
|
||
|
|
"textDocument/hover",
|
||
|
|
{"textDocument": {"uri": uri}, "position": position_of(seq_text, "(seq-unfold")},
|
||
|
|
request=True,
|
||
|
|
)
|
||
|
|
h = answer["result"]["contents"]["value"] if answer["result"] else None
|
||
|
|
expect(
|
||
|
|
"hover: a builtin's type and doc",
|
||
|
|
h and "(: seq-unfold (->" in h and "A sequence generated from a state, lazily." in h,
|
||
|
|
h,
|
||
|
|
)
|
||
|
|
server.diagnostics_after("textDocument/didClose", uri)
|
||
|
|
|
||
|
|
|
||
|
|
LOCALS = """(import (std prelude))
|
||
|
|
|
||
|
|
(: total (-> (List int) int int))
|
||
|
|
(defun (total xs (: bonus int))
|
||
|
|
(def base 10)
|
||
|
|
(let ((doubled (list-map (fun (x) (* 2 x)) xs))
|
||
|
|
(count 0))
|
||
|
|
(match doubled
|
||
|
|
((Cons first rest) (+ first bonus base count))
|
||
|
|
(Nil (loop (:for y xs)
|
||
|
|
(:acc sum (summing (+ y bonus)))
|
||
|
|
=> sum)))))
|
||
|
|
|
||
|
|
(: shadow (-> int int))
|
||
|
|
(defun (shadow v)
|
||
|
|
(let ((v (+ v 1)))
|
||
|
|
v))
|
||
|
|
|
||
|
|
(: typing (-> int int))
|
||
|
|
(defun (typing amount)
|
||
|
|
(let ((amplified (* amount 2)))
|
||
|
|
(+ am
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def locals_(server, tmp):
|
||
|
|
path = os.path.join(tmp, "locals.bjo")
|
||
|
|
uri = "file://" + path
|
||
|
|
with open(path, "w") as f:
|
||
|
|
f.write(LOCALS)
|
||
|
|
server.diagnostics_after("textDocument/didOpen", uri, LOCALS)
|
||
|
|
|
||
|
|
def ask(method, needle, offset=1, nth=0):
|
||
|
|
at = position_of(LOCALS, needle, nth, offset)
|
||
|
|
return server.send(method, {"textDocument": {"uri": uri}, "position": at}, request=True)["result"]
|
||
|
|
|
||
|
|
def goes_to(needle, target, target_offset, offset=1, nth=0):
|
||
|
|
d = ask("textDocument/definition", needle, offset, nth)
|
||
|
|
return d and d["range"]["start"] == position_of(LOCALS, target, 0, target_offset), d
|
||
|
|
|
||
|
|
cases = [
|
||
|
|
("a parameter", "bonus base", "(: bonus int)", 3),
|
||
|
|
("a def in the body", "base count", "(def base", 5),
|
||
|
|
("a let binding", "doubled\n", "(doubled (list-map", 1),
|
||
|
|
("a fun's parameter", "x)) xs", "(fun (x)", 6),
|
||
|
|
("a match pattern's variable", "first bonus", "(Cons first", 6),
|
||
|
|
("a loop variable, from its :acc", "y bonus", "(:for y", 6),
|
||
|
|
("an accumulator, from =>", "sum)))))", "(:acc sum", 6),
|
||
|
|
("the inner of two of a name", "v))\n", "((v (+ v 1)))", 2),
|
||
|
|
("the outer, in the inner's value", "v 1)", "(shadow v)", 8),
|
||
|
|
]
|
||
|
|
for name, needle, target, target_offset in cases:
|
||
|
|
ok, d = goes_to(needle, target, target_offset, 0)
|
||
|
|
expect("local definition: " + name, ok, d)
|
||
|
|
|
||
|
|
h = ask("textDocument/hover", "bonus base", 0)
|
||
|
|
h = h and h["contents"]["value"]
|
||
|
|
expect("local hover: a typed parameter shows its type", h and "(: bonus int)" in h and "A parameter of `total`" in h, h)
|
||
|
|
h = ask("textDocument/hover", "first bonus", 0)
|
||
|
|
h = h and h["contents"]["value"]
|
||
|
|
expect("local hover: a pattern shows the pattern", h and "(Cons first rest)" in h and "`match` pattern" in h, h)
|
||
|
|
|
||
|
|
# Completion while the form is being typed: the locals of `typing`, and
|
||
|
|
# nothing of `total`'s.
|
||
|
|
at = position_of(LOCALS, "(+ am", 0, 5)
|
||
|
|
items = server.send("textDocument/completion", {"textDocument": {"uri": uri}, "position": at}, request=True)["result"]["items"]
|
||
|
|
labels = {i["label"]: i for i in items}
|
||
|
|
expect(
|
||
|
|
"local completion: what is in scope, first",
|
||
|
|
labels.get("amount", {}).get("sortText", "").startswith("0")
|
||
|
|
and labels.get("amplified", {}).get("sortText", "").startswith("0"),
|
||
|
|
sorted((l, i["sortText"]) for l, i in labels.items())[:10],
|
||
|
|
)
|
||
|
|
expect("local completion: another function's locals are not", "doubled" not in labels and "bonus" not in labels, sorted(labels)[:10])
|
||
|
|
doc = labels.get("amplified", {}).get("documentation", {}).get("value", "")
|
||
|
|
expect("local completion: its doc comes along", "(amplified (* amount 2))" in doc, doc)
|
||
|
|
server.diagnostics_after("textDocument/didClose", uri)
|
||
|
|
|
||
|
|
|
||
|
|
CHAIN_MACROS = """;; Two macros, one expanding to the other.
|
||
|
|
(import (std syntax-match))
|
||
|
|
|
||
|
|
(def/macro (twice form inject compare)
|
||
|
|
(syntax-match form
|
||
|
|
((_ e) #'(both ,e ,e))))
|
||
|
|
|
||
|
|
(def/macro (both form inject compare)
|
||
|
|
(syntax-match form
|
||
|
|
((_ a b) #'(let ((tmp ,a)) (+ tmp ,b)))))
|
||
|
|
"""
|
||
|
|
|
||
|
|
CHAIN = """(import "chain-macros.bjo")
|
||
|
|
|
||
|
|
(: main (-> (Vec string) int))
|
||
|
|
(defun (main args)
|
||
|
|
(twice 21))
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def expanding(server, tmp):
|
||
|
|
with open(os.path.join(tmp, "chain-macros.bjo"), "w") as f:
|
||
|
|
f.write(CHAIN_MACROS)
|
||
|
|
path = os.path.join(tmp, "chain.bjo")
|
||
|
|
uri = "file://" + path
|
||
|
|
with open(path, "w") as f:
|
||
|
|
f.write(CHAIN)
|
||
|
|
found = server.diagnostics_after("textDocument/didOpen", uri, CHAIN)
|
||
|
|
expect("expansion: a file with an imported macro module checks", found == [], found)
|
||
|
|
expect(
|
||
|
|
"expansion: the import was built by the installed compiler",
|
||
|
|
os.path.exists(os.path.join(tmp, "chain-macros.dll")),
|
||
|
|
os.listdir(tmp),
|
||
|
|
)
|
||
|
|
|
||
|
|
def expand(needle, **flags):
|
||
|
|
at = position_of(CHAIN, needle, 0, 2)
|
||
|
|
answer = server.send(
|
||
|
|
"bjolang/expandMacro", dict(textDocument={"uri": uri}, position=at, **flags), request=True
|
||
|
|
)
|
||
|
|
return answer["result"]
|
||
|
|
|
||
|
|
r = expand("(twice", full=False)
|
||
|
|
expect("expansion: one step", r and r["macro"] == "twice" and r["text"] == "(both 21 21)", r)
|
||
|
|
r = expand("(twice")
|
||
|
|
expect("expansion: all the way", r and r["text"] == "(let ((tmp 21)) (+ tmp 21))", r)
|
||
|
|
r = expand("(twice", hygienic=True)
|
||
|
|
expect("expansion: hygienic names kept", r and "tmp__" in r["text"], r)
|
||
|
|
r = expand("(defun (main", full=False)
|
||
|
|
expect("expansion: none where there is no macro call", r is None, r)
|
||
|
|
|
||
|
|
# An unsaved change is expanded as it stands, without waiting for the pause.
|
||
|
|
changed = CHAIN.replace("(twice 21)", "(twice (twice 1))")
|
||
|
|
server.change(uri, changed, 2)
|
||
|
|
at = position_of(changed, "(twice (twice", 0, 2)
|
||
|
|
r = server.send("bjolang/expandMacro", {"textDocument": {"uri": uri}, "position": at}, request=True)["result"]
|
||
|
|
expect(
|
||
|
|
"expansion: of the buffer, nested calls expanded too",
|
||
|
|
r and r["text"].count("(+ tmp") == 3 and "twice" not in r["text"] and "both" not in r["text"],
|
||
|
|
r,
|
||
|
|
)
|
||
|
|
server.diagnostics_after("textDocument/didClose", uri)
|
||
|
|
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as tmp:
|
||
|
|
path = os.path.join(tmp, "smoke.bjo")
|
||
|
|
uri = "file://" + path
|
||
|
|
server = Server()
|
||
|
|
|
||
|
|
answer = server.send("initialize", {"processId": None, "rootUri": None, "capabilities": {}}, request=True)
|
||
|
|
expect("initialize answers capabilities", "capabilities" in answer.get("result", {}), answer)
|
||
|
|
server.send("initialized")
|
||
|
|
|
||
|
|
with open(path, "w") as f:
|
||
|
|
f.write(BROKEN)
|
||
|
|
found = server.diagnostics_after("textDocument/didOpen", uri, BROKEN)
|
||
|
|
expect("a type error is reported", len(found) >= 1 and found[0]["severity"] == 1, found)
|
||
|
|
expect(
|
||
|
|
"on the line it is on",
|
||
|
|
any(d["range"]["start"]["line"] == 4 for d in found),
|
||
|
|
json.dumps(found, indent=1),
|
||
|
|
)
|
||
|
|
for d in found:
|
||
|
|
print(" ", d["range"]["start"], d["message"].splitlines()[0])
|
||
|
|
|
||
|
|
# Saved: the editor sends the change, writes the file, and says so.
|
||
|
|
server.change(uri, FIXED, 2)
|
||
|
|
with open(path, "w") as f:
|
||
|
|
f.write(FIXED)
|
||
|
|
found = server.diagnostics_after("textDocument/didSave", uri)
|
||
|
|
expect("the fixed file has none", found == [], json.dumps(found, indent=1))
|
||
|
|
|
||
|
|
# Not saved: the check follows the buffer once typing pauses, while the
|
||
|
|
# file on disk stays fixed.
|
||
|
|
server.change(uri, UNCLOSED, 3)
|
||
|
|
found = server.next_diagnostics(uri)
|
||
|
|
expect("a parse error in an unsaved buffer is reported", len(found) >= 1, found)
|
||
|
|
for d in found:
|
||
|
|
print(" ", d["range"]["start"], d["message"].splitlines()[0])
|
||
|
|
|
||
|
|
# A burst of typing is checked once, after it, not once per change.
|
||
|
|
for version in range(4, 9):
|
||
|
|
server.change(uri, FIXED if version % 2 else BROKEN, version)
|
||
|
|
found = server.next_diagnostics(uri)
|
||
|
|
expect("a burst is checked as its last text", len(found) >= 1, found)
|
||
|
|
import time
|
||
|
|
time.sleep(1.0)
|
||
|
|
server.send("textDocument/documentSymbol", {"textDocument": {"uri": uri}}, request=True)
|
||
|
|
expect("and only once", server.published(uri) == [], "more than one check published")
|
||
|
|
|
||
|
|
found = server.diagnostics_after("textDocument/didClose", uri)
|
||
|
|
expect("closing clears them", found == [], found)
|
||
|
|
|
||
|
|
symbols_and_definitions(server, tmp)
|
||
|
|
documented(server, tmp)
|
||
|
|
completing(server, tmp)
|
||
|
|
locals_(server, tmp)
|
||
|
|
expanding(server, tmp)
|
||
|
|
|
||
|
|
answer = server.send("shutdown", request=True)
|
||
|
|
expect("shutdown answers", "result" in answer, answer)
|
||
|
|
server.send("exit")
|
||
|
|
expect("exit ends the process with 0", server.proc.wait(timeout=10) == 0)
|
||
|
|
|
||
|
|
sys.exit(1 if failures else 0)
|