Skip to content

Commit e9d526f

Browse files
committed
add a tiny 'framework' to implement a language server
Not really sure how I should call this...
1 parent 5a4870a commit e9d526f

File tree

4 files changed

+943
-0
lines changed

4 files changed

+943
-0
lines changed

build.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ pub fn build(b: *std.Build) void {
124124
const run_hello_client_step = b.step("run-hello-client", "Run the hello-client example");
125125
run_hello_client_step.dependOn(&run_hello_client.step);
126126

127+
const my_first_server = b.addExecutable(.{
128+
.name = "my-first-server",
129+
.root_module = b.createModule(.{
130+
.root_source_file = b.path("examples/my_first_server.zig"),
131+
.target = target,
132+
.optimize = optimize,
133+
.imports = &.{
134+
.{ .name = "lsp", .module = lsp_module },
135+
},
136+
}),
137+
.use_lld = use_llvm,
138+
.use_llvm = use_llvm,
139+
});
140+
b.installArtifact(my_first_server);
141+
127142
// --------------------------------- Tests ---------------------------------
128143

129144
const lsp_tests = b.addTest(.{

examples/my_first_server.zig

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
//! Implements a language server using the `lsp.basic_server` abstraction.
2+
3+
const std = @import("std");
4+
const builtin = @import("builtin");
5+
const lsp = @import("lsp");
6+
7+
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
8+
var log_transport: ?lsp.AnyTransport = null;
9+
10+
pub fn main() !void {
11+
const gpa, const is_debug = switch (builtin.mode) {
12+
.Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
13+
.ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
14+
};
15+
defer if (is_debug) {
16+
_ = debug_allocator.deinit();
17+
};
18+
19+
// language server typically communicate over stdio (stdin and stdout)
20+
var transport: lsp.TransportOverStdio = .init(std.io.getStdIn(), std.io.getStdOut());
21+
22+
var handler: Handler = .init(gpa);
23+
defer handler.deinit();
24+
25+
try lsp.basic_server.run(
26+
gpa,
27+
transport.any(),
28+
&handler,
29+
std.log.err,
30+
);
31+
}
32+
33+
// Most functions can be omitted if you are not interested them. A unhandled request will automatically return a 'null' response.
34+
35+
pub const Handler = struct {
36+
allocator: std.mem.Allocator,
37+
files: std.StringHashMapUnmanaged([]u8),
38+
offset_encoding: lsp.offsets.Encoding,
39+
40+
fn init(allocator: std.mem.Allocator) Handler {
41+
return .{
42+
.allocator = allocator,
43+
.files = .{},
44+
.offset_encoding = .@"utf-16",
45+
};
46+
}
47+
48+
fn deinit(handler: *Handler) void {
49+
var file_it = handler.files.iterator();
50+
while (file_it.next()) |entry| {
51+
handler.allocator.free(entry.key_ptr.*);
52+
handler.allocator.free(entry.value_ptr.*);
53+
}
54+
55+
handler.files.deinit(handler.allocator);
56+
57+
handler.* = undefined;
58+
}
59+
60+
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize
61+
pub fn initialize(
62+
handler: *Handler,
63+
_: std.mem.Allocator,
64+
request: lsp.types.InitializeParams,
65+
) lsp.types.InitializeResult {
66+
std.log.debug("Received 'initialize' message", .{});
67+
68+
if (request.clientInfo) |client_info| {
69+
std.log.info("The client is '{s}' ({s})", .{ client_info.name, client_info.version orelse "unknown version" });
70+
}
71+
72+
// Specifies which features are supported by the client/editor.
73+
const client_capabilities: lsp.types.ClientCapabilities = request.capabilities;
74+
75+
// Pick the client's favorite character offset encoding.
76+
if (client_capabilities.general) |general| {
77+
for (general.positionEncodings orelse &.{}) |encoding| {
78+
handler.offset_encoding = switch (encoding) {
79+
.@"utf-8" => .@"utf-8",
80+
.@"utf-16" => .@"utf-16",
81+
.@"utf-32" => .@"utf-32",
82+
.custom_value => continue,
83+
};
84+
break;
85+
}
86+
}
87+
88+
// Specifies which features are supported by the language server.
89+
const server_capabilities: lsp.types.ServerCapabilities = .{
90+
.positionEncoding = switch (handler.offset_encoding) {
91+
.@"utf-8" => .@"utf-8",
92+
.@"utf-16" => .@"utf-16",
93+
.@"utf-32" => .@"utf-32",
94+
},
95+
.textDocumentSync = .{
96+
.TextDocumentSyncOptions = .{
97+
.openClose = true,
98+
.change = .Full,
99+
},
100+
},
101+
.hoverProvider = .{ .bool = true },
102+
};
103+
104+
// Tries to validate that our server capabilities are actually implemented.
105+
if (@import("builtin").mode == .Debug) {
106+
lsp.basic_server.validateServerCapabilities(Handler, server_capabilities);
107+
}
108+
109+
return .{
110+
.serverInfo = .{
111+
.name = "My first LSP",
112+
.version = "0.1.0",
113+
},
114+
.capabilities = server_capabilities,
115+
};
116+
}
117+
118+
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialized
119+
pub fn initialized(
120+
_: *Handler,
121+
_: std.mem.Allocator,
122+
_: lsp.types.InitializedParams,
123+
) void {
124+
std.log.debug("Received 'initialized' notification", .{});
125+
}
126+
127+
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#shutdown
128+
pub fn shutdown(
129+
_: *Handler,
130+
_: std.mem.Allocator,
131+
_: void,
132+
) ?void {
133+
std.log.debug("Received 'shutdown' request", .{});
134+
return null;
135+
}
136+
137+
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#exit
138+
/// The `lsp.basic_server.run` function will automatically return after this function completes.
139+
pub fn exit(
140+
_: *Handler,
141+
_: std.mem.Allocator,
142+
_: void,
143+
) void {
144+
std.log.debug("Received 'exit' notification", .{});
145+
}
146+
147+
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didOpen
148+
pub fn @"textDocument/didOpen"(
149+
self: *Handler,
150+
_: std.mem.Allocator,
151+
notification: lsp.types.DidOpenTextDocumentParams,
152+
) !void {
153+
std.log.debug("Received 'textDocument/didOpen' notification", .{});
154+
155+
const new_text = try self.allocator.dupe(u8, notification.textDocument.text);
156+
errdefer self.allocator.free(new_text);
157+
158+
const gop = try self.files.getOrPut(self.allocator, notification.textDocument.uri);
159+
160+
if (gop.found_existing) {
161+
std.log.warn("Document opened twice: '{s}'", .{notification.textDocument.uri});
162+
self.allocator.free(gop.value_ptr.*);
163+
} else {
164+
errdefer std.debug.assert(self.files.remove(notification.textDocument.uri));
165+
gop.key_ptr.* = try self.allocator.dupe(u8, notification.textDocument.uri);
166+
}
167+
168+
gop.value_ptr.* = new_text;
169+
}
170+
171+
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didChange
172+
pub fn @"textDocument/didChange"(
173+
self: *Handler,
174+
_: std.mem.Allocator,
175+
notification: lsp.types.DidChangeTextDocumentParams,
176+
) !void {
177+
std.log.debug("Received 'textDocument/didChange' notification", .{});
178+
179+
const current_text = self.files.getPtr(notification.textDocument.uri) orelse {
180+
std.log.warn("Modifying non existent Document: '{s}'", .{notification.textDocument.uri});
181+
return;
182+
};
183+
184+
var buffer: std.ArrayListUnmanaged(u8) = .empty;
185+
errdefer buffer.deinit(self.allocator);
186+
187+
try buffer.appendSlice(self.allocator, current_text.*);
188+
189+
for (notification.contentChanges) |content_change| {
190+
switch (content_change) {
191+
.literal_1 => |change| {
192+
buffer.clearRetainingCapacity();
193+
try buffer.appendSlice(self.allocator, change.text);
194+
},
195+
.literal_0 => |change| {
196+
const loc = lsp.offsets.rangeToLoc(buffer.items, change.range, self.offset_encoding);
197+
try buffer.replaceRange(self.allocator, loc.start, loc.end - loc.start, change.text);
198+
},
199+
}
200+
}
201+
202+
const new_text = try buffer.toOwnedSlice(self.allocator);
203+
self.allocator.free(current_text.*);
204+
current_text.* = new_text;
205+
}
206+
207+
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didClose
208+
pub fn @"textDocument/didClose"(
209+
self: *Handler,
210+
_: std.mem.Allocator,
211+
notification: lsp.types.DidCloseTextDocumentParams,
212+
) !void {
213+
std.log.debug("Received 'textDocument/didClose' notification", .{});
214+
215+
const entry = self.files.fetchRemove(notification.textDocument.uri) orelse {
216+
std.log.warn("Closing non existent Document: '{s}'", .{notification.textDocument.uri});
217+
return;
218+
};
219+
self.allocator.free(entry.key);
220+
self.allocator.free(entry.value);
221+
}
222+
223+
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
224+
///
225+
/// This function can be omitted if you are not interested in this request. A `null` response will be automatically send back.
226+
pub fn @"textDocument/hover"(
227+
handler: *Handler,
228+
_: std.mem.Allocator,
229+
params: lsp.types.HoverParams,
230+
) ?lsp.types.Hover {
231+
std.log.debug("Received 'textDocument/hover' request", .{});
232+
233+
const source = handler.files.get(params.textDocument.uri) orelse
234+
return null; // We should actually read the document from the file system
235+
236+
const source_index = lsp.offsets.positionToIndex(source, params.position, handler.offset_encoding);
237+
std.log.debug("Hover position: line={d}, character={d}, index={d}", .{ params.position.line, params.position.character, source_index });
238+
239+
return .{
240+
.contents = .{
241+
.MarkupContent = .{
242+
.kind = .plaintext,
243+
.value = "I don't know what you are hovering over but I'd like to point out that you have a nice editor theme",
244+
},
245+
},
246+
};
247+
}
248+
249+
/// We received a response message from the client/editor.
250+
pub fn onResponse(
251+
_: *Handler,
252+
_: std.mem.Allocator,
253+
response: lsp.JsonRPCMessage.Response,
254+
) void {
255+
// We didn't make any requests to the client/editor.
256+
std.log.warn("received unexpected response from client with id '{?}'!", .{response.id});
257+
}
258+
};

0 commit comments

Comments
 (0)