so... jai :D
This commit is contained in:
48
src/lsp/document.zig
Normal file
48
src/lsp/document.zig
Normal file
@@ -0,0 +1,48 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const DocumentStore = struct {
|
||||
documents: std.StringHashMap(Document),
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub const Document = struct {
|
||||
uri: []const u8,
|
||||
text: []const u8,
|
||||
version: i64,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) DocumentStore {
|
||||
return .{
|
||||
.documents = std.StringHashMap(Document).init(allocator),
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn open(self: *DocumentStore, uri: []const u8, text: []const u8, version: i64) !void {
|
||||
const uri_copy = try self.allocator.dupe(u8, uri);
|
||||
const text_copy = try self.allocator.dupe(u8, text);
|
||||
try self.documents.put(uri_copy, .{
|
||||
.uri = uri_copy,
|
||||
.text = text_copy,
|
||||
.version = version,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update(self: *DocumentStore, uri: []const u8, text: []const u8, version: i64) !void {
|
||||
if (self.documents.getPtr(uri)) |doc| {
|
||||
self.allocator.free(doc.text);
|
||||
doc.text = try self.allocator.dupe(u8, text);
|
||||
doc.version = version;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(self: *DocumentStore, uri: []const u8) void {
|
||||
if (self.documents.fetchRemove(uri)) |kv| {
|
||||
self.allocator.free(kv.value.text);
|
||||
self.allocator.free(kv.key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *const DocumentStore, uri: []const u8) ?*const Document {
|
||||
return self.documents.getPtr(uri);
|
||||
}
|
||||
};
|
||||
1776
src/lsp/server.zig
Normal file
1776
src/lsp/server.zig
Normal file
File diff suppressed because it is too large
Load Diff
75
src/lsp/transport.zig
Normal file
75
src/lsp/transport.zig
Normal file
@@ -0,0 +1,75 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Transport = struct {
|
||||
in: *std.Io.Reader,
|
||||
out_file: std.Io.File,
|
||||
io: std.Io,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, io: std.Io, in: *std.Io.Reader, out_file: std.Io.File) Transport {
|
||||
return .{
|
||||
.in = in,
|
||||
.out_file = out_file,
|
||||
.io = io,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
/// Read one LSP message: parse Content-Length header, read body.
|
||||
pub fn readMessage(self: *Transport) ![]const u8 {
|
||||
var content_length: ?usize = null;
|
||||
|
||||
// Parse headers (terminated by \r\n\r\n)
|
||||
while (true) {
|
||||
const line = try self.readLine();
|
||||
if (line.len == 0) break; // empty line = end of headers
|
||||
|
||||
if (std.mem.startsWith(u8, line, "Content-Length: ")) {
|
||||
content_length = std.fmt.parseInt(usize, line["Content-Length: ".len..], 10) catch
|
||||
return error.InvalidContentLength;
|
||||
}
|
||||
}
|
||||
|
||||
const len = content_length orelse return error.MissingContentLength;
|
||||
|
||||
const body = try self.allocator.alloc(u8, len);
|
||||
try self.in.readSliceAll(body);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/// Write one LSP message: Content-Length header + body.
|
||||
pub fn writeMessage(self: *Transport, body: []const u8) !void {
|
||||
var buf: [32]u8 = undefined;
|
||||
const len_str = std.fmt.bufPrint(&buf, "{d}", .{body.len}) catch unreachable;
|
||||
|
||||
self.out_file.writeStreamingAll(self.io, "Content-Length: ") catch return error.WriteFailed;
|
||||
self.out_file.writeStreamingAll(self.io, len_str) catch return error.WriteFailed;
|
||||
self.out_file.writeStreamingAll(self.io, "\r\n\r\n") catch return error.WriteFailed;
|
||||
self.out_file.writeStreamingAll(self.io, body) catch return error.WriteFailed;
|
||||
}
|
||||
|
||||
/// Read a single line terminated by \r\n. Returns content without \r\n.
|
||||
fn readLine(self: *Transport) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).empty;
|
||||
while (true) {
|
||||
const byte = self.in.takeByte() catch |err| switch (err) {
|
||||
error.EndOfStream => {
|
||||
if (buf.items.len == 0) return error.EndOfStream;
|
||||
return buf.items;
|
||||
},
|
||||
else => return error.ReadFailed,
|
||||
};
|
||||
|
||||
if (byte == '\n') {
|
||||
const line = buf.items;
|
||||
if (line.len > 0 and line[line.len - 1] == '\r') {
|
||||
return line[0 .. line.len - 1];
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
try buf.append(self.allocator, byte);
|
||||
}
|
||||
}
|
||||
};
|
||||
331
src/lsp/types.zig
Normal file
331
src/lsp/types.zig
Normal file
@@ -0,0 +1,331 @@
|
||||
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,\"hoverProvider\":true,\"documentSymbolProvider\":true," ++
|
||||
"\"completionProvider\":{{\"triggerCharacters\":[\".\"]}}," ++
|
||||
"\"signatureHelpProvider\":{{\"triggerCharacters\":[\"(\",\",\"]}}," ++
|
||||
"\"semanticTokensProvider\":{{\"legend\":{{" ++
|
||||
"\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\"]," ++
|
||||
"\"tokenModifiers\":[\"declaration\",\"readonly\"]" ++
|
||||
"}},\"full\":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 {
|
||||
var buf = std.ArrayList(u8).empty;
|
||||
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, ']');
|
||||
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 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;
|
||||
};
|
||||
|
||||
/// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user