lsp: type-aware field completions — scan type/let/param decls, complete on dot-access
When a document is opened or changed, scan for `type Name { field: Type }` blocks
and `let var: Type` / `fn foo(param: Type)` annotations. On completion requests,
if the text before the cursor ends with `identifier.`, look up the variable's type
and return its fields as Field (kind=5) completion items instead of the full list.
This commit is contained in:
BIN
Binary file not shown.
Vendored
+1252
File diff suppressed because it is too large
Load Diff
+308
-2
@@ -299,6 +299,293 @@ fn lsp_builtin_find(name: String, catalog: [String]) -> String {
|
||||
""
|
||||
}
|
||||
|
||||
// ── Type registry & variable type map ─────────────────────────────────────
|
||||
|
||||
// Key helpers for type registry and vartype map in process state.
|
||||
fn lsp_types_key(uri: String, type_name: String) -> String {
|
||||
"types:" + uri + ":" + type_name
|
||||
}
|
||||
|
||||
fn lsp_vartypes_key(uri: String) -> String {
|
||||
"vartypes:" + uri
|
||||
}
|
||||
|
||||
// Scan source for `type Name { field: Type, ... }` declarations.
|
||||
// Stores each type's fields in state as "field1:Type1|field2:Type2|...".
|
||||
fn lsp_scan_types(uri: String, source: String) -> Void {
|
||||
let lines: [String] = str_split_lines(source)
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
let in_type: Bool = false
|
||||
let cur_type: String = ""
|
||||
let fields: String = ""
|
||||
while i < n {
|
||||
let line: String = str_trim(native_list_get(lines, i))
|
||||
if in_type {
|
||||
// Check for closing brace (end of type block)
|
||||
if str_starts_with(line, "}") {
|
||||
// Save accumulated fields for cur_type
|
||||
state_set(lsp_types_key(uri, cur_type), fields)
|
||||
let in_type = false
|
||||
let cur_type = ""
|
||||
let fields = ""
|
||||
} else {
|
||||
// Try to parse "fieldName: TypeName," or "fieldName: TypeName"
|
||||
let colon_pos: Int = str_index_of(line, ":")
|
||||
if colon_pos > 0 {
|
||||
let raw_name: String = str_trim(str_slice(line, 0, colon_pos))
|
||||
let after_colon: String = str_trim(str_slice(line, colon_pos + 1, str_len(line)))
|
||||
// Strip trailing comma
|
||||
let type_name: String = after_colon
|
||||
let tlen: Int = str_len(after_colon)
|
||||
if tlen > 0 {
|
||||
let last_ch: String = str_slice(after_colon, tlen - 1, tlen)
|
||||
if str_eq(last_ch, ",") {
|
||||
let type_name = str_trim(str_slice(after_colon, 0, tlen - 1))
|
||||
}
|
||||
}
|
||||
// Validate field name (non-empty, starts with letter or _)
|
||||
if str_len(raw_name) > 0 {
|
||||
let fc: Int = str_char_code(raw_name, 0)
|
||||
let valid_start: Bool = false
|
||||
if fc >= 97 { if fc <= 122 { let valid_start = true } }
|
||||
if fc >= 65 { if fc <= 90 { let valid_start = true } }
|
||||
if fc == 95 { let valid_start = true }
|
||||
if valid_start {
|
||||
if str_len(fields) > 0 {
|
||||
let fields = fields + "|"
|
||||
}
|
||||
let fields = fields + raw_name + ":" + type_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Look for "type Name {" pattern
|
||||
if str_starts_with(line, "type ") {
|
||||
let after: String = str_slice(line, 5, str_len(line))
|
||||
let brace_pos: Int = str_index_of(after, "{")
|
||||
if brace_pos > 0 {
|
||||
let tname: String = str_trim(str_slice(after, 0, brace_pos))
|
||||
if str_len(tname) > 0 {
|
||||
let in_type = true
|
||||
let cur_type = tname
|
||||
let fields = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Look up fields for a type name. Returns "field1:Type1|field2:Type2|..." or "".
|
||||
fn lsp_get_type_fields(uri: String, type_name: String) -> String {
|
||||
state_get(lsp_types_key(uri, type_name))
|
||||
}
|
||||
|
||||
// Parse a single vartype entry "varName:TypeName" — extract the var name.
|
||||
fn lsp_varentry_name(entry: String) -> String {
|
||||
let colon: Int = str_index_of(entry, ":")
|
||||
if colon <= 0 {
|
||||
return ""
|
||||
}
|
||||
str_slice(entry, 0, colon)
|
||||
}
|
||||
|
||||
// Parse a single vartype entry "varName:TypeName" — extract the type name.
|
||||
fn lsp_varentry_type(entry: String) -> String {
|
||||
let colon: Int = str_index_of(entry, ":")
|
||||
if colon < 0 {
|
||||
return ""
|
||||
}
|
||||
str_slice(entry, colon + 1, str_len(entry))
|
||||
}
|
||||
|
||||
// Scan source for variable type annotations:
|
||||
// let varName: TypeName = ...
|
||||
// fn foo(varName: TypeName, ...) -> RetType {
|
||||
// Stores in state as "var1:Type1|var2:Type2|...".
|
||||
fn lsp_scan_vartypes(uri: String, source: String) -> Void {
|
||||
let lines: [String] = str_split_lines(source)
|
||||
let n: Int = native_list_len(lines)
|
||||
let i: Int = 0
|
||||
let entries: String = ""
|
||||
while i < n {
|
||||
let line: String = str_trim(native_list_get(lines, i))
|
||||
// Handle "let varName: TypeName" declarations
|
||||
if str_starts_with(line, "let ") {
|
||||
let after_let: String = str_slice(line, 4, str_len(line))
|
||||
let colon_pos: Int = str_index_of(after_let, ":")
|
||||
if colon_pos > 0 {
|
||||
let var_name: String = str_trim(str_slice(after_let, 0, colon_pos))
|
||||
let after_colon: String = str_trim(str_slice(after_let, colon_pos + 1, str_len(after_let)))
|
||||
// Extract type — stop at '=' or whitespace
|
||||
let eq_pos: Int = str_index_of(after_colon, "=")
|
||||
let raw_type: String = after_colon
|
||||
if eq_pos > 0 {
|
||||
let raw_type = str_trim(str_slice(after_colon, 0, eq_pos))
|
||||
} else {
|
||||
let sp_pos: Int = str_index_of(after_colon, " ")
|
||||
if sp_pos > 0 {
|
||||
let raw_type = str_trim(str_slice(after_colon, 0, sp_pos))
|
||||
}
|
||||
}
|
||||
if str_len(var_name) > 0 {
|
||||
if str_len(raw_type) > 0 {
|
||||
if str_len(entries) > 0 {
|
||||
let entries = entries + "|"
|
||||
}
|
||||
let entries = entries + var_name + ":" + raw_type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle "fn name(param: Type, ...)" — extract each parameter's type annotation
|
||||
if str_starts_with(line, "fn ") {
|
||||
let paren_open: Int = str_index_of(line, "(")
|
||||
let paren_close: Int = str_last_index_of(line, ")")
|
||||
if paren_open > 0 {
|
||||
if paren_close > paren_open {
|
||||
let params_str: String = str_slice(line, paren_open + 1, paren_close)
|
||||
// Split by comma to get each param
|
||||
let params: [String] = str_split(params_str, ",")
|
||||
let pn: Int = native_list_len(params)
|
||||
let pi: Int = 0
|
||||
while pi < pn {
|
||||
let param: String = str_trim(native_list_get(params, pi))
|
||||
let pc: Int = str_index_of(param, ":")
|
||||
if pc > 0 {
|
||||
let pname: String = str_trim(str_slice(param, 0, pc))
|
||||
let ptype: String = str_trim(str_slice(param, pc + 1, str_len(param)))
|
||||
if str_len(pname) > 0 {
|
||||
if str_len(ptype) > 0 {
|
||||
if str_len(entries) > 0 {
|
||||
let entries = entries + "|"
|
||||
}
|
||||
let entries = entries + pname + ":" + ptype
|
||||
}
|
||||
}
|
||||
}
|
||||
let pi = pi + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
state_set(lsp_vartypes_key(uri), entries)
|
||||
}
|
||||
|
||||
// Look up a variable's type. Returns type name or "".
|
||||
fn lsp_get_var_type(uri: String, var_name: String) -> String {
|
||||
let entries: String = state_get(lsp_vartypes_key(uri))
|
||||
if str_eq(entries, "") {
|
||||
return ""
|
||||
}
|
||||
let parts: [String] = str_split(entries, "|")
|
||||
let n: Int = native_list_len(parts)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = native_list_get(parts, i)
|
||||
let ename: String = lsp_varentry_name(entry)
|
||||
if str_eq(ename, var_name) {
|
||||
return lsp_varentry_type(entry)
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
// Scan both types and vartypes from source — call on open/change.
|
||||
fn lsp_scan_document(uri: String, source: String) -> Void {
|
||||
lsp_scan_types(uri, source)
|
||||
lsp_scan_vartypes(uri, source)
|
||||
}
|
||||
|
||||
// Given a line of text up to the cursor, extract the identifier before a trailing '.'.
|
||||
// e.g. "let x = user." → "user"
|
||||
// "foo.bar." → "bar" (innermost only — sufficient for one-level field access)
|
||||
// Returns "" if the text doesn't end with "identifier.".
|
||||
fn lsp_dot_ident_before(text: String) -> String {
|
||||
let tlen: Int = str_len(text)
|
||||
if tlen < 2 {
|
||||
return ""
|
||||
}
|
||||
// Must end with '.'
|
||||
let last_ch: String = str_slice(text, tlen - 1, tlen)
|
||||
if !str_eq(last_ch, ".") {
|
||||
return ""
|
||||
}
|
||||
// Find the identifier immediately before the dot
|
||||
let dot_pos: Int = tlen - 1
|
||||
// Walk backwards from dot_pos-1 collecting word chars
|
||||
let end_idx: Int = dot_pos
|
||||
let start_idx: Int = dot_pos
|
||||
let going: Bool = true
|
||||
while going {
|
||||
if start_idx <= 0 {
|
||||
let going = false
|
||||
} else {
|
||||
let c: Int = str_char_code(text, start_idx - 1)
|
||||
let is_word: Bool = false
|
||||
if c >= 97 { if c <= 122 { let is_word = true } }
|
||||
if c >= 65 { if c <= 90 { let is_word = true } }
|
||||
if c >= 48 { if c <= 57 { let is_word = true } }
|
||||
if c == 95 { let is_word = true }
|
||||
if is_word {
|
||||
let start_idx = start_idx - 1
|
||||
} else {
|
||||
let going = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if start_idx >= end_idx {
|
||||
return ""
|
||||
}
|
||||
str_slice(text, start_idx, end_idx)
|
||||
}
|
||||
|
||||
// Extract text on the given line up to (not including) char_no.
|
||||
fn lsp_line_prefix(source: String, line_no: Int, char_no: Int) -> String {
|
||||
let lines: [String] = str_split_lines(source)
|
||||
if line_no >= native_list_len(lines) {
|
||||
return ""
|
||||
}
|
||||
let line: String = native_list_get(lines, line_no)
|
||||
let line_len: Int = str_len(line)
|
||||
let end: Int = char_no
|
||||
if end > line_len {
|
||||
let end = line_len
|
||||
}
|
||||
str_slice(line, 0, end)
|
||||
}
|
||||
|
||||
// Build field completion items for a given type. Returns JSON array string or "".
|
||||
// Returns "" (not "[]") if no type/fields found, so caller can fall back.
|
||||
fn lsp_field_completions(uri: String, type_name: String) -> String {
|
||||
let fields_str: String = lsp_get_type_fields(uri, type_name)
|
||||
if str_eq(fields_str, "") {
|
||||
return ""
|
||||
}
|
||||
let field_entries: [String] = str_split(fields_str, "|")
|
||||
let n: Int = native_list_len(field_entries)
|
||||
let items: [String] = native_list_empty()
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = native_list_get(field_entries, i)
|
||||
let colon: Int = str_index_of(entry, ":")
|
||||
if colon > 0 {
|
||||
let fname: String = str_slice(entry, 0, colon)
|
||||
let ftype: String = str_slice(entry, colon + 1, str_len(entry))
|
||||
// kind=5 is Field in LSP
|
||||
let items = native_list_append(items, lsp_completion_item(fname, 5, ftype))
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
"[" + str_join(items, ",") + "]"
|
||||
}
|
||||
|
||||
// ── Source scanning ────────────────────────────────────────────────────────
|
||||
|
||||
// Extract user-defined function names from source text.
|
||||
@@ -415,8 +702,22 @@ fn lsp_completion_item(label: String, kind: Int, detail: String) -> String {
|
||||
"{\"label\":\"" + lsp_json_escape(label) + "\",\"kind\":" + int_to_str(kind) + ",\"detail\":\"" + lsp_json_escape(detail) + "\"}"
|
||||
}
|
||||
|
||||
fn lsp_build_completions(uri: String, catalog: [String]) -> String {
|
||||
fn lsp_build_completions(uri: String, catalog: [String], line_no: Int, char_no: Int) -> String {
|
||||
let source: String = lsp_doc_get(uri)
|
||||
// Check for dot-access: if the text before the cursor ends with "identifier.",
|
||||
// return field completions for that variable's type.
|
||||
let prefix: String = lsp_line_prefix(source, line_no, char_no)
|
||||
let dot_ident: String = lsp_dot_ident_before(prefix)
|
||||
if !str_eq(dot_ident, "") {
|
||||
// Look up the variable's type
|
||||
let var_type: String = lsp_get_var_type(uri, dot_ident)
|
||||
if !str_eq(var_type, "") {
|
||||
let field_items: String = lsp_field_completions(uri, var_type)
|
||||
if !str_eq(field_items, "") {
|
||||
return field_items
|
||||
}
|
||||
}
|
||||
}
|
||||
let items: [String] = native_list_empty()
|
||||
// User-defined fns (kind=3 Function)
|
||||
let user_fns: [String] = lsp_extract_fns(source)
|
||||
@@ -708,7 +1009,10 @@ fn lsp_handle_initialize(id: String) -> Void {
|
||||
fn lsp_handle_completion(id: String, params: String, catalog: [String]) -> Void {
|
||||
let td: String = json_get_raw(params, "textDocument")
|
||||
let uri: String = json_get_string(td, "uri")
|
||||
let items: String = lsp_build_completions(uri, catalog)
|
||||
let pos: String = json_get_raw(params, "position")
|
||||
let line_no: Int = json_get_int(pos, "line")
|
||||
let char_no: Int = json_get_int(pos, "character")
|
||||
let items: String = lsp_build_completions(uri, catalog, line_no, char_no)
|
||||
let result: String = "{\"isIncomplete\":false,\"items\":" + items + "}"
|
||||
lsp_write_message(lsp_response(id, result))
|
||||
}
|
||||
@@ -738,6 +1042,7 @@ fn lsp_handle_did_open(params: String, catalog: [String]) -> Void {
|
||||
let uri: String = json_get_string(td, "uri")
|
||||
let text: String = json_get_string(td, "text")
|
||||
lsp_doc_store(uri, text)
|
||||
lsp_scan_document(uri, text)
|
||||
lsp_publish_diagnostics(uri)
|
||||
}
|
||||
|
||||
@@ -748,6 +1053,7 @@ fn lsp_handle_did_change(params: String) -> Void {
|
||||
let first: String = json_array_get(changes, 0)
|
||||
let text: String = json_get_string(first, "text")
|
||||
lsp_doc_store(uri, text)
|
||||
lsp_scan_document(uri, text)
|
||||
lsp_publish_diagnostics(uri)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user