Files
sx/src/lsp/types.zig
agra 28e02a8372 lsp: implement textDocument/references (find references)
cmd-clicking a definition (or any use) now lists all references. Same-file matches are precise (by symbol index); cross-file matches a top-level name across loaded documents. Advertises referencesProvider. Verified: references to a free function resolve across files (rules.sx def + internal calls + main.sx caller).
2026-05-31 11:51:06 +03:00

389 lines
14 KiB
Zig

const std = @import("std");
pub const Position = struct {
line: u32,
character: u32,
};
pub const Range = struct {
start: Position,
end: Position,
};
pub const Location = struct {
uri: []const u8,
range: Range,
};
pub const Diagnostic = struct {
range: Range,
severity: u32,
message: []const u8,
source: []const u8 = "sx",
};
/// Build a JSON-RPC response with a pre-built result JSON string.
pub fn jsonRpcResponse(allocator: std.mem.Allocator, id_json: []const u8, result_json: []const u8) ![]const u8 {
return std.fmt.allocPrint(allocator, "{{\"jsonrpc\":\"2.0\",\"id\":{s},\"result\":{s}}}", .{ id_json, result_json });
}
/// Build a JSON-RPC notification.
pub fn jsonRpcNotification(allocator: std.mem.Allocator, method: []const u8, params_json: []const u8) ![]const u8 {
return std.fmt.allocPrint(allocator, "{{\"jsonrpc\":\"2.0\",\"method\":\"{s}\",\"params\":{s}}}", .{ method, params_json });
}
/// Serialize a JSON Value to string.
pub fn valueToJson(allocator: std.mem.Allocator, value: std.json.Value) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try writeJsonValue(&buf, allocator, value);
return buf.items;
}
/// Escape a string for JSON.
pub fn jsonString(allocator: std.mem.Allocator, s: []const u8) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '"');
for (s) |ch| {
switch (ch) {
'"' => try buf.appendSlice(allocator, "\\\""),
'\\' => try buf.appendSlice(allocator, "\\\\"),
'\n' => try buf.appendSlice(allocator, "\\n"),
'\r' => try buf.appendSlice(allocator, "\\r"),
'\t' => try buf.appendSlice(allocator, "\\t"),
else => try buf.append(allocator, ch),
}
}
try buf.append(allocator, '"');
return buf.items;
}
fn writeJsonValue(buf: *std.ArrayList(u8), allocator: std.mem.Allocator, value: std.json.Value) !void {
switch (value) {
.null => try buf.appendSlice(allocator, "null"),
.bool => |b| try buf.appendSlice(allocator, if (b) "true" else "false"),
.integer => |i| {
const s = try std.fmt.allocPrint(allocator, "{d}", .{i});
try buf.appendSlice(allocator, s);
},
.float => |f| {
const s = try std.fmt.allocPrint(allocator, "{d}", .{f});
try buf.appendSlice(allocator, s);
},
.string => |s| {
const escaped = try jsonString(allocator, s);
try buf.appendSlice(allocator, escaped);
},
.array => |arr| {
try buf.append(allocator, '[');
for (arr.items, 0..) |item, idx| {
if (idx > 0) try buf.append(allocator, ',');
try writeJsonValue(buf, allocator, item);
}
try buf.append(allocator, ']');
},
.object => |obj| {
try buf.append(allocator, '{');
var first = true;
var it = obj.iterator();
while (it.next()) |entry| {
if (!first) try buf.append(allocator, ',');
first = false;
const key = try jsonString(allocator, entry.key_ptr.*);
try buf.appendSlice(allocator, key);
try buf.append(allocator, ':');
try writeJsonValue(buf, allocator, entry.value_ptr.*);
}
try buf.append(allocator, '}');
},
.number_string => |s| try buf.appendSlice(allocator, s),
}
}
/// Build the initialize result JSON.
pub fn initializeResultJson(allocator: std.mem.Allocator) ![]const u8 {
return std.fmt.allocPrint(allocator,
"{{\"capabilities\":{{\"textDocumentSync\":1,\"definitionProvider\":true,\"referencesProvider\":true,\"hoverProvider\":true,\"documentSymbolProvider\":true," ++
"\"completionProvider\":{{\"triggerCharacters\":[\".\"]}}," ++
"\"signatureHelpProvider\":{{\"triggerCharacters\":[\"(\",\",\"]}}," ++
"\"semanticTokensProvider\":{{\"legend\":{{" ++
"\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\",\"interface\"]," ++
"\"tokenModifiers\":[\"declaration\",\"readonly\"]" ++
"}},\"full\":true}}," ++
"\"inlayHintProvider\":true}}}}",
.{},
);
}
/// LSP SymbolKind enum values.
pub const SymbolKindLsp = enum(u32) {
File = 1,
Module = 2,
Namespace = 3,
Package = 4,
Class = 5,
Method = 6,
Property = 7,
Field = 8,
Constructor = 9,
Enum = 10,
Interface = 11,
Function = 12,
Variable = 13,
Constant = 14,
String = 15,
Number = 16,
Boolean = 17,
Array = 18,
Object = 19,
Key = 20,
Null = 21,
EnumMember = 22,
Struct = 23,
Event = 24,
Operator = 25,
TypeParameter = 26,
};
/// LSP CompletionItemKind enum values.
pub const CompletionItemKind = enum(u32) {
Text = 1,
Method = 2,
Function = 3,
Constructor = 4,
Field = 5,
Variable = 6,
Class = 7,
Interface = 8,
Module = 9,
Property = 10,
Unit = 11,
Value = 12,
Enum = 13,
Keyword = 14,
Snippet = 15,
Color = 16,
File = 17,
Reference = 18,
Folder = 19,
EnumMember = 20,
Constant = 21,
Struct = 22,
Event = 23,
Operator = 24,
TypeParameter = 25,
};
/// Build document symbols JSON array.
pub fn documentSymbolsJson(allocator: std.mem.Allocator, symbols: []const DocumentSymbol) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '[');
for (symbols, 0..) |sym, idx| {
if (idx > 0) try buf.append(allocator, ',');
const name_escaped = try jsonString(allocator, sym.name);
const item = try std.fmt.allocPrint(allocator,
"{{\"name\":{s},\"kind\":{d},\"range\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}},\"selectionRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}}}",
.{
name_escaped, sym.kind,
sym.range.start.line, sym.range.start.character,
sym.range.end.line, sym.range.end.character,
sym.selection_range.start.line, sym.selection_range.start.character,
sym.selection_range.end.line, sym.selection_range.end.character,
},
);
try buf.appendSlice(allocator, item);
}
try buf.append(allocator, ']');
return buf.items;
}
pub const DocumentSymbol = struct {
name: []const u8,
kind: u32,
range: Range,
selection_range: Range,
};
/// Build completion items JSON array.
pub fn completionItemsJson(allocator: std.mem.Allocator, items: []const CompletionItem) ![]const u8 {
return completionItemsJsonInner(allocator, items, false);
}
/// Build a CompletionList object with isIncomplete: false, preventing the client
/// from supplementing results with its own word-based suggestions.
pub fn completionListJson(allocator: std.mem.Allocator, items: []const CompletionItem) ![]const u8 {
return completionItemsJsonInner(allocator, items, true);
}
fn completionItemsJsonInner(allocator: std.mem.Allocator, items: []const CompletionItem, as_list: bool) ![]const u8 {
var buf = std.ArrayList(u8).empty;
if (as_list) try buf.appendSlice(allocator, "{\"isIncomplete\":false,\"items\":");
try buf.append(allocator, '[');
for (items, 0..) |item, idx| {
if (idx > 0) try buf.append(allocator, ',');
const label_escaped = try jsonString(allocator, item.label);
const detail_escaped = if (item.detail) |d| try jsonString(allocator, d) else null;
if (detail_escaped) |de| {
const json = try std.fmt.allocPrint(allocator,
"{{\"label\":{s},\"kind\":{d},\"detail\":{s}}}",
.{ label_escaped, item.kind, de },
);
try buf.appendSlice(allocator, json);
} else {
const json = try std.fmt.allocPrint(allocator,
"{{\"label\":{s},\"kind\":{d}}}",
.{ label_escaped, item.kind },
);
try buf.appendSlice(allocator, json);
}
}
try buf.append(allocator, ']');
if (as_list) try buf.append(allocator, '}');
return buf.items;
}
pub const CompletionItem = struct {
label: []const u8,
kind: u32,
detail: ?[]const u8 = null,
};
/// Build a Location JSON response (for go-to-definition).
pub fn locationJson(allocator: std.mem.Allocator, uri: []const u8, range: Range) ![]const u8 {
const uri_escaped = try jsonString(allocator, uri);
return std.fmt.allocPrint(allocator,
"{{\"uri\":{s},\"range\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}}}",
.{ uri_escaped, range.start.line, range.start.character, range.end.line, range.end.character },
);
}
/// Build a LocationLink JSON response (for go-to-definition with origin range).
pub fn locationLinkJson(allocator: std.mem.Allocator, target_uri: []const u8, target_range: Range, origin_range: Range) ![]const u8 {
const uri_escaped = try jsonString(allocator, target_uri);
return std.fmt.allocPrint(allocator,
"[{{\"originSelectionRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}," ++
"\"targetUri\":{s}," ++
"\"targetRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}," ++
"\"targetSelectionRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}}}]",
.{
origin_range.start.line, origin_range.start.character, origin_range.end.line, origin_range.end.character,
uri_escaped,
target_range.start.line, target_range.start.character, target_range.end.line, target_range.end.character,
target_range.start.line, target_range.start.character, target_range.end.line, target_range.end.character,
},
);
}
/// Build a Hover JSON response.
pub fn hoverJson(allocator: std.mem.Allocator, contents: []const u8) ![]const u8 {
const escaped = try jsonString(allocator, contents);
return std.fmt.allocPrint(allocator,
"{{\"contents\":{{\"kind\":\"markdown\",\"value\":{s}}}}}",
.{escaped},
);
}
/// Build a SignatureHelp JSON response.
pub fn signatureHelpJson(allocator: std.mem.Allocator, label: []const u8, param_labels: []const []const u8, active_param: u32) ![]const u8 {
var buf = std.ArrayList(u8).empty;
const label_escaped = try jsonString(allocator, label);
try buf.appendSlice(allocator, "{\"signatures\":[{\"label\":");
try buf.appendSlice(allocator, label_escaped);
try buf.appendSlice(allocator, ",\"parameters\":[");
for (param_labels, 0..) |pl, idx| {
if (idx > 0) try buf.append(allocator, ',');
const pl_escaped = try jsonString(allocator, pl);
try buf.appendSlice(allocator, "{\"label\":");
try buf.appendSlice(allocator, pl_escaped);
try buf.append(allocator, '}');
}
const ap_str = try std.fmt.allocPrint(allocator, "{d}", .{active_param});
try buf.appendSlice(allocator, "]}],\"activeSignature\":0,\"activeParameter\":");
try buf.appendSlice(allocator, ap_str);
try buf.append(allocator, '}');
return buf.items;
}
/// Semantic token type indices (must match legend in initializeResultJson).
pub const SemanticTokenType = struct {
pub const namespace: u32 = 0;
pub const type_: u32 = 1;
pub const enum_: u32 = 2;
pub const struct_: u32 = 3;
pub const parameter: u32 = 4;
pub const variable: u32 = 5;
pub const enum_member: u32 = 6;
pub const function: u32 = 7;
pub const keyword: u32 = 8;
pub const number: u32 = 9;
pub const string_: u32 = 10;
pub const operator_: u32 = 11;
pub const interface: u32 = 12;
};
/// Build a SemanticTokens JSON response.
pub fn semanticTokensJson(allocator: std.mem.Allocator, data: []const u32) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.appendSlice(allocator, "{\"data\":[");
for (data, 0..) |val, idx| {
if (idx > 0) try buf.append(allocator, ',');
const s = try std.fmt.allocPrint(allocator, "{d}", .{val});
try buf.appendSlice(allocator, s);
}
try buf.appendSlice(allocator, "]}");
return buf.items;
}
/// Build publishDiagnostics params JSON.
pub fn publishDiagnosticsJson(allocator: std.mem.Allocator, uri: []const u8, diagnostics: []const Diagnostic) ![]const u8 {
var buf = std.ArrayList(u8).empty;
const uri_escaped = try jsonString(allocator, uri);
try buf.appendSlice(allocator, "{\"uri\":");
try buf.appendSlice(allocator, uri_escaped);
try buf.appendSlice(allocator, ",\"diagnostics\":[");
for (diagnostics, 0..) |d, idx| {
if (idx > 0) try buf.append(allocator, ',');
const msg_escaped = try jsonString(allocator, d.message);
const src_escaped = try jsonString(allocator, d.source);
const diag_json = try std.fmt.allocPrint(allocator,
"{{\"range\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}},\"severity\":{d},\"message\":{s},\"source\":{s}}}",
.{ d.range.start.line, d.range.start.character, d.range.end.line, d.range.end.character, d.severity, msg_escaped, src_escaped },
);
try buf.appendSlice(allocator, diag_json);
}
try buf.appendSlice(allocator, "]}");
return buf.items;
}
pub const InlayHint = struct {
line: u32,
character: u32,
label: []const u8,
kind: u32 = 1, // 1 = Type
padding_left: bool = true,
padding_right: bool = false,
};
/// Build inlay hints JSON array response.
pub fn inlayHintsJson(allocator: std.mem.Allocator, hints: []const InlayHint) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '[');
for (hints, 0..) |hint, idx| {
if (idx > 0) try buf.append(allocator, ',');
const label_escaped = try jsonString(allocator, hint.label);
const json = try std.fmt.allocPrint(allocator,
"{{\"position\":{{\"line\":{d},\"character\":{d}}},\"label\":{s},\"kind\":{d},\"paddingLeft\":{s},\"paddingRight\":{s}}}",
.{ hint.line, hint.character, label_escaped, hint.kind, if (hint.padding_left) "true" else "false", if (hint.padding_right) "true" else "false" },
);
try buf.appendSlice(allocator, json);
}
try buf.append(allocator, ']');
return buf.items;
}