1:- module(lsp_server, [main/0]).
9:- use_module(library(apply), [maplist/2]). 10:- use_module(library(apply_macros)). 11:- use_module(library(debug), [debug/3, debug/1]). 12:- use_module(library(http/json), [atom_json_dict/3]). 13:- use_module(library(prolog_xref)). 14:- use_module(library(prolog_source), [directory_source_files/3]). 15:- use_module(library(utf8), [utf8_codes//1]). 16:- use_module(library(socket), [tcp_socket/1,
17 tcp_bind/2,
18 tcp_accept/3,
19 tcp_listen/2,
20 tcp_open_socket/2]). 21:- use_module(library(yall)). 22:- use_module(library(prolog_stack)). 23
24:- include('_lsp_path_add.pl'). 25
26:- use_module(lsp(lsp_utils)). 27:- use_module(lsp(lsp_checking), [check_errors/2]). 28:- use_module(lsp(lsp_parser), [lsp_request//1]). 29:- use_module(lsp(lsp_changes), [handle_doc_changes/2]). 30:- use_module(lsp(lsp_completion), [completions_at/3]). 31:- use_module(lsp(lsp_colours), [file_colours/2,
32 file_range_colours/4,
33 token_types/1,
34 token_modifiers/1]). 35:- use_module(lsp(lsp_formatter), [file_format_edits/2]). 36:- use_module(lsp(lsp_highlights), [highlights_at_position/3]). 37
38main :-
39 set_prolog_flag(debug_on_error, false),
40 set_prolog_flag(report_error, true),
41 set_prolog_flag(verbose, silent),
42 set_prolog_flag(toplevel_prompt, ''),
43 current_prolog_flag(argv, Args),
44 debug(server),
45 start(Args).
46
47start([stdio]) :- !,
48 debug(server, "Starting stdio client", []),
49 stdio_server.
50start([port, Port]) :- !,
51 debug(server, "Starting socket client on port ~w", [Port]),
52 atom_number(Port, PortN),
53 socket_server(PortN).
54start(Args) :-
55 debug(server, "Unknown args ~w", [Args]).
56
57:- dynamic shutdown_request_received/0. 58:- dynamic exit_request_received/0. 59
61
62stdio_server :-
63 current_input(In),
64 current_output(Out),
65 stream_pair(StreamPair, In, Out),
66 handle_requests_stream(StreamPair).
67
69socket_server(Port) :-
70 tcp_socket(Socket),
71 tcp_bind(Socket, Port),
72 tcp_listen(Socket, 5),
73 tcp_open_socket(Socket, StreamPair),
74 stream_pair(StreamPair, AcceptFd, _),
75 dispatch_socket_client(AcceptFd).
76
77dispatch_socket_client(AcceptFd) :-
78 tcp_accept(AcceptFd, Socket, Peer),
79 80 81 process_client(Socket, Peer).
82
83process_client(Socket, Peer) :-
84 setup_call_cleanup(
85 tcp_open_socket(Socket, StreamPair),
86 ( debug(server, "Connecting new client ~w", [Peer]),
87 handle_requests_stream(StreamPair) ),
88 close(StreamPair)).
89
91
92handle_requests_stream(StreamPair) :-
93 stream_pair(StreamPair, In, Out),
94 set_stream(In, buffer(full)),
95 set_stream(In, newline(posix)),
96 set_stream(In, tty(false)),
97 set_stream(In, representation_errors(error)),
98 99 100 set_stream(In, encoding(octet)),
101 set_stream(Out, encoding(utf8)),
102 client_handler(A-A, In, Out).
103
104client_handler(Extra-ExtraTail, In, Out) :-
105 wait_for_input([In], Ready, 1.0),
106 ( exit_request_received
107 -> debug(server(high), "ending client handler loop", [])
108 ; ( Ready =@= []
109 -> client_handler(Extra-ExtraTail, In, Out)
110 ; fill_buffer(In),
111 read_pending_codes(In, ReadCodes, Tail),
112 ( Tail == []
113 -> true
114 ; ExtraTail = ReadCodes,
115 handle_requests(Out, Extra, Remainder),
116 client_handler(Remainder-Tail, In, Out) ) ) ).
117
120handle_requests(Out, In, Tail) :-
121 phrase(lsp_request(Req), In, Rest), !,
122 ignore(handle_request(Req, Out)),
123 ( var(Rest)
124 -> Tail = Rest
125 ; handle_requests(Out, Rest, Tail) ).
126handle_requests(_, T, T).
127
129
130send_message(Stream, Msg) :-
131 put_dict(jsonrpc, Msg, "2.0", VersionedMsg),
132 atom_json_dict(JsonCodes, VersionedMsg, [as(codes), width(0)]),
133 phrase(utf8_codes(JsonCodes), UTF8Codes),
134 length(UTF8Codes, ContentLength),
135 format(Stream, "Content-Length: ~w\r\n\r\n~s", [ContentLength, JsonCodes]),
136 flush_output(Stream).
137
138handle_request(Req, OutStream) :-
139 debug(server(high), "Request ~w", [Req.body]),
140 catch_with_backtrace(
141 ( ( shutdown_request_received
142 -> ( Req.body.method == "exit"
143 -> handle_msg(Req.body.method, Req.body, _Resp)
144 ; send_message(OutStream, _{id: Req.body.id, error: _{code: -32600, message: "Invalid Request"}}) )
145 ; ( handle_msg(Req.body.method, Req.body, Resp)
146 -> true
147 ; throw(error(domain_error(handleable_message, Req),
148 context(_Loc, "handle_msg/3 returned false"))) ),
149 ( is_dict(Resp) -> send_message(OutStream, Resp) ; true ) ) ),
150 Err,
151 ( print_message(error, Err),
152 get_dict(id, Req.body, Id),
153 send_message(OutStream, _{id: Id,
154 error: _{code: -32001,
155 message: "server error"}})
156 )).
157
159
160server_capabilities(_{textDocumentSync: _{openClose: true,
161 change: 2, 162 save: _{includeText: false},
163 willSave: false,
164 willSaveWaitUntil: false},
165 hoverProvider: true,
166 completionProvider: _{},
167 definitionProvider: true,
168 declarationProvider: true,
169 implementationProvider: true,
170 referencesProvider: true,
171 documentHighlightProvider: true,
172 documentSymbolProvider: true,
173 workspaceSymbolProvider: true,
174 codeActionProvider: false,
176 documentFormattingProvider: true,
177 documentRangeFormattingProvider: true,
179 renameProvider: true,
180 181 182 foldingRangeProvider: false,
183 184 185 semanticTokensProvider: _{legend: _{tokenTypes: TokenTypes,
186 tokenModifiers: TokenModifiers},
187 range: true,
188 189 full: _{delta: false}},
190 workspace: _{workspaceFolders: _{supported: true,
191 changeNotifications: true}}}
191)
191 :-
192 token_types(TokenTypes),
193 token_modifiers(TokenModifiers)
193.
194
195:- dynamic loaded_source/1. 196
198handle_msg("initialize", Msg,
199 _{id: Id, result: _{capabilities: ServerCapabilities}}) :-
200 _{id: Id, params: Params} :< Msg, !,
201 ( Params.rootUri \== null
202 -> ( url_path(Params.rootUri, RootPath),
203 directory_source_files(RootPath, Files, [recursive(true)]),
204 maplist([F]>>assert(loaded_source(F)), Files) )
205 ; true ),
206 server_capabilities(ServerCapabilities).
207handle_msg("shutdown", Msg, _{id: Id, result: []}) :-
208 _{id: Id} :< Msg,
209 debug(server, "received shutdown message", []),
210 asserta(shutdown_request_received).
211handle_msg("exit", _Msg, false) :-
212 debug(server, "received exit, shutting down", []),
213 asserta(exit_request_received),
214 ( shutdown_request_received
215 -> debug(server, "Post-shutdown exit, okay", [])
216 ; debug(server, "No shutdown, unexpected exit", []),
217 halt(1) ).
218handle_msg("textDocument/hover", Msg, _{id: Id, result: Response}) :-
219 _{params: _{position: _{character: Char0, line: Line0},
220 textDocument: _{uri: Doc}}, id: Id} :< Msg,
221 url_path(Doc, Path),
222 Line1 is Line0 + 1,
223 ( help_at_position(Path, Line1, Char0, Help)
224 -> Response = _{contents: _{kind: plaintext, value: Help}}
225 ; Response = null ).
226handle_msg("textDocument/documentSymbol", Msg, _{id: Id, result: Symbols}) :-
227 _{id: Id, params: _{textDocument: _{uri: Doc}}} :< Msg,
228 url_path(Doc, Path), !,
229 xref_source(Path),
230 findall(
231 Symbol,
232 ( xref_defined(Path, Goal, local(Line)),
233 succ(Line, NextLine),
234 succ(Line0, Line),
235 functor(Goal, Name, Arity),
236 format(string(GoalName), "~w/~w", [Name, Arity]),
237 Symbol = _{name: GoalName,
238 kind: 12, 239 location:
240 _{uri: Doc,
241 range: _{start: _{line: Line0, character: 1},
242 end: _{line: NextLine, character: 0}}}}
243 ),
244 Symbols).
245handle_msg("textDocument/definition", Msg, _{id: Id, result: Location}) :-
246 _{id: Id, params: Params} :< Msg,
247 _{textDocument: _{uri: Doc},
248 position: _{line: Line0, character: Char0}} :< Params,
249 url_path(Doc, Path),
250 succ(Line0, Line1),
251 clause_in_file_at_position(Name/Arity, Path, line_char(Line1, Char0)),
252 defined_at(Path, Name/Arity, Location).
253handle_msg("textDocument/definition", Msg, _{id: Msg.id, result: null}) :- !.
254handle_msg("textDocument/references", Msg, _{id: Id, result: Locations}) :-
255 _{id: Id, params: Params} :< Msg,
256 _{textDocument: _{uri: Uri},
257 position: _{line: Line0, character: Char0}} :< Params,
258 url_path(Uri, Path),
259 succ(Line0, Line1),
260 clause_in_file_at_position(Clause, Path, line_char(Line1, Char0)),
261 findall(
262 Location,
263 ( loaded_source(Doc),
264 url_path(DocUri, Doc),
265 called_at(Doc, Clause, Locs0),
266 267 maplist([D0, D]>>put_dict(uri, D0, DocUri, D), Locs0, Locs1),
268 member(Location, Locs1)
269 ),
270 Locations0), !,
271 ordered_locations(Locations0, Locations).
272handle_msg("textDocument/references", Msg, _{id: Msg.id, result: null}) :- !.
273handle_msg("textDocument/completion", Msg, _{id: Id, result: Completions}) :-
274 _{id: Id, params: Params} :< Msg,
275 _{textDocument: _{uri: Uri},
276 position: _{line: Line0, character: Char0}} :< Params,
277 url_path(Uri, Path),
278 succ(Line0, Line1),
279 completions_at(Path, line_char(Line1, Char0), Completions).
280handle_msg("textDocument/formatting", Msg, _{id: Id, result: Edits}) :-
281 _{id: Id, params: Params} :< Msg,
282 _{textDocument: _{uri: Uri}} :< Params,
283 url_path(Uri, Path),
284 file_format_edits(Path, Edits).
285handle_msg("textDocument/rangeFormatting", Msg, _{id: Id, result: Edits}) :-
286 _{id: Id, params: Params} :< Msg,
287 _{textDocument: _{uri: Uri}, range: Range} :< Params,
288 url_path(Uri, Path),
289 file_format_edits(Path, Edits0),
290 include(edit_in_range(Range), Edits0, Edits).
291handle_msg("textDocument/documentHighlight", Msg, _{id: Id, result: Locations}) :-
292 _{id: Id, params: Params} :< Msg,
293 _{textDocument: _{uri: Uri},
294 position: _{line: Line0, character: Char0}} :< Params,
295 url_path(Uri, Path),
296 succ(Line0, Line1),
297 highlights_at_position(Path, line_char(Line1, Char0), Locations), !.
298handle_msg("textDocument/documentHighlight", Msg, _{id: Id, result: null}) :-
299 _{id: Id} :< Msg.
300handle_msg("textDocument/rename", Msg, _{id: Id, result: Result}) :-
301 _{id: Id, params: Params} :< Msg,
302 _{textDocument: _{uri: Uri},
303 position: _{line: Line0, character: Char0},
304 newName: NewName} :< Params,
305 url_path(Uri, Path),
306 succ(Line0, Line1),
307 308 309 lsp_highlights:highlights_at_position(Path, line_char(Line1, Char0), '$var'(_),
310 Positions),
311 maplist([P0, P1]>>put_dict(newText, P0, NewName, P1), Positions, Edits),
312 atom_string(AUri, Uri), 313 dict_create(Changes, _, [AUri=Edits]),
314 Result = _{changes: Changes}.
315handle_msg("textDocument/rename", Msg, _{id: Id, error: _{message: "Nothing that can be renamed here.",
316 code: -32602}}) :-
317 _{id: Id} :< Msg.
318handle_msg("textDocument/semanticTokens", Msg, Response) :-
319 handle_msg("textDocument/semanticTokens/full", Msg, Response).
320handle_msg("textDocument/semanticTokens/full", Msg,
321 _{id: Id, result: _{data: Highlights}}) :-
322 _{id: Id, params: Params} :< Msg,
323 _{textDocument: _{uri: Uri}} :< Params,
324 url_path(Uri, Path), !,
325 xref_source(Path),
326 file_colours(Path, Highlights).
327handle_msg("textDocument/semanticTokens/range", Msg,
328 _{id: Id, result: _{data: Highlights}}) :-
329 _{id: Id, params: Params} :< Msg,
330 _{textDocument: _{uri: Uri}, range: Range} :< Params,
331 _{start: _{line: StartLine0, character: StartChar},
332 end: _{line: EndLine0, character: EndChar}} :< Range,
333 url_path(Uri, Path), !,
334 succ(StartLine0, StartLine), succ(EndLine0, EndLine),
335 xref_source(Path),
336 file_range_colours(Path,
337 line_char(StartLine, StartChar),
338 line_char(EndLine, EndChar),
339 Highlights).
341handle_msg("textDocument/didOpen", Msg, Resp) :-
342 _{params: _{textDocument: TextDoc}} :< Msg,
343 _{uri: FileUri} :< TextDoc,
344 url_path(FileUri, Path),
345 ( loaded_source(Path) ; assertz(loaded_source(Path)) ),
346 check_errors_resp(FileUri, Resp).
347handle_msg("textDocument/didChange", Msg, false) :-
348 _{params: _{textDocument: TextDoc,
349 contentChanges: Changes}} :< Msg,
350 _{uri: Uri} :< TextDoc,
351 url_path(Uri, Path),
352 handle_doc_changes(Path, Changes).
353handle_msg("textDocument/didSave", Msg, Resp) :-
354 _{params: Params} :< Msg,
355 check_errors_resp(Params.textDocument.uri, Resp).
356handle_msg("textDocument/didClose", Msg, false) :-
357 _{params: _{textDocument: TextDoc}} :< Msg,
358 _{uri: FileUri} :< TextDoc,
359 url_path(FileUri, Path),
360 retractall(loaded_source(Path)).
361handle_msg("initialized", Msg, false) :-
362 debug(server, "initialized ~w", [Msg]).
363handle_msg("$/cancelRequest", _Msg, false).
365handle_msg(_, Msg, _{id: Id, error: _{code: -32603, message: "Unimplemented"}}) :-
366 _{id: Id} :< Msg, !,
367 debug(server, "unknown message ~w", [Msg]).
368handle_msg(_, Msg, false) :-
369 debug(server, "unknown notification ~w", [Msg]).
370
371check_errors_resp(FileUri, _{method: "textDocument/publishDiagnostics",
372 params: _{uri: FileUri, diagnostics: Errors}}) :-
373 url_path(FileUri, Path),
374 check_errors(Path, Errors).
375check_errors_resp(_, false) :-
376 debug(server, "Failed checking errors", []).
377
378edit_in_range(Range, Edit) :-
379 _{start: _{line: RStartLine, character: RStartChar},
380 end: _{line: REndLine, character: REndChar}} :< Range,
381 _{start: _{line: EStartLine, character: EStartChar},
382 end: _{line: EEndLine, character: EEndChar}} :< Edit.range,
383 RStartLine =< EStartLine, REndLine >= EEndLine,
384 ( RStartLine == EStartLine
385 -> RStartChar =< EStartChar
386 387 ; ( REndLine == EEndLine
388 -> REndChar >= EEndChar
389 ; true ) ).
394ordered_locations(Locations, OrderedLocations) :-
395 maplist([D, SL-D]>>( get_dict(range, D, Range),
396 get_dict(start, Range, Start),
397 get_dict(line, Start, SL) ),
398 Locations,
399 Locs1),
400 sort(1, @=<, Locs1, Locs2),
401 maplist([_-D, D]>>true, Locs2, OrderedLocations)
LSP Server
The main entry point for the Language Server implementation.