Files
ViperC/App/lib/lsp/LspSource.vp
T
TermiNexus 1183f92344 fix(lsp): 修正 define/extern 声明的名称偏移
声明节点的位置可能落在修饰符或行首;按行内整词重新定位名称,并让无冒号的 extern 头解析出函数名。语义高亮对不匹配源文本的声明 token 直接跳过。
2026-10-26 18:51:01 +08:00

367 lines
12 KiB
Plaintext

from stdint import *
import string
import json
import os
import os.path
import memhub
import io
import lib.lsp.LspDocuments as LspDocuments
import lib.lsp.LspPositions as LspPositions
# Ranges are 0-based lines with UTF-16 character columns, matching the LSP wire format.
class LspRange:
sl: int
sc: int
el: int
ec: int
def __new__(self, pool: memhub.MemManager*, sl: int, sc: int,
el: int, ec: int) -> rawptr:
p: LspRange* = (LspRange*)(pool.alloc(LspRange.__sizeof__()))
if p:
string.memset(p, 0, LspRange.__sizeof__())
return rawptr(p)
def __init__(self, pool: memhub.MemManager*, sl: int, sc: int,
el: int, ec: int):
self.sl = sl
self.sc = sc
self.el = el
self.ec = ec
def NewRange(pool: memhub.MemManager*, sl: int, sc: int,
el: int, ec: int) -> LspRange*:
return LspRange(pool, sl, sc, el, ec)
def RangeToJson(pool: memhub.MemManager*, r: LspRange*) -> json.JsonValue*:
if r is None:
return None
rng: json.JsonValue* = json.object(pool)
start: json.JsonValue* = json.object(pool)
end: json.JsonValue* = json.object(pool)
if rng is None or start is None or end is None:
return None
json.object_set(pool, start, "line", json.int_val(pool, int64(r.sl)))
json.object_set(pool, start, "character", json.int_val(pool, int64(r.sc)))
json.object_set(pool, end, "line", json.int_val(pool, int64(r.el)))
json.object_set(pool, end, "character", json.int_val(pool, int64(r.ec)))
json.object_set(pool, rng, "start", start)
json.object_set(pool, rng, "end", end)
return rng
def LineByteLength(doc: LspDocuments.LspDocument*, line: int) -> int:
if doc is None or doc.index is None:
return 0
count: int = doc.index.Count()
if count <= 0:
return 0
if line < 0:
line = 0
if line >= count:
line = count - 1
return int(doc.index.LineEnd(line) - doc.index.LineStart(line))
def ByteColToUtf16(doc: LspDocuments.LspDocument*, line: int, col: int) -> int:
if doc is None or doc.index is None:
return 0
count: int = doc.index.Count()
if count <= 0:
return 0
if line < 0:
line = 0
if line >= count:
line = count - 1
return doc.index.ByteColToUtf16(line, col)
def Utf16ColToByte(doc: LspDocuments.LspDocument*, line: int, col: int) -> int:
if doc is None or doc.index is None:
return 0
if doc.index.Count() <= 0:
return 0
return doc.index.Utf16ColToByte(line, col)
def SpanFromBytes(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
sl: int, sc: int, el: int, ec: int) -> LspRange*:
"""Build a range from 0-based lines and UTF-8 byte columns."""
if doc is None or doc.index is None or doc.index.Count() <= 0:
return NewRange(pool, 0, 0, 0, 0)
count: int = doc.index.Count()
if sl < 0:
sl = 0
if sl >= count:
sl = count - 1
if el < sl:
el = sl
if el >= count:
el = count - 1
sc = ByteColToUtf16(doc, sl, sc)
ec = ByteColToUtf16(doc, el, ec)
if el == sl and ec <= sc:
ec = sc + 1
return NewRange(pool, sl, sc, el, ec)
def ByteSpan(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
line: int, byte_col: int, length: int) -> LspRange*:
"""Build a range for a token on one line, clamped to the line end."""
if doc is None or doc.index is None or doc.index.Count() <= 0:
return NewRange(pool, 0, 0, 0, 1)
count: int = doc.index.Count()
if line < 0:
line = 0
if line >= count:
line = count - 1
limit: int = LineByteLength(doc, line)
if byte_col < 0:
byte_col = 0
if byte_col > limit:
byte_col = limit
end_col: int = byte_col + length
if end_col > limit:
end_col = limit
if end_col <= byte_col:
end_col = byte_col + 1
return SpanFromBytes(pool, doc, line, byte_col, line, end_col)
def DocEndRange(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*) -> LspRange*:
if doc is None or doc.index is None or doc.index.Count() <= 0:
return NewRange(pool, 0, 0, 0, 0)
last: int = doc.index.Count() - 1
return SpanFromBytes(pool, doc, last, LineByteLength(doc, last), last, LineByteLength(doc, last))
def NewTempDoc(pool: memhub.MemManager*, text: str) -> LspDocuments.LspDocument*:
"""Wrap on-disk source text in a document with a line index (no pool ownership)."""
if pool is None or text is None:
return None
d: LspDocuments.LspDocument* = (LspDocuments.LspDocument*)(pool.alloc(LspDocuments.LspDocument.__sizeof__()))
if d is None:
return None
string.memset(d, 0, LspDocuments.LspDocument.__sizeof__())
d.text = text
d.version = 0
d.index = LspPositions.LspLineIndex(pool)
if d.index is None or d.index.Build(text) != 0:
return None
return d
def SliceBytes(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
sl: int, sc: int, el: int, ec: int) -> str:
"""Copy a source slice (0-based lines, UTF-8 byte columns) into pool."""
if doc is None or doc.index is None or doc.index.Count() <= 0:
return None
count: int = doc.index.Count()
if sl < 0:
sl = 0
if el < sl:
el = sl
if sl >= count:
return None
if el >= count:
el = count - 1
if sc < 0:
sc = 0
if ec < 0:
ec = 0
start: sizet = doc.index.LineStart(sl) + sizet(sc)
end: sizet = doc.index.LineStart(el) + sizet(ec)
total: sizet = doc.index.Length()
if start > total:
start = total
if end > total:
end = total
if end <= start:
return None
n: sizet = end - start
buf: bytes = pool.alloc(n + 1)
if buf is None:
return None
string.memcpy(buf, doc.text + start, n)
buf[n] = 0
return str(buf)
def _IsIdentStart(ch: char) -> int:
if ch >= 'a' and ch <= 'z': return 1
if ch >= 'A' and ch <= 'Z': return 1
if ch == '_': return 1
return 0
def _IsIdentChar(ch: char) -> int:
if _IsIdentStart(ch) != 0: return 1
if ch >= '0' and ch <= '9': return 1
return 0
def _SkipQuoted(doc: LspDocuments.LspDocument*, off: sizet, end: sizet) -> sizet:
"""Skip a quoted string starting at off; returns the offset after its closing quote."""
quote: char = doc.text[off]
triple: int = 0
if off + 2 < end and doc.text[off + 1] == quote and doc.text[off + 2] == quote:
triple = 1
off += 1
if triple != 0:
off += 2
while off < end:
ch: char = doc.text[off]
if ch == '\\':
off += 2
continue
if ch == quote:
if triple == 0:
return off + 1
if off + 2 < end and doc.text[off + 1] == quote and doc.text[off + 2] == quote:
return off + 3
if ch == '\n' and triple == 0:
return off
off += 1
return off
def FindHeaderColon(doc: LspDocuments.LspDocument*, line: int, byte_col: int,
out_line: int*, out_col: int*) -> int:
"""Find the ':' that terminates a def/class header; 1 when found.
The scan tracks bracket depth and skips strings and comments, so annotations and default
values containing ':' do not terminate the header early.
"""
if doc is None or doc.index is None or doc.index.Count() <= 0:
return 0
if line < 0:
line = 0
if line >= doc.index.Count():
line = doc.index.Count() - 1
end: sizet = doc.index.Length()
off: sizet = doc.index.LineStart(line) + sizet(byte_col)
if off >= end:
return 0
depth: int = 0
while off < end:
ch: char = doc.text[off]
if ch == '#':
while off < end and doc.text[off] != '\n':
off += 1
continue
if ch == '\'' or ch == '"':
off = _SkipQuoted(doc, off, end)
continue
if ch == '(' or ch == '[' or ch == '{':
depth += 1
off += 1
continue
if ch == ')' or ch == ']' or ch == '}':
if depth > 0:
depth -= 1
off += 1
continue
if ch == ':' and depth == 0:
l: int = 0
c: int = 0
doc.index.OffsetToPosition(off, &l, &c)
out_line[0] = l
out_col[0] = int(off - doc.index.LineStart(l))
return 1
if ch == '\n' and depth == 0:
return 0
off += 1
return 0
def FindDeclName(doc: LspDocuments.LspDocument*, line: int, byte_col: int,
name: str, want_class: int,
out_line: int*, out_col: int*) -> int:
"""Locate the declared identifier inside a def/class header; 1 when found.
Declaration-only forms (e.g. `extern lib name(...) -> T`) have no header colon, so the scan
falls back to the end of the declaration line.
"""
if doc is None or doc.index is None or doc.index.Count() <= 0 or name is None:
return 0
if line < 0:
return 0
el: int = 0
ec: int = 0
if FindHeaderColon(doc, line, byte_col, &el, &ec) == 0:
el = line
ec = LineByteLength(doc, line)
start: sizet = doc.index.LineStart(line) + sizet(byte_col)
end: sizet = doc.index.LineStart(el) + sizet(ec)
if end <= start:
return 0
off: sizet = start
seen_kw: int = 0
while off < end:
ch: char = doc.text[off]
if ch == '#':
while off < end and doc.text[off] != '\n':
off += 1
continue
if ch == '\'' or ch == '"':
off = _SkipQuoted(doc, off, end)
continue
if _IsIdentStart(ch) == 0:
if seen_kw != 0:
# The next non-space run after the keyword is the declared name; operator
# overloads have no identifier, so report the first visible character.
if ch != ' ' and ch != '\t' and ch != '\n' and ch != '\r':
l: int = 0
c: int = 0
doc.index.OffsetToPosition(off, &l, &c)
out_line[0] = l
out_col[0] = int(off - doc.index.LineStart(l))
return 1
off += 1
continue
tok_start: sizet = off
while off < end and _IsIdentChar(doc.text[off]) != 0:
off += 1
tok_len: sizet = off - tok_start
if seen_kw != 0:
l2: int = 0
c2: int = 0
doc.index.OffsetToPosition(tok_start, &l2, &c2)
out_line[0] = l2
out_col[0] = int(tok_start - doc.index.LineStart(l2))
return 1
if tok_len == 3:
if string.strncmp(str(doc.text + tok_start), "def", 3) == 0:
seen_kw = 1
if tok_len == 2 and want_class == 0:
if string.strncmp(str(doc.text + tok_start), "fn", 2) == 0:
seen_kw = 1
if tok_len == 5 and want_class != 0:
if string.strncmp(str(doc.text + tok_start), "class", 5) == 0:
seen_kw = 1
continue
# Fallback: find the identifier by name at a word boundary.
off = start
nlen: sizet = string.strlen(name)
while off + nlen <= end:
ch = doc.text[off]
if _IsIdentStart(ch) != 0 and string.strncmp(str(doc.text + off), name, nlen) == 0:
next_ok: int = 1
if off + nlen < end:
if _IsIdentChar(doc.text[off + nlen]) != 0:
next_ok = 0
if next_ok != 0:
l3: int = 0
c3: int = 0
doc.index.OffsetToPosition(off, &l3, &c3)
out_line[0] = l3
out_col[0] = int(off - doc.index.LineStart(l3))
return 1
off += 1
return 0