layout
This commit is contained in:
@@ -277,12 +277,15 @@ GLSL;
|
|||||||
if e.key == {
|
if e.key == {
|
||||||
case .escape: running = false;
|
case .escape: running = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
case .window_exposed: (e) {
|
||||||
|
|
||||||
}
|
}
|
||||||
case .key_down: (e) {
|
case .key_down: (e) {
|
||||||
k : u32 = xx e.key;
|
k : u32 = xx e.key;
|
||||||
print("ts={} wid={} sc={} key={}\n",
|
print("ts={} wid={} sc={} key={}\n",
|
||||||
e.timestamp, e.window_id, e.scancode, k);
|
e.timestamp, e.window_id, e.scancode, k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +293,7 @@ GLSL;
|
|||||||
ticks := SDL_GetTicks();
|
ticks := SDL_GetTicks();
|
||||||
ms : f32 = xx ticks;
|
ms : f32 = xx ticks;
|
||||||
angle := ms * 0.001;
|
angle := ms * 0.001;
|
||||||
|
|
||||||
// Build matrices
|
// Build matrices
|
||||||
proj : Matrix44 = ---;
|
proj : Matrix44 = ---;
|
||||||
mat4_perspective(proj, PI / 4.0, 800.0 / 600.0, 0.1, 100.0);
|
mat4_perspective(proj, PI / 4.0, 800.0 / 600.0, 0.1, 100.0);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ field_name :: ($T: Type, idx: s64) -> string #builtin;
|
|||||||
field_value :: (s: $T, idx: s64) -> Any #builtin;
|
field_value :: (s: $T, idx: s64) -> Any #builtin;
|
||||||
is_flags :: ($T: Type) -> bool #builtin;
|
is_flags :: ($T: Type) -> bool #builtin;
|
||||||
field_value_int :: ($T: Type, idx: s64) -> s64 #builtin;
|
field_value_int :: ($T: Type, idx: s64) -> s64 #builtin;
|
||||||
|
field_index :: ($T: Type, val: T) -> s64 #builtin;
|
||||||
string :: []u8 #builtin;
|
string :: []u8 #builtin;
|
||||||
|
|
||||||
int_to_string :: (n: s64) -> string {
|
int_to_string :: (n: s64) -> string {
|
||||||
@@ -221,9 +222,9 @@ flags_to_string :: (val: $T) -> string {
|
|||||||
|
|
||||||
enum_to_string :: (u: $T) -> string {
|
enum_to_string :: (u: $T) -> string {
|
||||||
if is_flags(T) { return flags_to_string(u); }
|
if is_flags(T) { return flags_to_string(u); }
|
||||||
tag := cast(s64) u;
|
idx := field_index(T, u);
|
||||||
result := concat(".", field_name(T, tag));
|
result := concat(".", field_name(T, idx));
|
||||||
payload := field_value(u, tag);
|
payload := field_value(u, idx);
|
||||||
pstr := any_to_string(payload);
|
pstr := any_to_string(payload);
|
||||||
if pstr.len > 0 {
|
if pstr.len > 0 {
|
||||||
result = concat(result, concat("(", concat(pstr, ")")));
|
result = concat(result, concat("(", concat(pstr, ")")));
|
||||||
|
|||||||
40
specs.md
40
specs.md
@@ -148,6 +148,18 @@ if s == {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Payload Capture
|
||||||
|
Match arms can capture the variant's payload into a local variable:
|
||||||
|
```sx
|
||||||
|
if s == {
|
||||||
|
case .circle: (radius) { print("radius: {}\n", radius); }
|
||||||
|
case .rect: (size) => print("size: {}\n", size);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The `(name)` after the colon binds the payload. Two forms:
|
||||||
|
- Block: `case .variant: (name) { body }`
|
||||||
|
- Short: `case .variant: (name) => expr;`
|
||||||
|
|
||||||
#### Enum Interpolation
|
#### Enum Interpolation
|
||||||
Payload-less enums print as `.variant`. Enums with payloads print as `.variant(value)` or `<TypeName tag=N>`:
|
Payload-less enums print as `.variant`. Enums with payloads print as `.variant(value)` or `<TypeName tag=N>`:
|
||||||
```sx
|
```sx
|
||||||
@@ -568,6 +580,33 @@ Syntax: `Name :: enum [flags] [type] { ... }`
|
|||||||
|
|
||||||
The backing type must be an integer type (`u8`, `u16`, `u32`, `s8`, `s16`, `s32`, `s64`, etc.). When omitted, the default is `s64`. This is useful for C interop (matching C enum sizes) and memory efficiency.
|
The backing type must be an integer type (`u8`, `u16`, `u32`, `s8`, `s16`, `s32`, `s64`, etc.). When omitted, the default is `s64`. This is useful for C interop (matching C enum sizes) and memory efficiency.
|
||||||
|
|
||||||
|
### Enum Layout Struct
|
||||||
|
|
||||||
|
For C interop with tagged unions (e.g. SDL_Event), a struct can be used as the backing type to specify the exact memory layout:
|
||||||
|
|
||||||
|
```sx
|
||||||
|
// Inline layout
|
||||||
|
SDL_Event :: enum struct { tag: u32; _: u32; payload: [30]u32; } {
|
||||||
|
quit :: 0x100;
|
||||||
|
key_down :: 0x300: SDL_KeyData;
|
||||||
|
key_up :: 0x301: SDL_KeyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named layout
|
||||||
|
EventLayout :: struct { tag: u32; _: u32; payload: [30]u32; }
|
||||||
|
SDL_Event :: enum EventLayout {
|
||||||
|
quit :: 0x100;
|
||||||
|
key_down :: 0x300: SDL_KeyData;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The layout struct must have:
|
||||||
|
- A field named `tag` — integer type, the discriminant. Its type becomes the enum's backing type.
|
||||||
|
- A field named `payload` — array type, the variant data area. Its size determines the maximum payload capacity.
|
||||||
|
- Any other fields are treated as padding/reserved and positioned by the struct layout.
|
||||||
|
|
||||||
|
This gives explicit control over the memory layout instead of relying on automatic alignment. The total size equals the struct size. Without a layout struct, tagged enums use `{ tag, [max_payload_size x i8] }` with no padding.
|
||||||
|
|
||||||
### Enum Flags
|
### Enum Flags
|
||||||
|
|
||||||
```sx
|
```sx
|
||||||
@@ -882,6 +921,7 @@ Built-in functions are declared in `std.sx` with the `#builtin` suffix, which te
|
|||||||
- `field_count($T: Type) -> s64` — returns the number of fields (struct), variants (enum), or elements (vector) in type `T`
|
- `field_count($T: Type) -> s64` — returns the number of fields (struct), variants (enum), or elements (vector) in type `T`
|
||||||
- `field_name($T: Type, idx: s64) -> string` — returns the name of the `idx`-th field (struct) or variant (enum) of type `T`
|
- `field_name($T: Type, idx: s64) -> string` — returns the name of the `idx`-th field (struct) or variant (enum) of type `T`
|
||||||
- `field_value(s: $T, idx: s64) -> Any` — returns the `idx`-th field (struct) or element (vector) of `s`, boxed as `Any`
|
- `field_value(s: $T, idx: s64) -> Any` — returns the `idx`-th field (struct) or element (vector) of `s`, boxed as `Any`
|
||||||
|
- `field_index($T: Type, val: T) -> s64` — returns the sequential variant index for an explicit enum value (reverse of `field_value_int`). Returns `-1` if no variant matches.
|
||||||
|
|
||||||
### Type Conversion
|
### Type Conversion
|
||||||
- `cast(Type) expr` — prefix operator that converts `expr` to `Type`. Examples: `cast(s32) 3.14`, `cast(f64) n`. When `Type` is a runtime `Type` value inside a type-category match arm, the compiler generates a dispatch switch over all types in the category, monomorphizing the callee for each concrete type.
|
- `cast(Type) expr` — prefix operator that converts `expr` to `Type`. Examples: `cast(s32) 3.14`, `cast(f64) n`. When `Type` is a runtime `Type` value inside a type-category match arm, the compiler generates a dispatch switch over all types in the category, monomorphizing the callee for each concrete type.
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub const Document = struct {
|
|||||||
root: ?*sx.ast.Node,
|
root: ?*sx.ast.Node,
|
||||||
/// Sema results for this file (references are relative to this source).
|
/// Sema results for this file (references are relative to this source).
|
||||||
sema: ?sx.sema.SemaResult,
|
sema: ?sx.sema.SemaResult,
|
||||||
|
/// Last successful sema (preserved across parse failures for completions).
|
||||||
|
last_good_sema: ?sx.sema.SemaResult = null,
|
||||||
/// Import declarations parsed from this file.
|
/// Import declarations parsed from this file.
|
||||||
imports: []const Import,
|
imports: []const Import,
|
||||||
|
|
||||||
@@ -199,6 +201,9 @@ pub const DocumentStore = struct {
|
|||||||
|
|
||||||
// Run sema on this file's own AST
|
// Run sema on this file's own AST
|
||||||
doc.sema = analyzer.analyze(root) catch null;
|
doc.sema = analyzer.analyze(root) catch null;
|
||||||
|
if (doc.sema != null) {
|
||||||
|
doc.last_good_sema = doc.sema;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: *const DocumentStore, path: []const u8) ?*Document {
|
pub fn get(self: *const DocumentStore, path: []const u8) ?*Document {
|
||||||
|
|||||||
@@ -462,36 +462,32 @@ pub const Server = struct {
|
|||||||
var items = std.ArrayList(lsp.CompletionItem).empty;
|
var items = std.ArrayList(lsp.CompletionItem).empty;
|
||||||
|
|
||||||
if (extractDotPrefix(doc.source, cursor_offset)) |prefix| {
|
if (extractDotPrefix(doc.source, cursor_offset)) |prefix| {
|
||||||
const sema = doc.sema orelse {
|
if (doc.sema) |sema| {
|
||||||
const items_json = try lsp.completionListJson(self.allocator, items.items);
|
// Check if prefix is a namespace — offer imported doc's declarations
|
||||||
return try self.sendResponse(id_json, items_json);
|
if (self.findImportByNs(doc, prefix)) |imp| {
|
||||||
};
|
if (self.documents.get(imp.path)) |imp_doc| {
|
||||||
|
if (imp_doc.root) |imp_root| {
|
||||||
|
if (imp_root.data == .root) {
|
||||||
|
try collectDeclCompletions(self.allocator, &items, imp_root.data.root.decls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (doc.root) |root| {
|
||||||
|
// Try as type name directly (e.g. Vec2., Color.)
|
||||||
|
try self.collectMemberCompletions(&items, sema, root, prefix);
|
||||||
|
|
||||||
// Check if prefix is a namespace — offer imported doc's declarations
|
// Try as variable name — resolve to type and offer fields + UFCS methods
|
||||||
if (self.findImportByNs(doc, prefix)) |imp| {
|
if (items.items.len == 0) {
|
||||||
if (self.documents.get(imp.path)) |imp_doc| {
|
if (resolveVariableType(sema, prefix)) |type_name| {
|
||||||
if (imp_doc.root) |imp_root| {
|
try self.collectMemberCompletions(&items, sema, root, type_name);
|
||||||
if (imp_root.data == .root) {
|
try self.collectUfcsCompletions(&items, root, type_name);
|
||||||
try collectDeclCompletions(self.allocator, &items, imp_root.data.root.decls);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
const root = doc.root orelse {
|
// Fallback: try resolving as a match arm capture variable (works without current parse)
|
||||||
const items_json = try lsp.completionListJson(self.allocator, items.items);
|
if (items.items.len == 0) {
|
||||||
return try self.sendResponse(id_json, items_json);
|
try self.collectCaptureCompletions(&items, doc, cursor_offset, prefix);
|
||||||
};
|
|
||||||
|
|
||||||
// Try as type name directly (e.g. Vec2., Color.)
|
|
||||||
try self.collectMemberCompletions(&items, sema, root, prefix);
|
|
||||||
|
|
||||||
// Try as variable name — resolve to type and offer fields + UFCS methods
|
|
||||||
if (items.items.len == 0) {
|
|
||||||
if (resolveVariableType(sema, prefix)) |type_name| {
|
|
||||||
try self.collectMemberCompletions(&items, sema, root, type_name);
|
|
||||||
try self.collectUfcsCompletions(&items, root, type_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Bare dot (no prefix) — check if we're inside a match expression
|
// Bare dot (no prefix) — check if we're inside a match expression
|
||||||
@@ -504,62 +500,235 @@ pub const Server = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collectMatchEnumCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32) !void {
|
fn collectMatchEnumCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32) !void {
|
||||||
const root = doc.root orelse return;
|
// Text-based approach: scan backward from cursor to find enclosing "SUBJECT == {"
|
||||||
const sema = doc.sema orelse return;
|
// This works even when the file has parse errors (user is mid-typing)
|
||||||
|
const subject_name = findMatchSubjectText(doc.source, cursor_offset) orelse return;
|
||||||
|
|
||||||
// Find enclosing match expression's subject
|
// Use sema (current or last successful) to resolve the subject's type
|
||||||
const subject = sx.sema.findEnclosingMatchSubject(root, cursor_offset) orelse return;
|
const sema = doc.sema orelse doc.last_good_sema orelse return;
|
||||||
|
|
||||||
// Resolve the subject to an enum type name
|
// Resolve subject name to an enum type
|
||||||
const enum_name: ?[]const u8 = switch (subject.data) {
|
const enum_name = self.resolveExprEnumType(sema, subject_name) orelse return;
|
||||||
.identifier => |id| blk: {
|
|
||||||
// Look up variable type, then check if it's an enum
|
// Use enum variant names directly from sema symbols (works even without doc.root)
|
||||||
for (sema.symbols) |sym| {
|
for (sema.symbols) |sym| {
|
||||||
if (!std.mem.eql(u8, sym.name, id.name)) continue;
|
if (!std.mem.eql(u8, sym.name, enum_name)) continue;
|
||||||
if (sym.kind != .variable and sym.kind != .param) continue;
|
if (sym.kind != .enum_type) continue;
|
||||||
const ty = sym.ty orelse break;
|
|
||||||
break :blk switch (ty) {
|
// Find the enum_decl node in the origin doc (or main doc)
|
||||||
.enum_type => |n| n,
|
const lookup_doc = if (sym.origin) |origin_path|
|
||||||
.union_type => |n| n,
|
self.documents.get(origin_path)
|
||||||
else => null,
|
else
|
||||||
};
|
null;
|
||||||
|
const lookup_root = if (lookup_doc) |ld| ld.root orelse doc.root else doc.root;
|
||||||
|
const root = lookup_root orelse continue;
|
||||||
|
|
||||||
|
if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| {
|
||||||
|
if (node.data == .enum_decl) {
|
||||||
|
for (node.data.enum_decl.variant_names) |variant| {
|
||||||
|
try items.append(self.allocator, .{
|
||||||
|
.label = variant,
|
||||||
|
.kind = @intFromEnum(lsp.CompletionItemKind.EnumMember),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break :blk null;
|
}
|
||||||
},
|
break;
|
||||||
.field_access => |fa| blk: {
|
}
|
||||||
// e.g. e.key — resolve the field's type
|
}
|
||||||
if (fa.object.data == .identifier) {
|
|
||||||
const var_name = fa.object.data.identifier.name;
|
/// Scan backward through source text to find the subject of an enclosing match expression.
|
||||||
// Find variable's struct type, then look up the field type
|
/// Looks for the pattern: SUBJECT == { ... case .
|
||||||
for (sema.symbols) |sym| {
|
/// Returns the subject text (e.g. "event", "e.key") or null.
|
||||||
if (!std.mem.eql(u8, sym.name, var_name)) continue;
|
fn findMatchSubjectText(source: []const u8, cursor_offset: u32) ?[]const u8 {
|
||||||
const ty = sym.ty orelse break;
|
var pos: u32 = cursor_offset;
|
||||||
const struct_name = switch (ty) {
|
var brace_depth: u32 = 0;
|
||||||
.struct_type => |n| n,
|
|
||||||
else => break,
|
// Walk backward, tracking brace depth, looking for "== {"
|
||||||
};
|
while (pos > 2) {
|
||||||
// Look up the struct's field type
|
pos -= 1;
|
||||||
if (sema.struct_types.get(struct_name)) |info| {
|
const ch = source[pos];
|
||||||
for (info.field_names, 0..) |fname, fi| {
|
if (ch == '}') {
|
||||||
if (std.mem.eql(u8, fname, fa.field) and fi < info.field_types.len) {
|
brace_depth += 1;
|
||||||
break :blk switch (info.field_types[fi]) {
|
} else if (ch == '{') {
|
||||||
.enum_type => |n| n,
|
if (brace_depth > 0) {
|
||||||
.union_type => |n| n,
|
brace_depth -= 1;
|
||||||
else => null,
|
} else {
|
||||||
};
|
// Found an unmatched '{' — check if preceded by "=="
|
||||||
|
var scan = pos;
|
||||||
|
while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1;
|
||||||
|
if (scan >= 2 and source[scan - 1] == '=' and source[scan - 2] == '=') {
|
||||||
|
// Found "== {" — extract the subject before "=="
|
||||||
|
var end = scan - 2;
|
||||||
|
while (end > 0 and std.ascii.isWhitespace(source[end - 1])) end -= 1;
|
||||||
|
if (end == 0) return null;
|
||||||
|
var start = end;
|
||||||
|
while (start > 0 and (isIdentChar(source[start - 1]) or source[start - 1] == '.')) {
|
||||||
|
start -= 1;
|
||||||
|
}
|
||||||
|
if (start < end) return source[start..end];
|
||||||
|
}
|
||||||
|
// Not a match expr — but keep scanning (might be a nested block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a capture variable's struct type by scanning backward for `case .VARIANT: (name)`
|
||||||
|
/// and looking up the variant's payload type in the enum declaration.
|
||||||
|
fn collectCaptureCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32, var_name: []const u8) !void {
|
||||||
|
const sema = doc.sema orelse doc.last_good_sema orelse return;
|
||||||
|
|
||||||
|
// Scan backward from cursor to find: case .VARIANT: (var_name)
|
||||||
|
const variant_name = findCaptureVariant(doc.source, cursor_offset, var_name) orelse return;
|
||||||
|
|
||||||
|
// Find the enclosing match subject
|
||||||
|
const subject_name = findMatchSubjectText(doc.source, cursor_offset) orelse return;
|
||||||
|
|
||||||
|
// Resolve subject to an enum type
|
||||||
|
const enum_name = self.resolveExprEnumType(sema, subject_name) orelse return;
|
||||||
|
|
||||||
|
// Find the enum_decl and look up the variant's payload type
|
||||||
|
for (sema.symbols) |sym| {
|
||||||
|
if (!std.mem.eql(u8, sym.name, enum_name)) continue;
|
||||||
|
if (sym.kind != .enum_type) continue;
|
||||||
|
|
||||||
|
const lookup_doc = if (sym.origin) |origin_path|
|
||||||
|
self.documents.get(origin_path)
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
const lookup_root = if (lookup_doc) |ld| ld.root orelse doc.root else doc.root;
|
||||||
|
const root = lookup_root orelse continue;
|
||||||
|
|
||||||
|
if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| {
|
||||||
|
if (node.data == .enum_decl) {
|
||||||
|
const ed = node.data.enum_decl;
|
||||||
|
// Find the matching variant and its payload type
|
||||||
|
for (ed.variant_names, 0..) |vn, vi| {
|
||||||
|
if (!std.mem.eql(u8, vn, variant_name)) continue;
|
||||||
|
if (vi >= ed.variant_types.len) break;
|
||||||
|
const vt = ed.variant_types[vi] orelse break;
|
||||||
|
// The payload type should be a struct — get its name
|
||||||
|
const payload_type_name = if (vt.data == .type_expr) vt.data.type_expr.name else break;
|
||||||
|
// Now offer that struct's fields
|
||||||
|
const payload_sema = if (lookup_doc) |ld| ld.sema orelse sema else sema;
|
||||||
|
try self.collectMemberCompletions(items, payload_sema, root, payload_type_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan backward from cursor to find `case .VARIANT: (var_name)` and return VARIANT.
|
||||||
|
fn findCaptureVariant(source: []const u8, cursor_offset: u32, var_name: []const u8) ?[]const u8 {
|
||||||
|
// Look backward for pattern: case .VARIANT: (var_name)
|
||||||
|
// We search for "(var_name)" first, then look for "case .VARIANT:" before it
|
||||||
|
var pos: u32 = cursor_offset;
|
||||||
|
while (pos > var_name.len + 10) { // need room for "case .X: (name)"
|
||||||
|
pos -= 1;
|
||||||
|
// Look for the closing ) of a capture
|
||||||
|
if (source[pos] != ')') continue;
|
||||||
|
// Check if the capture name matches
|
||||||
|
if (pos < var_name.len + 1) continue;
|
||||||
|
const name_end = pos;
|
||||||
|
const name_start = pos - @as(u32, @intCast(var_name.len));
|
||||||
|
if (!std.mem.eql(u8, source[name_start..name_end], var_name)) continue;
|
||||||
|
// Check for ( before the name
|
||||||
|
if (name_start < 1 or source[name_start - 1] != '(') continue;
|
||||||
|
|
||||||
|
// Now scan backward from '(' to find ": " then ".VARIANT" then "case "
|
||||||
|
var scan = name_start - 1;
|
||||||
|
// Skip whitespace
|
||||||
|
while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1;
|
||||||
|
// Expect ':'
|
||||||
|
if (scan < 1 or source[scan - 1] != ':') continue;
|
||||||
|
scan -= 1;
|
||||||
|
// Now extract the variant name: scan backward for ".VARIANT"
|
||||||
|
// Skip whitespace before ':'
|
||||||
|
// The variant is an enum literal like .key_down
|
||||||
|
// Actually the ':' comes right after the variant value (explicit) or variant name
|
||||||
|
// Pattern: case .VARIANT: (name) OR case .VARIANT :: 0x300: PayloadType;... nah
|
||||||
|
// In the match arm: case .key_down: (e) {
|
||||||
|
// So before ':' we have the enum literal .key_down
|
||||||
|
while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1;
|
||||||
|
// Extract identifier (variant name)
|
||||||
|
const vend = scan;
|
||||||
|
while (scan > 0 and (isIdentChar(source[scan - 1]))) scan -= 1;
|
||||||
|
if (scan >= vend) continue;
|
||||||
|
const variant = source[scan..vend];
|
||||||
|
// Check for '.' before variant name
|
||||||
|
if (scan < 1 or source[scan - 1] != '.') continue;
|
||||||
|
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a text expression like "event" or "e.key" to an enum/union type name.
|
||||||
|
fn resolveExprEnumType(self: *Server, sema: SemaResult, expr: []const u8) ?[]const u8 {
|
||||||
|
// Simple identifier: look up variable type
|
||||||
|
if (std.mem.indexOfScalar(u8, expr, '.') == null) {
|
||||||
|
for (sema.symbols) |sym| {
|
||||||
|
if (!std.mem.eql(u8, sym.name, expr)) continue;
|
||||||
|
if (sym.kind != .variable and sym.kind != .param) continue;
|
||||||
|
const ty = sym.ty orelse return null;
|
||||||
|
return switch (ty) {
|
||||||
|
.enum_type => |n| n,
|
||||||
|
.union_type => |n| n,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field access: "var.field" — resolve var's struct type, then look up field
|
||||||
|
if (std.mem.indexOfScalar(u8, expr, '.')) |dot| {
|
||||||
|
const var_name = expr[0..dot];
|
||||||
|
const field_name = expr[dot + 1 ..];
|
||||||
|
for (sema.symbols) |sym| {
|
||||||
|
if (!std.mem.eql(u8, sym.name, var_name)) continue;
|
||||||
|
const ty = sym.ty orelse return null;
|
||||||
|
const struct_name = switch (ty) {
|
||||||
|
.struct_type => |n| n,
|
||||||
|
else => return null,
|
||||||
|
};
|
||||||
|
if (sema.struct_types.get(struct_name)) |info| {
|
||||||
|
for (info.field_names, 0..) |fname, fi| {
|
||||||
|
if (std.mem.eql(u8, fname, field_name) and fi < info.field_types.len) {
|
||||||
|
return switch (info.field_types[fi]) {
|
||||||
|
.enum_type => |n| n,
|
||||||
|
.union_type => |n| n,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also check imported docs for struct info
|
||||||
|
if (sym.origin) |origin_path| {
|
||||||
|
if (self.documents.get(origin_path)) |od| {
|
||||||
|
if (od.sema) |imp_sema| {
|
||||||
|
if (imp_sema.struct_types.get(struct_name)) |info| {
|
||||||
|
for (info.field_names, 0..) |fname, fi| {
|
||||||
|
if (std.mem.eql(u8, fname, field_name) and fi < info.field_types.len) {
|
||||||
|
return switch (info.field_types[fi]) {
|
||||||
|
.enum_type => |n| n,
|
||||||
|
.union_type => |n| n,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break :blk null;
|
return null;
|
||||||
},
|
}
|
||||||
else => null,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const name = enum_name orelse return;
|
return null;
|
||||||
try self.collectMemberCompletions(items, sema, root, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collectDeclCompletions(allocator: std.mem.Allocator, items: *std.ArrayList(lsp.CompletionItem), decls: []const *sx.ast.Node) !void {
|
fn collectDeclCompletions(allocator: std.mem.Allocator, items: *std.ArrayList(lsp.CompletionItem), decls: []const *sx.ast.Node) !void {
|
||||||
@@ -1896,3 +2065,45 @@ pub const Server = struct {
|
|||||||
return buf.items;
|
return buf.items;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test "findMatchSubjectText: simple identifier" {
|
||||||
|
const source = "if event == {\n case .";
|
||||||
|
const result = Server.findMatchSubjectText(source, @intCast(source.len));
|
||||||
|
try std.testing.expectEqualStrings("event", result.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "findMatchSubjectText: field access" {
|
||||||
|
const source = "if e.key == {\n case .";
|
||||||
|
const result = Server.findMatchSubjectText(source, @intCast(source.len));
|
||||||
|
try std.testing.expectEqualStrings("e.key", result.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "findMatchSubjectText: nested braces" {
|
||||||
|
const source = "if event == {\n case .quit: { do_something(); }\n case .";
|
||||||
|
const result = Server.findMatchSubjectText(source, @intCast(source.len));
|
||||||
|
try std.testing.expectEqualStrings("event", result.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "findMatchSubjectText: no match context" {
|
||||||
|
const source = "while true {\n case .";
|
||||||
|
const result = Server.findMatchSubjectText(source, @intCast(source.len));
|
||||||
|
try std.testing.expect(result == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "findCaptureVariant: simple capture" {
|
||||||
|
const source = "case .key_down: (e) {\n e.";
|
||||||
|
const result = Server.findCaptureVariant(source, @intCast(source.len), "e");
|
||||||
|
try std.testing.expectEqualStrings("key_down", result.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "findCaptureVariant: different name" {
|
||||||
|
const source = "case .mouse_motion: (evt) {\n evt.";
|
||||||
|
const result = Server.findCaptureVariant(source, @intCast(source.len), "evt");
|
||||||
|
try std.testing.expectEqualStrings("mouse_motion", result.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "findCaptureVariant: no capture" {
|
||||||
|
const source = "case .quit: running = false;\n e.";
|
||||||
|
const result = Server.findCaptureVariant(source, @intCast(source.len), "e");
|
||||||
|
try std.testing.expect(result == null);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user