diff --git a/App/lib/lsp/LspHover.vp b/App/lib/lsp/LspHover.vp new file mode 100644 index 0000000..526a8d4 --- /dev/null +++ b/App/lib/lsp/LspHover.vp @@ -0,0 +1,686 @@ +from stdint import * +import string +import json +import memhub +import ast +import math +import lib.lsp.LspDocuments as LspDocuments +import lib.lsp.LspSource as LspSource +import lib.lsp.LspSymbols as LspSymbols +import lib.lsp.LspIndex as LspIndex +import lib.lsp.LspWorkspace as LspWorkspace +import lib.lsp.LspTransport as LspTransport +import lib.lsp.LspRpc as LspRpc +import lib.lsp.LspNavigation as LspNavigation + + +# Longest literal text shown verbatim in a hover (a docstring can be huge). +define HOVER_LITERAL_TEXT_CAP = 160 +define HOVER_U64_MAX = 18446744073709551615 + + +# ============================================================================= +# Small text helpers +# ============================================================================= + +def _Cat(pool: memhub.MemManager*, a: str, b: str) -> str: + return LspNavigation._Cat(pool, a, b) + + +def _CodeBlock(pool: memhub.MemManager*, body: str) -> str: + """Wrap hover body text in a Viper fenced code block (alignment is preserved).""" + if body is None: + return None + return _Cat(pool, _Cat(pool, "```viper\n", body), "\n```") + + +def _LineStart(doc: LspDocuments.LspDocument*, line0: int) -> sizet: + return doc.index.LineStart(line0) + + +def _ByteAt(doc: LspDocuments.LspDocument*, line0: int, col: int) -> char: + if doc is None or doc.index is None: + return char(0) + if col < 0 or col >= LspSource.LineByteLength(doc, line0): + return char(0) + return doc.text[_LineStart(doc, line0) + sizet(col)] + + +def _IsDigit(ch: char) -> int: + return 1 if ch >= '0' and ch <= '9' else 0 + + +def _IsLetter(ch: char) -> int: + if ch >= 'a' and ch <= 'z': + return 1 + if ch >= 'A' and ch <= 'Z': + return 1 + return 0 + + +def _IsLitChar(ch: char) -> int: + if _IsDigit(ch) != 0 or _IsLetter(ch) != 0: + return 1 + if ch == '_' or ch == '.': + return 1 + return 0 + + +# ============================================================================= +# Literal lookup +# ============================================================================= + +def _InsideString(doc: LspDocuments.LspDocument*, line0: int, bcol: int) -> int: + """1 when the cursor sits inside a single-line quoted literal (content or quote).""" + limit: int = LspSource.LineByteLength(doc, line0) + i: int = 0 + quote: char = char(0) + escaped: int = 0 + while i < limit: + ch: char = _ByteAt(doc, line0, i) + if quote == char(0): + if ch == '#': + return 0 + if ch == '\'' or ch == '"': + if i == bcol: + return 1 + quote = ch + else: + if i == bcol: + return 1 + if escaped != 0: + escaped = 0 + elif ch == '\\': + escaped = 1 + elif ch == quote: + quote = char(0) + i += 1 + return 0 + + +def _LiteralCandidate(doc: LspDocuments.LspDocument*, line0: int, bcol: int) -> int: + """Cheap pre-check before tokenizing: can the cursor be inside a literal? + + Walking back over literal characters rejects plain identifiers (which are handled by the + symbol path) while still accepting every character of `0x1F`, `1e9`, `128_u8` and prefixed + strings such as `b"..."`. String content is handled separately because it may contain any + character. + """ + if doc is None or doc.index is None or line0 < 0 or line0 >= doc.index.Count(): + return 0 + limit: int = LspSource.LineByteLength(doc, line0) + if bcol < 0 or bcol >= limit: + return 0 + if _InsideString(doc, line0, bcol) != 0: + return 1 + start: int = bcol + while start > 0: + if _IsLitChar(_ByteAt(doc, line0, start - 1)) != 0: + start -= 1 + else: + break + ch0: char = _ByteAt(doc, line0, start) + if _IsDigit(ch0) != 0: + return 1 + if ch0 == '.': + return 1 if _IsDigit(_ByteAt(doc, line0, start + 1)) != 0 else 0 + if ch0 == '\'' or ch0 == '"': + return 1 + if ch0 == 'r' or ch0 == 'R' or ch0 == 'b' or ch0 == 'B' \ + or ch0 == 'f' or ch0 == 'F' or ch0 == 'u' or ch0 == 'U': + nx: char = _ByteAt(doc, line0, start + 1) + if nx == '\'' or nx == '"': + return 1 + return 0 + + +def _TokenContains(tok: ast.Token*, line0: int, bcol: int) -> int: + if tok is None: + return 0 + sl: int = tok.lineno - 1 + el: int = tok.end_lineno - 1 + if line0 < sl or line0 > el: + return 0 + if line0 == sl and bcol < tok.col_offset: + return 0 + if line0 == el and bcol >= tok.end_col_offset: + return 0 + return 1 + + +def _FindLiteral(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + line0: int, ch: int) -> ast.Token*: + """Return the number/string token under the cursor, or None.""" + if doc is None or doc.index is None: + return None + bcol: int = LspSource.Utf16ColToByte(doc, line0, ch) + if _LiteralCandidate(doc, line0, bcol) == 0: + return None + lx: ast.Lexer* = ast.new_lexer(pool) + if lx is None: + return None + ast._lexer_init(lx, doc.text, pool) + tok: ast.Token* = ast.tokenize(lx) + while tok is not None: + if tok.lineno - 1 > line0: + break + if _TokenContains(tok, line0, bcol) != 0: + if tok.type == ast.TokenType.Number or tok.type == ast.TokenType.String: + return tok + return None + tok = tok.next + return None + + +def _WordAt(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + line0: int, ch: int) -> str: + """Identifier-shaped text under the cursor (used for True/False/None literals).""" + if doc is None or doc.index is None or line0 < 0 or line0 >= doc.index.Count(): + return None + limit: int = LspSource.LineByteLength(doc, line0) + bcol: int = LspSource.Utf16ColToByte(doc, line0, ch) + if bcol < 0 or bcol >= limit: + return None + start: int = bcol + while start > 0: + if LspSource._IsIdentChar(_ByteAt(doc, line0, start - 1)) != 0: + start -= 1 + else: + break + end: int = bcol + while end < limit: + if LspSource._IsIdentChar(_ByteAt(doc, line0, end)) != 0: + end += 1 + else: + break + if end <= start: + return None + return LspSource.SliceBytes(pool, doc, line0, start, line0, end) + + +# ============================================================================= +# Integer literal analysis +# ============================================================================= + +def _DigitValue(ch: char) -> int: + if ch >= '0' and ch <= '9': + return int(ch) - 48 + if ch >= 'a' and ch <= 'f': + return int(ch) - 87 + if ch >= 'A' and ch <= 'F': + return int(ch) - 55 + return -1 + + +def _ParseMagnitude(text: str, overflow: int*) -> uint64: + """Parse an unsigned integer literal body (0x/0o/0b/decimal) before any width suffix.""" + overflow[0] = 0 + if text is None: + return uint64(0) + n: sizet = string.strlen(text) + if n == 0: + return uint64(0) + base: uint64 = uint64(10) + i: sizet = 0 + if n >= 2 and text[0] == '0': + c: char = text[1] + if c == 'x' or c == 'X': + base = uint64(16) + i = 2 + elif c == 'b' or c == 'B': + base = uint64(2) + i = 2 + elif c == 'o' or c == 'O': + base = uint64(8) + i = 2 + v: uint64 = uint64(0) + while i < n: + ch: char = text[i] + if ch == '_': + # `_` is either a digit separator or the start of an explicit width suffix. + if i + 1 >= n: + break + d2: int = _DigitValue(text[i + 1]) + if d2 < 0 or uint64(d2) >= base: + break + i += 1 + continue + d: int = _DigitValue(ch) + if d < 0 or uint64(d) >= base: + break + if v > (uint64(HOVER_U64_MAX) - uint64(d)) / base: + overflow[0] = 1 + return uint64(0) + v = v * base + uint64(d) + i += 1 + return v + + +def _U64Bits(v: uint64) -> int: + """Number of significant bits; zero still needs one bit.""" + if v == uint64(0): + return 1 + n: int = 0 + x: uint64 = v + while x != uint64(0): + n += 1 + x = x >> uint64(1) + return n + + +def _TrailingZeros(v: uint64) -> int: + if v == uint64(0): + return 64 + n: int = 0 + x: uint64 = v + while (x & uint64(1)) == uint64(0): + n += 1 + x = x >> uint64(1) + return n + + +def _PowTwoWidth(bits: int) -> int: + """Smallest power-of-two width >= bits from the language's integer sizes (8..128).""" + if bits <= 8: + return 8 + if bits <= 16: + return 16 + if bits <= 32: + return 32 + if bits <= 64: + return 64 + return 128 + + +def _HexU64Digits(pool: memhub.MemManager*, v: uint64, min_digits: int) -> str: + """Uppercase hex with at least min_digits digits, written nibble by nibble. + + snprintf's `%llX` is avoided: this runtime mis-prints wide values through the variadic path. + """ + digits: int = (_U64Bits(v) + 3) / 4 + if digits < min_digits: + digits = min_digits + buf: bytes = pool.alloc(sizet(digits + 3)) + if buf is None: + return None + buf[0] = '0' + buf[1] = 'x' + p: sizet = 2 + i: int = digits - 1 + while i >= 0: + nib: uint64 = (v >> uint64(i * 4)) & uint64(15) + if nib < uint64(10): + buf[p] = char(48 + int(nib)) + else: + buf[p] = char(55 + int(nib)) + p += 1 + i -= 1 + buf[p] = 0 + return str(buf) + + +def _HexU64(pool: memhub.MemManager*, v: uint64) -> str: + return _HexU64Digits(pool, v, 1) + + +def _HexU64Fixed(pool: memhub.MemManager*, v: uint64) -> str: + """16 hex digits: the canonical f64 bit-pattern spelling.""" + return _HexU64Digits(pool, v, 16) + + +def _BinU64(pool: memhub.MemManager*, v: uint64) -> str: + """Binary text grouped in nibbles from the least significant bit.""" + bits: int = _U64Bits(v) + seps: int = (bits - 1) / 4 + buf: bytes = pool.alloc(sizet(2 + bits + seps + 1)) + if buf is None: + return None + buf[0] = '0' + buf[1] = 'b' + p: sizet = 2 + i: int = bits - 1 + while i >= 0: + # Group from the least significant bit so every 4-bit group matches a hex nibble. + if i != bits - 1 and (i + 1) % 4 == 0: + buf[p] = '_' + p += 1 + if ((v >> uint64(i)) & uint64(1)) != uint64(0): + buf[p] = '1' + else: + buf[p] = '0' + p += 1 + i -= 1 + buf[p] = 0 + return str(buf) + + +def _IntFloatExact(v: uint64, pbits: int, maxv: uint64) -> int: + """Whether an integer is exactly representable in a binary float with pbits significand bits.""" + if v == uint64(0): + return 1 + if v > maxv: + return 0 + return 1 if (_U64Bits(v) - _TrailingZeros(v)) <= pbits else 0 + + +def _IntFloatLine(pool: memhub.MemManager*, v: uint64) -> str: + f16: str = "rounded" + if v > uint64(65520): + f16 = "overflow" + elif _IntFloatExact(v, 11, uint64(65504)) != 0: + f16 = "exact" + f32: str = "exact" if _IntFloatExact(v, 24, uint64(HOVER_U64_MAX)) != 0 else "rounded" + f64: str = "exact" if _IntFloatExact(v, 53, uint64(HOVER_U64_MAX)) != 0 else "rounded" + return _Cat(pool, _Cat(pool, _Cat(pool, _Cat(pool, _Cat(pool, + "f16 ", f16), ", f32 "), f32), ", f64 "), f64) + + +def _IntMarkdown(pool: memhub.MemManager*, tok: ast.Token*) -> str: + ovf: int = 0 + v: uint64 = _ParseMagnitude(tok.str_val, &ovf) + head: str = tok.str_val + if head is None: + head = "0" + body: str = head + if ovf != 0: + return _CodeBlock(pool, _Cat(pool, head, "\nvalue exceeds 64 bits (unsigned)")) + body = _Cat(pool, body, _Cat(pool, "\ndec ", _DecU64(pool, v))) + body = _Cat(pool, body, _Cat(pool, "\nhex ", _HexU64(pool, v))) + body = _Cat(pool, body, _Cat(pool, "\nbinary ", _BinU64(pool, v))) + ub: int = _U64Bits(v) + sb: int = ub + 1 + bits_line: str = _Cat(pool, _Cat(pool, _Cat(pool, _Cat(pool, + "\nbits ", _U64ToString(pool, uint64(ub))), + " unsigned, "), + _U64ToString(pool, uint64(sb))), + " signed") + body = _Cat(pool, body, bits_line) + uw: int = _PowTwoWidth(ub) + iw: int = _PowTwoWidth(sb) + body = _Cat(pool, body, _Cat(pool, "\nuint u", _U64ToString(pool, uint64(uw)))) + body = _Cat(pool, body, _Cat(pool, "\nint i", _U64ToString(pool, uint64(iw)))) + body = _Cat(pool, body, _Cat(pool, "\nfloat ", _IntFloatLine(pool, v))) + if tok.int_width > 0: + decl: str = "u" if tok.int_unsigned != 0 else "i" + decl = _Cat(pool, decl, _U64ToString(pool, uint64(tok.int_width))) + fits: int = 0 + if tok.int_unsigned != 0: + fits = 1 if ub <= tok.int_width else 0 + else: + fits = 1 if v <= (uint64(1) << uint64(tok.int_width - 1)) - uint64(1) else 0 + state: str = "fits" if fits != 0 else "overflow" + body = _Cat(pool, body, _Cat(pool, _Cat(pool, _Cat(pool, + "\ndeclared ", decl), " ("), _Cat(pool, state, ")"))) + return _CodeBlock(pool, body) + + +def _DecU64(pool: memhub.MemManager*, v: uint64) -> str: + """Decimal text written by hand so wide values never go through the variadic formatter.""" + if v == uint64(0): + return "0" + buf: bytes = pool.alloc(sizet(24)) + if buf is None: + return None + pos: int = 23 + buf[pos] = 0 + x: uint64 = v + while x != uint64(0): + pos -= 1 + buf[pos] = char(48 + int(x % uint64(10))) + x = x / uint64(10) + return str(buf + pos) + + +def _U64ToString(pool: memhub.MemManager*, v: uint64) -> str: + return _DecU64(pool, v) + + +# ============================================================================= +# Floating point literal analysis +# ============================================================================= + +def _FloatScale(pbits: int) -> double: + """2^pbits as a double (powers of two are exact).""" + s: double = 1.0 + i: int = 0 + while i < pbits: + s = s * 2.0 + i += 1 + return s + + +def _FloatState(v: double, width: int) -> int: + """1 exact, 2 rounded, 3 overflow, 4 non-finite (binary-float representability).""" + if math.isnan(v) != 0 or math.isinf(v) != 0: + return 4 + if width == 64: + return 1 + a: double = v + if a < 0.0: + a = -a + if a == 0.0: + return 1 + pbits: int = 10 if width == 16 else 23 + min_exp: int = -14 if width == 16 else -126 + max_exp: int = 15 if width == 16 else 127 + max_finite: double = 65504.0 if width == 16 else 3.4028234663852886e38 + overflow_at: double = 65520.0 if width == 16 else 3.4028235677973366e38 + if a > overflow_at: + return 3 + # Normalize to a = m * 2^e with m in [1, 2). Scaling by two is exact for doubles. + e: int = 0 + m: double = a + while m >= 2.0: + m = m / 2.0 + e += 1 + while m < 1.0: + m = m * 2.0 + e -= 1 + if a > max_finite: + return 2 + scale: double = _FloatScale(pbits) + t: double = m * scale + if t != math.floor(t): + return 2 + if e >= min_exp: + return 1 if e <= max_exp else 2 + # Subnormal: value must be an integral multiple of 2^(min_exp - pbits), below 2^pbits. + ksub: double = t + shift: int = min_exp - e + j: int = 0 + while j < shift: + ksub = ksub / 2.0 + j += 1 + if ksub != math.floor(ksub): + return 2 + return 1 if ksub < scale else 2 + + +def _FloatStateName(state: int) -> str: + if state == 1: + return "exact" + if state == 2: + return "rounded" + if state == 3: + return "overflow" + return "non-finite" + + +def _FloatFitLine(pool: memhub.MemManager*, v: double) -> str: + s16: str = _FloatStateName(_FloatState(v, 16)) + s32: str = _FloatStateName(_FloatState(v, 32)) + s64: str = _FloatStateName(_FloatState(v, 64)) + return _Cat(pool, _Cat(pool, _Cat(pool, _Cat(pool, _Cat(pool, + "f16 ", s16), ", f32 "), s32), ", f64 "), s64) + + +def _FloatMarkdown(pool: memhub.MemManager*, tok: ast.Token*) -> str: + v: double = tok.float_val + bits: uint64 = uint64(0) + *&bits = *&v + body: str = tok.str_val + if body is None: + body = "0.0" + body = _Cat(pool, body, _Cat(pool, "\nhex ", _HexU64Fixed(pool, bits))) + body = _Cat(pool, body, _Cat(pool, "\nbinary ", _BinU64(pool, bits))) + body = _Cat(pool, body, _Cat(pool, "\nfloat ", _FloatFitLine(pool, v))) + return _CodeBlock(pool, body) + + +# ============================================================================= +# String and keyword literals +# ============================================================================= + +def _StringMarkdown(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + tok: ast.Token*) -> str: + raw: str = LspSource.SliceBytes(pool, doc, tok.lineno - 1, tok.col_offset, + tok.end_lineno - 1, tok.end_col_offset) + shown: str = raw + if raw is not None and int(string.strlen(raw)) > HOVER_LITERAL_TEXT_CAP: + shown = _Cat(pool, LspSymbols._SliceStr(pool, raw, 0, HOVER_LITERAL_TEXT_CAP - 3), "...") + if shown is None: + shown = "\"\"" + kind: str = "str" + if raw is not None and string.strlen(raw) > 0: + c0: char = raw[0] + if c0 == 'b' or c0 == 'B': + kind = "bytes" + elif c0 == 'f' or c0 == 'F': + kind = "f-string" + elif c0 == 'r' or c0 == 'R': + kind = "raw string" + elif c0 == 'u' or c0 == 'U': + kind = "str" + n: sizet = string.strlen(tok.str_val) + body: str = _Cat(pool, shown, _Cat(pool, "\ntype ", kind)) + body = _Cat(pool, body, _Cat(pool, _Cat(pool, "\nlength ", + _U64ToString(pool, uint64(n))), " bytes")) + return _CodeBlock(pool, body) + + +def _KeywordMarkdown(pool: memhub.MemManager*, word: str) -> str: + if word is None: + return None + if string.strcmp(word, "True") == 0: + return _CodeBlock(pool, "True\nbool literal (true)") + if string.strcmp(word, "False") == 0: + return _CodeBlock(pool, "False\nbool literal (false)") + if string.strcmp(word, "None") == 0: + return _CodeBlock(pool, "None\nnull literal") + return None + + +# ============================================================================= +# Symbol hover +# ============================================================================= + +def _PrefixInside(root: str, file: str) -> int: + """1 when file is root plus a separator and a relative tail (case-insensitive).""" + if root is None or file is None: + return 0 + nr: sizet = string.strlen(root) + nf: sizet = string.strlen(file) + if nr == 0 or nf <= nr: + return 0 + i: sizet = 0 + while i < nr: + a: char = root[i] + b: char = file[i] + if a == '\\': + a = '/' + if b == '\\': + b = '/' + if a >= 'A' and a <= 'Z': + a = char(int(a) + 32) + if b >= 'A' and b <= 'Z': + b = char(int(b) + 32) + if a != b: + return 0 + i += 1 + tail: char = file[nr] + return 1 if tail == '/' or tail == '\\' else 0 + + +def _DisplayPath(pool: memhub.MemManager*, file: str) -> str: + """Workspace-relative path when the file is under a known root, else the full path.""" + if file is None: + return None + roots: list[str]* = LspIndex.Roots() + if roots is not None: + i: int = 0 + while i < int(len(roots)): + root: str = roots.get(sizet(i)) + i += 1 + if _PrefixInside(root, file) != 0: + return LspSymbols._SliceStr(pool, file, int(string.strlen(root)) + 1, + int(string.strlen(file))) + return file + + +def _TargetMarkdown(pool: memhub.MemManager*, t: LspIndex.LspTarget*) -> str: + if t is None: + return None + code: str = t.detail + if code is None: + if t.kind == LspSymbols.LSP_KIND_MODULE: + code = _Cat(pool, "module ", t.name) + elif t.kind == LspSymbols.LSP_KIND_CONSTANT: + code = _Cat(pool, "const ", t.name) + else: + code = t.name + if code is None: + return None + text: str = _CodeBlock(pool, code) + if t.kind == LspSymbols.LSP_KIND_MODULE and t.file is not None: + text = _Cat(pool, text, _Cat(pool, _Cat(pool, "\n\n`", + _DisplayPath(pool, t.file)), "`")) + if text is None: + return None + if t.doc is not None and t.doc[0] != 0: + text = _Cat(pool, text, _Cat(pool, "\n\n---\n\n", t.doc)) + return text + + +# ============================================================================= +# Entry point +# ============================================================================= + +def Hover(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, + docs: LspDocuments.LspDocuments*, ws: LspWorkspace.LspWorkspace*, + id_node: json.JsonValue*, params: json.JsonValue*) -> int: + doc: LspDocuments.LspDocument* = LspNavigation._Doc(pool, docs, params) + pos: json.JsonValue* = LspNavigation._Position(params) + if doc is None or pos is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + line0: int = LspRpc.GetIntField(pos, "line", 0) + ch: int = LspRpc.GetIntField(pos, "character", 0) + uri: str = LspNavigation._Uri(params) + a: LspSymbols.LspAnalysis* = LspNavigation._Analyze(pool, docs, doc, uri) + text: str = None + t: LspIndex.LspTarget* = LspNavigation._TargetAt( + ws, pool, docs.pool, a, doc, line0, ch) + if t is not None: + text = _TargetMarkdown(pool, t) + if text is None: + tok: ast.Token* = _FindLiteral(pool, doc, line0, ch) + if tok is not None: + if tok.type == ast.TokenType.String: + text = _StringMarkdown(pool, doc, tok) + elif tok.is_complex != 0: + text = None + elif tok.is_float != 0: + text = _FloatMarkdown(pool, tok) + else: + text = _IntMarkdown(pool, tok) + if text is None: + text = _KeywordMarkdown(pool, _WordAt(pool, doc, line0, ch)) + if text is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + result: json.JsonValue* = json.object(pool) + contents: json.JsonValue* = json.object(pool) + if result is None or contents is None: + return -1 + json.object_set(pool, contents, "kind", json.string_val(pool, "markdown")) + json.object_set(pool, contents, "value", json.string_val(pool, text)) + json.object_set(pool, result, "contents", contents) + return LspRpc.WriteResponse(pool, tr, id_node, result) diff --git a/App/lib/lsp/LspNavigation.vp b/App/lib/lsp/LspNavigation.vp index aeb392d..805f758 100644 --- a/App/lib/lsp/LspNavigation.vp +++ b/App/lib/lsp/LspNavigation.vp @@ -688,37 +688,3 @@ def DocumentHighlight(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, json.object_set(pool, h2, "kind", json.int_val(pool, int64(2))) json.array_append(pool, arr, h2) return LspRpc.WriteResponse(pool, tr, id_node, arr) - - -def Hover(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, - docs: LspDocuments.LspDocuments*, ws: LspWorkspace.LspWorkspace*, - id_node: json.JsonValue*, params: json.JsonValue*) -> int: - doc: LspDocuments.LspDocument* = _Doc(pool, docs, params) - pos: json.JsonValue* = _Position(params) - if doc is None or pos is None: - return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) - line0: int = LspRpc.GetIntField(pos, "line", 0) - ch: int = LspRpc.GetIntField(pos, "character", 0) - a: LspSymbols.LspAnalysis* = _Analyze(pool, docs, doc, _Uri(params)) - t: LspIndex.LspTarget* = _TargetAt(ws, pool, docs.pool, a, doc, line0, ch) - if t is None: - return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) - code: str = t.detail - if code is None: - if t.kind == LspSymbols.LSP_KIND_MODULE: - code = _Cat(pool, "module ", t.name) - else: - code = t.name - text: str = _Cat(pool, "```viper\n", code) - text = _Cat(pool, text, "\n```") - if t.doc is not None and t.doc[0] != 0: - text = _Cat(pool, text, "\n\n---\n\n") - text = _Cat(pool, text, t.doc) - result: json.JsonValue* = json.object(pool) - contents: json.JsonValue* = json.object(pool) - if result is None or contents is None: - return -1 - json.object_set(pool, contents, "kind", json.string_val(pool, "markdown")) - json.object_set(pool, contents, "value", json.string_val(pool, text)) - json.object_set(pool, result, "contents", contents) - return LspRpc.WriteResponse(pool, tr, id_node, result) diff --git a/App/lib/lsp/LspServer.vp b/App/lib/lsp/LspServer.vp index 7375aea..5137ae2 100644 --- a/App/lib/lsp/LspServer.vp +++ b/App/lib/lsp/LspServer.vp @@ -15,6 +15,7 @@ import lib.lsp.LspWorkspace as LspWorkspace import lib.lsp.LspDiagnostics as LspDiagnostics import lib.lsp.LspIndex as LspIndex import lib.lsp.LspNavigation as LspNavigation +import lib.lsp.LspHover as LspHover import lib.lsp.LspCompletion as LspCompletion import lib.lsp.LspSignature as LspSignature import lib.lsp.LspRename as LspRename @@ -501,7 +502,7 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int: elif string.strcmp(method, "textDocument/documentHighlight") == 0: rc = LspNavigation.DocumentHighlight(pool, tr, docs, ws, id_node, params) elif string.strcmp(method, "textDocument/hover") == 0: - rc = LspNavigation.Hover(pool, tr, docs, ws, id_node, params) + rc = LspHover.Hover(pool, tr, docs, ws, id_node, params) elif string.strcmp(method, "textDocument/completion") == 0: rc = LspCompletion.Completion(pool, tr, docs, ws, id_node, params) elif string.strcmp(method, "textDocument/signatureHelp") == 0: diff --git a/Tools/lsp/fixtures/gate10_use.vp b/Tools/lsp/fixtures/gate10_use.vp index 3ad0eee..1100cfa 100644 --- a/Tools/lsp/fixtures/gate10_use.vp +++ b/Tools/lsp/fixtures/gate10_use.vp @@ -58,6 +58,30 @@ def alias_use() -> int: return aliased_fn(EXTRA_CONST) +def numeric_use() -> int: + return 128 + + +def hex_use() -> int: + return 0x10 + + +def binary_use() -> int: + return 0b1010 + + +def float_use() -> float: + return 3.5 + + +def float_round_use() -> float: + return 3.14 + + +def string_use() -> int: + return len("hello") + + def else_block(flag: int) -> int: if flag != 0: return 1 diff --git a/Tools/lsp/probe.py b/Tools/lsp/probe.py index 4adb35c..6d2e1f5 100644 --- a/Tools/lsp/probe.py +++ b/Tools/lsp/probe.py @@ -1737,6 +1737,39 @@ def gate10(exe: str) -> int: if definition_of(at("StubThing.count", 10))[0] != "gate10_stub.vpi": raise AssertionError("definition on .vpi class field chain failed") + # Literal hovers carry the numeric spelling, storage widths and float representability. + module_hover = hover_text(at("import gate10_types", 7)) + if "module gate10_types" not in module_hover or "gate10_types.vp" not in module_hover: + raise AssertionError(f"module hover lacks name/path: {module_hover}") + int_hover = hover_text(at("return 128", 7)) + for piece in ("dec 128", "hex 0x80", "binary 0b1000_0000", + "bits 8 unsigned, 9 signed", "uint u8", "int i16", + "float f16 exact, f32 exact, f64 exact"): + if piece not in int_hover: + raise AssertionError(f"integer hover lacks {piece!r}: {int_hover}") + hex_hover = hover_text(at("return 0x10", 7)) + for piece in ("dec 16", "hex 0x10", "binary 0b1_0000"): + if piece not in hex_hover: + raise AssertionError(f"hex hover lacks {piece!r}: {hex_hover}") + bin_hover = hover_text(at("return 0b1010", 7)) + for piece in ("dec 10", "hex 0xA", "binary 0b1010"): + if piece not in bin_hover: + raise AssertionError(f"binary hover lacks {piece!r}: {bin_hover}") + float_hover = hover_text(at("return 3.5", 7)) + if "hex 0x400C000000000000" not in float_hover: + raise AssertionError(f"float hover has the wrong bit pattern: {float_hover}") + if "f16 exact, f32 exact, f64 exact" not in float_hover: + raise AssertionError(f"float hover misreports exactness: {float_hover}") + round_hover = hover_text(at("return 3.14", 7)) + if "hex 0x40091EB851EB851F" not in round_hover: + raise AssertionError(f"float hover has the wrong bit pattern: {round_hover}") + if "f16 rounded, f32 rounded, f64 exact" not in round_hover: + raise AssertionError(f"float hover misreports exactness: {round_hover}") + string_hover = hover_text(at('return len("hello")', len("return len("))) + for piece in ("type str", "length 5 bytes"): + if piece not in string_hover: + raise AssertionError(f"string hover lacks {piece!r}: {string_hover}") + def reference_ranges(pos: dict[str, int]) -> list[tuple[str, int, int]]: result = request("textDocument/references", pos, {"context": {"includeDeclaration": True}})