声明节点的位置可能落在修饰符或行首;按行内整词重新定位名称,并让无冒号的 extern 头解析出函数名。语义高亮对不匹配源文本的声明 token 直接跳过。
367 lines
12 KiB
Plaintext
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
|
|
|