Skip to content

Commit 81da5d2

Browse files
committed
add example client and server implementations
1 parent 3b26e57 commit 81da5d2

File tree

3 files changed

+549
-0
lines changed

3 files changed

+549
-0
lines changed

build.zig

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,52 @@ pub fn build(b: *std.Build) void {
7878
const docs_step = b.step("docs", "Generate and install documentation");
7979
docs_step.dependOn(&install_docs.step);
8080

81+
// ------------------------------- Examples --------------------------------
82+
83+
const hello_server_exe = b.addExecutable(.{
84+
.name = "hello-server",
85+
.root_module = b.createModule(.{
86+
.root_source_file = b.path("examples/hello_server.zig"),
87+
.target = target,
88+
.optimize = optimize,
89+
.imports = &.{
90+
.{ .name = "lsp", .module = lsp_module },
91+
},
92+
}),
93+
.use_lld = use_llvm,
94+
.use_llvm = use_llvm,
95+
});
96+
b.installArtifact(hello_server_exe);
97+
98+
const install_hello_server_step = b.step("install-hello-server", "Install the hello-server example");
99+
install_hello_server_step.dependOn(&b.addInstallArtifact(hello_server_exe, .{}).step);
100+
101+
const hello_client_exe = b.addExecutable(.{
102+
.name = "hello-client",
103+
.root_module = b.createModule(.{
104+
.root_source_file = b.path("examples/hello_client.zig"),
105+
.target = target,
106+
.optimize = optimize,
107+
.imports = &.{
108+
.{ .name = "lsp", .module = lsp_module },
109+
},
110+
}),
111+
.use_lld = use_llvm,
112+
.use_llvm = use_llvm,
113+
});
114+
b.installArtifact(hello_client_exe);
115+
116+
const run_hello_client = b.addRunArtifact(hello_client_exe);
117+
if (b.args) |args| {
118+
run_hello_client.addArgs(args);
119+
if (args.len == 1) {
120+
run_hello_client.addArtifactArg(hello_server_exe);
121+
}
122+
}
123+
124+
const run_hello_client_step = b.step("run-hello-client", "Run the hello-client example");
125+
run_hello_client_step.dependOn(&run_hello_client.step);
126+
81127
// --------------------------------- Tests ---------------------------------
82128

83129
const lsp_tests = b.addTest(.{

examples/hello_client.zig

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
//! This file implements an example LSP client.
2+
//!
3+
//! This example is NOT meant to be a blueprint on how to design an LSP client.
4+
//! Instead it meant to showcase the various low-level utilities provided by
5+
//! this library to LSP client authors.
6+
//!
7+
//! This library will take care of the tedious boilerplate (stdio, JSON-RPC,
8+
//! LSP data types) while allowing authors to freely decide on how to
9+
//! architect their LSP client.
10+
//!
11+
//! For detailed information on how the language server protocol works, checkout the official specificiation:
12+
//! https://microsoft.github.io/language-server-protocol/specifications/specification-current
13+
//!
14+
//! The run this example program with the build system, use the following command:
15+
//! ```
16+
//! zig build run-hello-client -- path/to/unformatted/file.zig /path/to/zls
17+
//! ```
18+
//!
19+
//! Omitting the arguments to the language server will use `./hello_server.zig` as the langauge server:
20+
//! ```
21+
//! zig build run-hello-client -- path/to/unformatted/file.zig
22+
//! ```
23+
//!
24+
//! See the `usage` below for more information.
25+
//!
26+
27+
const std = @import("std");
28+
const builtin = @import("builtin");
29+
const lsp = @import("lsp");
30+
31+
pub const std_options: std.Options = .{
32+
.log_level = .info,
33+
};
34+
35+
/// Be aware that the output doesn't clearly show the source (client or server) of the output
36+
const inherit_langauge_server_stderr: bool = false;
37+
38+
const usage =
39+
\\hello-client
40+
\\
41+
\\Give me a document an I will ask the language server whether it has any suggested changes to format the document.
42+
\\
43+
\\Usage: hello-client /path/to/unformatted/file.zig <language server arguments>
44+
\\
45+
\\Example: hello-client file.zig /path/to/zls
46+
\\ hello-client ../file.zig zls
47+
\\
48+
\\
49+
;
50+
51+
fn fatalWithUsage(comptime format: []const u8, args: anytype) noreturn {
52+
std.io.getStdErr().writeAll(usage) catch {};
53+
std.log.err(format, args);
54+
std.process.exit(1);
55+
}
56+
57+
fn fatal(comptime format: []const u8, args: anytype) noreturn {
58+
std.log.err(format, args);
59+
std.process.exit(1);
60+
}
61+
62+
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
63+
64+
pub fn main() !void {
65+
const gpa, const is_debug = switch (builtin.mode) {
66+
.Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
67+
.ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
68+
};
69+
defer if (is_debug) {
70+
_ = debug_allocator.deinit();
71+
};
72+
73+
const args = try std.process.argsAlloc(gpa);
74+
defer std.process.argsFree(gpa, args);
75+
76+
if (args.len < 3) fatalWithUsage("expected at least 2 arguments but got {d}", .{args.len - 1});
77+
78+
const input_file = std.fs.cwd().readFileAlloc(gpa, args[1], std.math.maxInt(u32)) catch |err|
79+
fatal("failed to read file '{s}': {}", .{ args[1], err });
80+
defer gpa.free(input_file);
81+
82+
// Spawn the language server as a child process.
83+
var child_process: std.process.Child = .init(args[2..], gpa);
84+
child_process.stdin_behavior = .Pipe;
85+
child_process.stdout_behavior = .Pipe;
86+
child_process.stderr_behavior = if (inherit_langauge_server_stderr) .Inherit else .Ignore;
87+
88+
child_process.spawn() catch |err| fatal("child process could not be created: {}", .{err});
89+
child_process.waitForSpawn() catch |err| fatal("child process could not be created: {}", .{err});
90+
91+
// Language servers can support multiple communication channels (e.g. stdio, pipes, sockets).
92+
// See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#implementationConsiderations
93+
//
94+
// The `TransportOverStdio` implements the necessary logic to read and write messages over stdio.
95+
var transport: lsp.TransportOverStdio = .init(child_process.stdout.?, child_process.stdin.?);
96+
97+
// The order of exchanged messages will look similar to this:
98+
//
99+
// 1. send `initialize` request and receive response
100+
// 2. send `initialized` notification
101+
// 3. send various requests like `textDocument/formatting`
102+
// 4. send `shutdown` request and receive response
103+
// 5. send `exit` notification
104+
105+
// Send an "initialize" request to the server
106+
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize
107+
try sendRequestToServer(gpa, transport.any(), .{ .number = 0 }, "initialize", lsp.types.InitializeParams{
108+
.capabilities = .{}, // the client capabilities tell the server what "features" the client supports
109+
});
110+
111+
// Wait for the response from the server
112+
// For the sake of simplicity, we will block here and read messages until the response to our request has been found. All other messages will be ignored.
113+
// A more sophisticated client implementation will need to handle messages asynchronously.
114+
const initialize_response = try readAndIgnoreUntilResponse(gpa, transport.any(), .{ .number = 0 }, "initialize");
115+
defer initialize_response.deinit();
116+
117+
const initialize_result: lsp.types.InitializeResult = initialize_response.value;
118+
_ = initialize_result.capabilities; // the server capabilities tell the client what "features" the server supports
119+
120+
const serverName = if (initialize_result.serverInfo) |serverInfo| serverInfo.name else "unknown";
121+
std.log.info("Good morning Mx. {s}.", .{serverName});
122+
123+
// Check whether the server supports the `textDocument/formatting` request
124+
const supports_formatting = blk: {
125+
const documentFormattingProvider = initialize_result.capabilities.documentFormattingProvider orelse break :blk false;
126+
switch (documentFormattingProvider) {
127+
.bool => |supported| break :blk supported,
128+
.DocumentFormattingOptions => break :blk true,
129+
}
130+
};
131+
132+
if (!supports_formatting) {
133+
std.log.err("It seems like you're not qualified for this task. Get out!", .{});
134+
std.process.exit(1);
135+
}
136+
137+
// Send a "initialized" notification to the server
138+
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialized
139+
try sendNotificationToServer(gpa, transport.any(), "initialized", lsp.types.InitializedParams{});
140+
141+
// ----------------
142+
143+
std.log.info("This document recently came in by the CLI.", .{});
144+
try sendNotificationToServer(gpa, transport.any(), "textDocument/didOpen", lsp.types.DidOpenTextDocumentParams{
145+
.textDocument = .{
146+
.uri = "untitled:Document", // Usually a file system uri will be provided like 'file:///path/to/main.zig'
147+
.languageId = "",
148+
.text = input_file,
149+
.version = 0,
150+
},
151+
});
152+
153+
std.log.info("Just to double check, could you verify that it is formatted correctly?", .{});
154+
try sendRequestToServer(gpa, transport.any(), .{ .number = 1 }, "textDocument/formatting", lsp.types.DocumentFormattingParams{
155+
.textDocument = .{ .uri = "untitled:Document" },
156+
.options = .{ .tabSize = 4, .insertSpaces = true },
157+
});
158+
159+
const formatting_response = try readAndIgnoreUntilResponse(gpa, transport.any(), .{ .number = 1 }, "textDocument/formatting");
160+
defer formatting_response.deinit();
161+
162+
const text_edits = formatting_response.value orelse &.{};
163+
if (text_edits.len == 0) {
164+
std.log.info("{s}: I have no comments.", .{serverName});
165+
} else {
166+
std.log.info("{s}: I have identified {d} non-compliance(s) with my formatting specification", .{ serverName, text_edits.len });
167+
}
168+
169+
// ----------------
170+
171+
std.log.info("Well, thanks for your insight on this. Now get out!", .{});
172+
173+
// Send a "shutdown" request to the server
174+
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#shutdown
175+
// Even though this is a request, we do not wait for a response because we are going to close the server anyway.
176+
try sendRequestToServer(gpa, transport.any(), .{ .number = 2 }, "shutdown", {});
177+
178+
// Send a "exit" notification to the server
179+
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialized
180+
try sendNotificationToServer(gpa, transport.any(), "exit", {});
181+
182+
// The "exit" notification will ask the server to exit its process. Ideally we should wait with a timeout in case the server is not behaving correctly.
183+
_ = try child_process.wait();
184+
}
185+
186+
fn sendRequestToServer(
187+
allocator: std.mem.Allocator,
188+
transport: lsp.AnyTransport,
189+
id: lsp.JsonRPCMessage.ID,
190+
comptime method: []const u8,
191+
params: lsp.ParamsType(method),
192+
) !void {
193+
std.log.debug("sending '{s}' request to server", .{method});
194+
195+
const request: lsp.TypedJsonRPCRequest(lsp.ParamsType(method)) = .{
196+
.id = id,
197+
.method = method,
198+
.params = params,
199+
};
200+
201+
const request_stringified = try std.json.stringifyAlloc(allocator, request, .{ .emit_null_optional_fields = false });
202+
defer allocator.free(request_stringified);
203+
204+
try transport.writeJsonMessage(request_stringified);
205+
}
206+
207+
fn sendNotificationToServer(
208+
allocator: std.mem.Allocator,
209+
transport: lsp.AnyTransport,
210+
comptime method: []const u8,
211+
params: lsp.ParamsType(method),
212+
) !void {
213+
std.log.debug("sending '{s}' notification to server", .{method});
214+
215+
const notification: lsp.TypedJsonRPCNotification(lsp.ParamsType(method)) = .{
216+
.method = method,
217+
.params = params,
218+
};
219+
220+
const notification_stringified = try std.json.stringifyAlloc(allocator, notification, .{ .emit_null_optional_fields = false });
221+
defer allocator.free(notification_stringified);
222+
223+
try transport.writeJsonMessage(notification_stringified);
224+
}
225+
226+
/// Do not use such a function in an actual implementation.
227+
fn readAndIgnoreUntilResponse(
228+
allocator: std.mem.Allocator,
229+
transport: lsp.AnyTransport,
230+
id: lsp.JsonRPCMessage.ID,
231+
comptime method: []const u8,
232+
) !std.json.Parsed(lsp.ResultType(method)) {
233+
while (true) {
234+
// read the unparsed JSON-RPC message
235+
const json_message = try transport.readJsonMessage(allocator);
236+
defer allocator.free(json_message);
237+
std.log.debug("received message from server: {s}", .{json_message});
238+
239+
// try to find the "id" field
240+
const parsed_message = try std.json.parseFromSlice(
241+
struct { id: ?lsp.JsonRPCMessage.ID = null },
242+
allocator,
243+
json_message,
244+
.{ .ignore_unknown_fields = true },
245+
);
246+
defer parsed_message.deinit();
247+
248+
const actual_id = parsed_message.value.id orelse {
249+
// std.log.info("received message from server while waiting for '{}'. Ignoring...", .{std.json.fmt(id, .{})});
250+
continue;
251+
};
252+
253+
if (!id.eql(actual_id)) {
254+
// std.log.info("received message from server while waiting for '{}'. Ignoring...", .{std.json.fmt(id, .{})});
255+
continue;
256+
}
257+
258+
// parse the "result" field to the expected type
259+
const parsed_response = try std.json.parseFromSlice(
260+
struct { result: lsp.ResultType(method) },
261+
allocator,
262+
json_message,
263+
.{ .ignore_unknown_fields = true, .allocate = .alloc_always },
264+
);
265+
266+
return .{
267+
.arena = parsed_response.arena,
268+
.value = parsed_response.value.result,
269+
};
270+
}
271+
}

0 commit comments

Comments
 (0)