1:- module(lsp_server, [main/0]).

LSP Server

The main entry point for the Language Server implementation.

author
- James Cash */
    9:- use_module(library(apply), [maplist/2]).   10:- use_module(library(debug), [debug/3, debug/1]).   11:- use_module(library(http/json), [atom_json_dict/3]).   12:- use_module(library(prolog_xref)).   13:- use_module(library(prolog_source), [directory_source_files/3]).   14:- use_module(library(utf8), [utf8_codes//1]).   15:- use_module(library(yall)).   16
   17:- use_module(lsp_utils).   18:- use_module(lsp_checking, [check_errors/2]).   19:- use_module(lsp_parser, [lsp_request//1]).   20:- use_module(lsp_changes, [handle_doc_changes/2]).   21:- use_module(lsp_completion, [completions_at/3]).   22:- use_module(lsp_colours, [file_colours/2,
   23                            file_range_colours/4,
   24                            token_types/1,
   25                            token_modifiers/1]).   26
   27main :-
   28    set_prolog_flag(debug_on_error, false),
   29    set_prolog_flag(report_error, true),
   30    set_prolog_flag(toplevel_prompt, ''),
   31    current_prolog_flag(argv, Args),
   32    debug(server),
   33    start(Args).
   34
   35start([stdio]) :- !,
   36    debug(server, "Starting stdio client", []),
   37    stdio_server.
   38start(Args) :-
   39    debug(server, "Unknown args ~w", [Args]).
   40
   41
   42% stdio server
   43
   44stdio_server :-
   45    current_input(In),
   46    set_stream(In, buffer(full)),
   47    set_stream(In, newline(posix)),
   48    set_stream(In, tty(false)),
   49    set_stream(In, representation_errors(error)),
   50    % handling UTF decoding in JSON parsing, but doing the auto-translation
   51    % causes Content-Length to be incorrect
   52    set_stream(In, encoding(octet)),
   53    current_output(Out),
   54    set_stream(Out, encoding(utf8)),
   55    stdio_handler(A-A, In).
   56% [TODO] add multithreading? Guess that will also need a message queue
   57% to write to stdout
   58stdio_handler(Extra-ExtraTail, In) :-
   59    wait_for_input([In], _, infinite),
   60    fill_buffer(In),
   61    read_pending_codes(In, ReadCodes, Tail),
   62    ( Tail == []
   63    -> true
   64    ; ( current_output(Out),
   65        ExtraTail = ReadCodes,
   66        handle_requests(Out, Extra, Remainder),
   67        stdio_handler(Remainder-Tail, In) )
   68    ).
   69
   70handle_requests(Out, In, Tail) :-
   71    handle_request(Out, In, Rest), !,
   72    ( var(Rest)
   73    -> Tail = Rest
   74    ; handle_requests(Out, Rest, Tail) ).
   75handle_requests(_, T, T).
   76
   77% general handling stuff
   78
   79send_message(Stream, Msg) :-
   80    put_dict(jsonrpc, Msg, "2.0", VersionedMsg),
   81    atom_json_dict(JsonCodes, VersionedMsg, [as(codes), width(0)]),
   82    phrase(utf8_codes(JsonCodes), UTF8Codes),
   83    length(UTF8Codes, ContentLength),
   84    format(Stream, "Content-Length: ~w\r\n\r\n~s", [ContentLength, JsonCodes]),
   85    flush_output(Stream).
   86
   87handle_request(OutStream, Input, Rest) :-
   88    phrase(lsp_request(Req), Input, Rest),
   89    debug(server(high), "Request ~w", [Req.body]),
   90    catch(
   91        ( handle_msg(Req.body.method, Req.body, Resp),
   92          debug(server(high), "response ~w", [Resp]),
   93          ( is_dict(Resp) -> send_message(OutStream, Resp) ; true ) ),
   94        Err,
   95        ( debug(server, "error handling msg ~w", [Err]),
   96          get_dict(id, Req.body, Id),
   97          send_message(OutStream, _{id: Id,
   98                                    error: _{code: -32001,
   99                                             message: "server error"}})
  100        )).
  101
  102% Handling messages
  103
  104server_capabilities(
  105    _{textDocumentSync: _{openClose: true,
  106                          change: 2, %incremental
  107                          save: _{includeText: false},
  108                          willSave: false,
  109                          willSaveWaitUntil: false %???
  110                          },
  111      hoverProvider: true,
  112      completionProvider: _{},
  113      definitionProvider: true,
  114      declarationProvider: true,
  115      implementationProvider: true,
  116      referencesProvider: true,
  117      documentHighlightProvider: false,
  118      documentSymbolProvider: true,
  119      workspaceSymbolProvider: true,
  120      codeActionProvider: false,
  122      documentFormattingProvider:false,
  124      renameProvider: false,
  125      % documentLinkProvider: false,
  126      % colorProvider: true,
  127      foldingRangeProvider: false,
  128      executeCommandProvider: _{commands: ["query", "assert"]},
  129      semanticTokensProvider: _{legend: _{tokenTypes: TokenTypes,
  130                                          tokenModifiers: TokenModifiers},
  131                                range: true,
  132                                % [TODO] implement deltas
  133                                full: _{delta: false}},
  134      workspace: _{workspaceFolders: _{supported: true,
  135                                       changeNotifications: true}}
  136     }
  137)
  137 :-
  138    token_types(TokenTypes),
  139    token_modifiers(TokenModifiers)
  139.
  140
  141:- dynamic loaded_source/1.  142
  143% messages (with a response)
  144handle_msg("initialize", Msg,
  145           _{id: Id, result: _{capabilities: ServerCapabilities} }) :-
  146    _{id: Id, params: Params} :< Msg, !,
  147    ( Params.rootUri \== null
  148    -> ( atom_concat('file://', RootPath, Params.rootUri),
  149         directory_source_files(RootPath, Files, [recursive(true)]),
  150         maplist([F]>>assert(loaded_source(F)), Files) )
  151    ; true ),
  152    server_capabilities(ServerCapabilities).
  153handle_msg("shutdown", Msg, _{id: Id, result: null}) :-
  154    _{id: Id} :< Msg,
  155    debug(server, "recieved shutdown message", []).
  156handle_msg("textDocument/hover", Msg, _{id: Id, result: Response}) :-
  157    _{params: _{position: _{character: Char0, line: Line0},
  158                textDocument: _{uri: Doc}}, id: Id} :< Msg,
  159    atom_concat('file://', Path, Doc),
  160    Line1 is Line0 + 1,
  161    (  help_at_position(Path, Line1, Char0, Help)
  162    -> Response = _{contents: _{kind: plaintext, value: Help}}
  163    ;  Response = null).
  164handle_msg("textDocument/documentSymbol", Msg, _{id: Id, result: Symbols}) :-
  165    _{id: Id, params: _{textDocument: _{uri: Doc}}} :< Msg,
  166    atom_concat('file://', Path, Doc), !,
  167    xref_source(Path),
  168    findall(
  169        Symbol,
  170        ( xref_defined(Path, Goal, local(Line)),
  171          succ(Line, NextLine),
  172          succ(Line0, Line),
  173          functor(Goal, Name, Arity),
  174          format(string(GoalName), "~w/~w", [Name, Arity]),
  175          Symbol = _{name: GoalName,
  176                     kind: 12, % function
  177                     location:
  178                     _{uri: Doc,
  179                       range: _{start: _{line: Line0, character: 1},
  180                                end: _{line: NextLine, character: 0}}}}
  181        ),
  182        Symbols).
  183handle_msg("textDocument/definition", Msg, _{id: Id, result: Location}) :-
  184    _{id: Id, params: Params} :< Msg,
  185    _{textDocument: _{uri: Doc},
  186      position: _{line: Line0, character: Char0}} :< Params,
  187    atom_concat('file://', Path, Doc),
  188    succ(Line0, Line1),
  189    clause_in_file_at_position(Name/Arity, Path, line_char(Line1, Char0)),
  190    defined_at(Path, Name/Arity, Location).
  191handle_msg("textDocument/definition", Msg, _{id: Msg.id, result: null}) :- !.
  192handle_msg("textDocument/references", Msg, _{id: Id, result: Locations}) :-
  193    _{id: Id, params: Params} :< Msg,
  194    _{textDocument: _{uri: Uri},
  195      position: _{line: Line0, character: Char0}} :< Params,
  196    atom_concat('file://', Path, Uri),
  197    succ(Line0, Line1),
  198    clause_in_file_at_position(Clause, Path, line_char(Line1, Char0)),
  199    findall(
  200        Location,
  201        ( loaded_source(Doc),
  202          atom_concat('file://', Doc, DocUri),
  203          called_at(Doc, Clause, Caller, Loc),
  204          relative_ref_location(DocUri, Caller, Loc, Location)
  205        ),
  206        Locations), !.
  207handle_msg("textDocument/references", Msg, _{id: Msg.id, result: null}) :- !.
  208handle_msg("textDocument/completion", Msg, _{id: Id, result: Completions}) :-
  209    _{id: Id, params: Params} :< Msg,
  210    _{textDocument: _{uri: Uri},
  211      position: _{line: Line0, character: Char0}} :< Params,
  212    atom_concat('file://', Path, Uri),
  213    succ(Line0, Line1),
  214    completions_at(Path, line_char(Line1, Char0), Completions).
  215handle_msg("textDocument/semanticTokens", Msg, Response) :-
  216    handle_msg("textDocument/semanticTokens/full", Msg, Response).
  217handle_msg("textDocument/semanticTokens/full", Msg,
  218           _{id: Id, result: _{data: Highlights}}) :-
  219    _{id: Id, params: Params} :< Msg,
  220    _{textDocument: _{uri: Uri}} :< Params,
  221    atom_concat('file://', Path, Uri), !,
  222    xref_source(Path),
  223    file_colours(Path, Highlights).
  224handle_msg("textDocument/semanticTokens/range", Msg,
  225           _{id: Id, result: _{data: Highlights}}) :-
  226    _{id: Id, params: Params} :< Msg,
  227    _{textDocument: _{uri: Uri}, range: Range} :< Params,
  228    _{start: _{line: StartLine0, character: StartChar},
  229      end: _{line: EndLine0, character: EndChar}} :< Range,
  230    atom_concat('file://', Path, Uri), !,
  231    succ(StartLine0, StartLine), succ(EndLine0, EndLine),
  232    xref_source(Path),
  233    file_range_colours(Path,
  234                       line_char(StartLine, StartChar),
  235                       line_char(EndLine, EndChar),
  236                       Highlights).
  237% notifications (no response)
  238handle_msg("textDocument/didOpen", Msg, Resp) :-
  239    _{params: _{textDocument: TextDoc}} :< Msg,
  240    _{uri: FileUri} :< TextDoc,
  241    atom_concat('file://', Path, FileUri),
  242    ( loaded_source(Path) ; assertz(loaded_source(Path)) ),
  243    check_errors_resp(FileUri, Resp).
  244handle_msg("textDocument/didChange", Msg, false) :-
  245    _{params: _{textDocument: TextDoc,
  246                contentChanges: Changes}} :< Msg,
  247    _{uri: Uri} :< TextDoc,
  248    atom_concat('file://', Path, Uri),
  249    handle_doc_changes(Path, Changes).
  250handle_msg("textDocument/didSave", Msg, Resp) :-
  251    _{params: Params} :< Msg,
  252    check_errors_resp(Params.textDocument.uri, Resp).
  253handle_msg("textDocument/didClose", Msg, false) :-
  254    _{params: _{textDocument: TextDoc}} :< Msg,
  255    _{uri: FileUri} :< TextDoc,
  256    atom_concat('file://', Path, FileUri),
  257    retractall(loaded_source(Path)).
  258handle_msg("initialized", Msg, false) :-
  259    debug(server, "initialized ~w", [Msg]).
  260handle_msg("$/cancelRequest", Msg, false) :-
  261    debug(server, "Cancel request Msg ~w", [Msg]).
  262handle_msg("exit", _Msg, false) :-
  263    debug(server, "recieved exit, shutting down", []),
  264    halt.
  265% wildcard
  266handle_msg(_, Msg, _{id: Id, error: _{code: -32603, message: "Unimplemented"}}) :-
  267    _{id: Id} :< Msg, !,
  268    debug(server, "unknown message ~w", [Msg]).
  269handle_msg(_, Msg, false) :-
  270    debug(server, "unknown notification ~w", [Msg]).
  271
  272check_errors_resp(FileUri, _{method: "textDocument/publishDiagnostics",
  273                             params: _{uri: FileUri, diagnostics: Errors}}) :-
  274    atom_concat('file://', Path, FileUri),
  275    check_errors(Path, Errors).
  276check_errors_resp(_, false) :-
  277    debug(server, "Failed checking errors", [])