diff --git a/src/packs/lsp_server_metta/README.org b/src/packs/lsp_server_metta/README.org new file mode 100644 index 00000000000..1a3c4ee53f3 --- /dev/null +++ b/src/packs/lsp_server_metta/README.org @@ -0,0 +1,114 @@ +* MeTTaLog Language server + + **Currently a fork of prolog Language server. Will update this README later.** + +Still a work-in-progress -- please open an issue if you have any issues or feature requests!. + +Currently supports showing documentation on hover, go to definition, go to callers, listing defined symbols in the file, and showing a limited number of diagnostics. + +Only tested with SWI-Prolog, as it heavily uses its introspection facilities to do its stuff. +It should work with any relatively-recent version of SWI-Prolog, but for best results (for "find references" in particular), use a version with ~xref_called/5~ (8.1.5 or newer; past commit [[https://github.com/SWI-Prolog/swipl-devel/commit/303f6430de5c9d7e225d8eb6fb8bb8b59e7c5f8f][303f6430de5c]]). + +Installable as a pack like ~?- pack_install(lsp_server).~ + +* Emacs + +** [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]]: + +#+begin_src emacs-lisp +(lsp-register-client + (make-lsp-client + :new-connection + (lsp-stdio-connection (list "swipl" + "-g" "use_module(library(lsp_server))." + "-g" "lsp_server:main" + "-t" "halt" + "--" "stdio")) + :major-modes '(prolog-mode) + :priority 1 + :multi-root t + :server-id 'prolog-ls)) +#+end_src + +* Vim/Neovim + +** [[https://github.com/autozimu/LanguageClient-neovim][LanguageClient]]: + +#+begin_src viml +let g:LanguageClient_serverCommands = { +\ 'prolog': ['swipl', +\ '-g', 'use_module(library(lsp_server)).', +\ '-g', 'lsp_server:main', +\ '-t', 'halt', +\ '--', 'stdio'] +\ } +#+end_src + +* Neovim + +** [[https://github.com/neoclide/coc.nvim][CoC]] + +Put the following in ~coc-settings.json~ (which you can access by using the command ~:CocConfig~). + +#+begin_src json +{"languageserver": { + "prolog-lsp": { + "command": "swipl", + "args": ["-g", "use_module(library(lsp_server)).", + "-g", "lsp_server:main", + "-t", "halt", + "--", "stdio" + ], + "filetypes": ["prolog"] + }} +} +#+end_src + +** Native LSP (for Neovim >= 0.5) + +Install the [[https://github.com/neovim/nvim-lspconfig][neovim/nvim-lspconfig]] package + +Put the following in ~$XDG_CONFIG_DIR/nvim/lua/lspconfig/prolog_lsp.lua~: + +#+begin_src lua +local configs = require 'lspconfig/configs' +local util = require 'lspconfig/util' + +configs.prolog_lsp = { + default_config = { + cmd = {"swipl", + "-g", "use_module(library(lsp_server)).", + "-g", "lsp_server:main", + "-t", "halt", + "--", "stdio"}; + filetypes = {"prolog"}; + root_dir = util.root_pattern("pack.pl"); + }; + docs = { + description = [[ + https://github.com/jamesnvc/prolog_lsp + + Prolog Language Server + ]]; + } +} +-- vim:et ts=2 sw=2 +#+end_src + +Then add the following to ~init.vim~: + +#+begin_src viml +lua << EOF +require('lspconfig/prolog_lsp') +require('lspconfig').prolog_lsp.setup{} +EOF +#+end_src + +* VSCode + + - download the latest ~.vsix~ file from the [[https://github.com/jamesnvc/lsp_server/releases][releases page]] + - clone this repo and copy/symlink the ~vscode/~ directory to ~~/.vscode/extensions/~ + - clone and build the ~.vsix~ file yourself by the follwing steps: + 1. install ~vsce~ (~npm install -g vsce~) + 2. run ~vsce publish~ from the ~vscode/~ directory + 3. add the resulting ~.vsix~ to VSCode. diff --git a/src/packs/lsp_server_metta/pack.pl b/src/packs/lsp_server_metta/pack.pl new file mode 100644 index 00000000000..33c3c52bf5e --- /dev/null +++ b/src/packs/lsp_server_metta/pack.pl @@ -0,0 +1,6 @@ +name(lsp_server_metta). +title('A MeTTa LSP Server'). +version('0.0.2'). +author('Roy Ward', 'roy@orange-kiwi.com'). +home('https://github.com/trueagi-io/metta-wam'). +provides(lsp_server_metta). diff --git a/src/packs/lsp_server_metta/prolog/lsp_metta_changes.pl b/src/packs/lsp_server_metta/prolog/lsp_metta_changes.pl new file mode 100644 index 00000000000..940dc7e2fad --- /dev/null +++ b/src/packs/lsp_server_metta/prolog/lsp_metta_changes.pl @@ -0,0 +1,76 @@ +:- module(lsp_metta_changes, [handle_doc_changes/2, + doc_text_fallback/2, + doc_text/2]). +/** LSP changes + Module for tracking edits to the source, in order to be able to act on + the code as it is in the editor buffer, before saving. + + @author James Cash +*/ + +:- use_module(library(readutil), [read_file_to_codes/3]). + +:- dynamic doc_text/2. + +%! handle_doc_changes(+File:atom, +Changes:list) is det. +% +% Track =Changes= to the file =File=. + +handle_doc_changes(_,_). + +handle_doc_changes(_, []) :- !. +handle_doc_changes(Path, [Change|Changes]) :- + handle_doc_change(Path, Change), + handle_doc_changes(Path, Changes). + +handle_doc_change(Path, Change) :- + _{range: _{start: _{line: StartLine, character: StartChar}, + end: _{line: _EndLine0, character: _EndChar}}, + rangeLength: ReplaceLen, text: Text} :< Change, + !, + atom_codes(Text, ChangeCodes), + doc_text_fallback(Path, OrigCodes), + replace_codes(OrigCodes, StartLine, StartChar, ReplaceLen, ChangeCodes, + NewText), + retractall(doc_text(Path, _)), + assertz(doc_text(Path, NewText)). +handle_doc_change(Path, Change) :- + retractall(doc_text(Path, _)), + atom_codes(Change.text, TextCodes), + assertz(doc_text(Path, TextCodes)). + +%! doc_text_fallback(+Path:atom, -Text:text) is det. +% +% Get the contents of the file at =Path=, either with the edits we've +% been tracking in memory, or from the file on disc if no edits have +% occured. +doc_text_fallback(Path, Text) :- + doc_text(Path, Text), !. +doc_text_fallback(Path, Text) :- + read_file_to_codes(Path, Text, []), + assertz(doc_text(Path, Text)). + +%! replace_codes(Text, StartLine, StartChar, ReplaceLen, ReplaceText, -NewText) is det. +replace_codes(Text, StartLine, StartChar, ReplaceLen, ReplaceText, NewText) :- + phrase(replace(StartLine, StartChar, ReplaceLen, ReplaceText), + Text, + NewText). + +replace(0, 0, 0, NewText), NewText --> !, []. +replace(0, 0, Skip, NewText) --> + !, skip(Skip), + replace(0, 0, 0, NewText). +replace(0, Chars, Skip, NewText), Take --> + { length(Take, Chars) }, + Take, !, + replace(0, 0, Skip, NewText). +replace(Lines1, Chars, Skip, NewText), Line --> + line(Line), !, + { succ(Lines0, Lines1) }, + replace(Lines0, Chars, Skip, NewText). + +skip(0) --> !, []. +skip(N) --> [_], { succ(N0, N) }, skip(N0). + +line([0'\n]) --> [0'\n], !. +line([C|Cs]) --> [C], line(Cs). diff --git a/src/packs/lsp_server_metta/prolog/lsp_metta_checking.pl b/src/packs/lsp_server_metta/prolog/lsp_metta_checking.pl new file mode 100644 index 00000000000..6f0e2d78303 --- /dev/null +++ b/src/packs/lsp_server_metta/prolog/lsp_metta_checking.pl @@ -0,0 +1,26 @@ +:- module(lsp_metta_checking, [check_errors/2]). +/** LSP Checking + Module for checking Prolog source files for errors and warnings. + @author Roy Ward +*/ +% :- use_module(library(apply_macros)). +% :- use_module(library(assoc), [list_to_assoc/2, +% get_assoc/3]). +% :- use_module(library(apply), [maplist/3]). +% :- use_module(library(debug), [debug/3]). +% :- use_module(library(lists), [member/2]). +% :- use_module(library(prolog_xref), [xref_clean/1, xref_source/1]). +% :- use_module(lsp_metta_utils, [clause_variable_positions/3]). +% +% :- dynamic message_hook/3. +% :- multifile message_hook/3. +% +% %! check_errors(+Path:atom, -Errors:List) is det. +% % +% % =Errors= is a list of the errors in the file given by =Path=. +% % This predicate changes the =user:message_hook/3= hook. + +% will do some real error checking later +check_errors(_,[]). + + diff --git a/src/packs/lsp_server_metta/prolog/lsp_metta_colours.pl b/src/packs/lsp_server_metta/prolog/lsp_metta_colours.pl new file mode 100644 index 00000000000..ee0b2d726e6 --- /dev/null +++ b/src/packs/lsp_server_metta/prolog/lsp_metta_colours.pl @@ -0,0 +1,41 @@ +:- module(lsp_metta_colours, [ + token_types/1, + token_modifiers/1]). + +% these are Prolog token types/modifiers. Need to change to meTTa. + +token_types([namespace, + type, + class, + enum, + interface, + struct, + typeParameter, + parameter, + variable, + property, + enumMember, + event, + function, + member, + macro, + keyword, + modifier, + comment, + string, + number, + regexp, + operator + ]). +token_modifiers([declaration, + definition, + readonly, + static, + deprecated, + abstract, + async, + modification, + documentation, + defaultLibrary + ]). + diff --git a/src/packs/lsp_server_metta/prolog/lsp_metta_completion.pl b/src/packs/lsp_server_metta/prolog/lsp_metta_completion.pl new file mode 100644 index 00000000000..64b471cff8a --- /dev/null +++ b/src/packs/lsp_server_metta/prolog/lsp_metta_completion.pl @@ -0,0 +1,68 @@ +% :- module(lsp_metta_completion, [completions_at/3]). +% /** LSP Completion +% +% This module implements code completion, based on defined predicates in +% the file & imports. +% +% Uses =lsp_metta_changes= in order to see the state of the buffer being edited. +% +% @see lsp_metta_changes:doc_text_fallback/2 +% +% @author James Cash +% */ +% +% :- use_module(library(apply), [maplist/3]). +% :- use_module(library(lists), [numlist/3]). +% :- use_module(library(prolog_xref), [xref_defined/3, xref_source/2]). +% :- use_module(library(yall)). +% :- use_module(lsp_metta_utils, [linechar_offset/3]). +% :- use_module(lsp_metta_changes, [doc_text_fallback/2]). +% +% part_of_prefix(Code) :- code_type(Code, prolog_var_start). +% part_of_prefix(Code) :- code_type(Code, prolog_atom_start). +% part_of_prefix(Code) :- code_type(Code, prolog_identifier_continue). +% +% get_prefix_codes(Stream, Offset, Codes) :- +% get_prefix_codes(Stream, Offset, [], Codes). +% +% get_prefix_codes(Stream, Offset0, Codes0, Codes) :- +% peek_code(Stream, Code), +% part_of_prefix(Code), !, +% succ(Offset1, Offset0), +% seek(Stream, Offset1, bof, Offset), +% get_prefix_codes(Stream, Offset, [Code|Codes0], Codes). +% get_prefix_codes(_, _, Codes, Codes). +% +% prefix_at(File, Position, Prefix) :- +% doc_text_fallback(File, DocCodes), +% setup_call_cleanup( +% open_string(DocCodes, Stream), +% ( linechar_offset(Stream, Position, _), +% seek(Stream, -1, current, Offset), +% get_prefix_codes(Stream, Offset, PrefixCodes), +% string_codes(Prefix, PrefixCodes) ), +% close(Stream) +% ). +% +% completions_at(File, Position, Completions) :- +% prefix_at(File, Position, Prefix), +% xref_source(File, [silent(true)]), +% findall( +% Result, +% ( xref_defined(File, Goal, _), +% functor(Goal, Name, Arity), +% atom_concat(Prefix, _, Name), +% args_str(Arity, Args), +% format(string(Func), "~w(~w)$0", [Name, Args]), +% format(string(Label), "~w/~w", [Name, Arity]), +% Result = _{label: Label, +% insertText: Func, +% insertTextFormat: 2}), +% Completions +% ). +% +% args_str(Arity, Str) :- +% numlist(1, Arity, Args), +% maplist([A, S]>>format(string(S), "${~w:_}", [A]), +% Args, ArgStrs), +% atomic_list_concat(ArgStrs, ', ', Str). diff --git a/src/packs/lsp_server_metta/prolog/lsp_metta_parser.pl b/src/packs/lsp_server_metta/prolog/lsp_metta_parser.pl new file mode 100644 index 00000000000..d921d313f6f --- /dev/null +++ b/src/packs/lsp_server_metta/prolog/lsp_metta_parser.pl @@ -0,0 +1,35 @@ +:- module(lsp_metta_parser, [lsp_metta_request//1]). +/** LSP Parser + +Module for parsing the body & headers from an LSP client. + +@author James Cash +*/ + +:- use_module(library(assoc), [list_to_assoc/2, get_assoc/3]). +:- use_module(library(codesio), [open_codes_stream/2]). +:- use_module(library(dcg/basics), [string_without//2]). +:- use_module(library(http/json), [json_read_dict/3]). + +header(Key-Value) --> + string_without(":", KeyC), ": ", string_without("\r", ValueC), + { string_codes(Key, KeyC), string_codes(Value, ValueC) }. + +headers([Header]) --> + header(Header), "\r\n\r\n", !. +headers([Header|Headers]) --> + header(Header), "\r\n", + headers(Headers). + +json_chars(0, []) --> []. +json_chars(N, [C|Cs]) --> [C], { succ(Nn, N) }, json_chars(Nn, Cs). + +lsp_metta_request(_{headers: Headers, body: Body}) --> + headers(HeadersList), + { list_to_assoc(HeadersList, Headers), + get_assoc("Content-Length", Headers, LengthS), + number_string(Length, LengthS) }, + json_chars(Length, JsonCodes), + { ground(JsonCodes), + open_codes_stream(JsonCodes, JsonStream), + json_read_dict(JsonStream, Body, []) }. diff --git a/src/packs/lsp_server_metta/prolog/lsp_metta_utils.pl b/src/packs/lsp_server_metta/prolog/lsp_metta_utils.pl new file mode 100644 index 00000000000..748dd950d8d --- /dev/null +++ b/src/packs/lsp_server_metta/prolog/lsp_metta_utils.pl @@ -0,0 +1,28 @@ +:- module(lsp_metta_utils, [ +% called_at/4, +% defined_at/3, +% name_callable/2, +% relative_ref_location/4, +% clause_variable_positions/3, +% seek_to_line/2, +% linechar_offset/3, +% clause_in_file_at_position/3, + help_at_position/4 + ]). +% /** LSP Utils +% +% Module with a bunch of helper predicates for looking through prolog +% source and stuff. +% +% @author James Cash +% */ +%! help_at_position(+Path:atom, +Line:integer, +Char:integer, -Help:string) is det. +% +% =Help= is the documentation for the term under the cursor at line +% =Line=, character =Char= in the file =Path=. + +help_at_position(_Path, _Line1, _Char0, "blah blah blah") :- !. % clause_in_file_at_position(Clause, Path, line_char(Line1, Char0)), !. +% help_at_position(Path, Line1, Char0, S) :- +% clause_in_file_at_position(Clause, Path, line_char(Line1, Char0)), +% predicate_help(Path, Clause, S0), +% format_help(S0, S). diff --git a/src/packs/lsp_server_metta/prolog/lsp_server_metta.pl b/src/packs/lsp_server_metta/prolog/lsp_server_metta.pl new file mode 100644 index 00000000000..079fa368a3a --- /dev/null +++ b/src/packs/lsp_server_metta/prolog/lsp_server_metta.pl @@ -0,0 +1,316 @@ +:- module(lsp_server_metta, [main/0]). +/** LSP Server + +The main entry point for the Language Server implementation. + +@author James Cash +*/ + +:- use_module(library(apply), [maplist/2]). +:- use_module(library(debug), [debug/3, debug/1]). +:- use_module(library(http/json), [atom_json_dict/3]). +:- use_module(library(prolog_xref)). +:- use_module(library(prolog_source), [directory_source_files/3]). +:- use_module(library(utf8), [utf8_codes//1]). +:- use_module(library(yall)). + +:- use_module(lsp_metta_utils). +:- use_module(lsp_metta_checking, [check_errors/2]). +:- use_module(lsp_metta_parser, [lsp_metta_request//1]). +:- use_module(lsp_metta_changes, [handle_doc_changes/2]). +:- use_module(lsp_metta_completion, [completions_at/3]). +:- use_module(lsp_metta_colours, [ +% file_colours/2, +% file_range_colours/4, + token_types/1, + token_modifiers/1]). + +main :- + set_prolog_flag(debug_on_error, false), + set_prolog_flag(report_error, true), + set_prolog_flag(toplevel_prompt, ''), + current_prolog_flag(argv, Args), + debug(server), + debug(server(high)), + start(Args). + +start([stdio]) :- !, + debug(server, "Starting stdio client", []), + stdio_server. +start(Args) :- + debug(server, "Unknown args ~w", [Args]). + +% stdio server + +stdio_server :- + current_input(In), + set_stream(In, buffer(full)), + set_stream(In, newline(posix)), + set_stream(In, tty(false)), + set_stream(In, representation_errors(error)), + % handling UTF decoding in JSON parsing, but doing the auto-translation + % causes Content-Length to be incorrect + set_stream(In, encoding(octet)), + current_output(Out), + set_stream(Out, encoding(utf8)), + stdio_handler(A-A, In). + +% [TODO] add multithreading? Guess that will also need a message queue +% to write to stdout +stdio_handler(Extra-ExtraTail, In) :- + wait_for_input([In], _, infinite), + fill_buffer(In), + read_pending_codes(In, ReadCodes, Tail), + ( Tail == [] + -> true + ; ( current_output(Out), + ExtraTail = ReadCodes, + handle_requests(Out, Extra, Remainder), + stdio_handler(Remainder-Tail, In) ) + ). + +handle_requests(Out, In, Tail) :- + handle_request(Out, In, Rest), !, + ( var(Rest) + -> Tail = Rest + ; handle_requests(Out, Rest, Tail) ). +handle_requests(_, T, T). + +% general handling stuff + +send_message(Stream, Msg) :- + put_dict(jsonrpc, Msg, "2.0", VersionedMsg), + atom_json_dict(JsonCodes, VersionedMsg, [as(codes), width(0)]), + phrase(utf8_codes(JsonCodes), UTF8Codes), + length(UTF8Codes, ContentLength), + format(Stream, "Content-Length: ~w\r\n\r\n~s", [ContentLength, JsonCodes]), + flush_output(Stream). + +handle_request(OutStream, Input, Rest) :- + phrase(lsp_metta_request(Req), Input, Rest), + debug(server(high), "Request ~w", [Req.body]), + catch( + ( handle_msg(Req.body.method, Req.body, Resp), + debug(server(high), "response ~w", [Resp]), + ( is_dict(Resp) -> send_message(OutStream, Resp) ; true ) ), + Err, + ( debug(server, "error handling msg ~w", [Err]), + get_dict(id, Req.body, Id), + send_message(OutStream, _{id: Id, + error: _{code: -32001, + message: "server error"}}) + )). + +% Handling messages + +server_capabilities( + _{textDocumentSync: _{openClose: true, + change: 2, %incremental + save: _{includeText: false}, + willSave: false, + willSaveWaitUntil: false %??? + }, + hoverProvider: true, + completionProvider: _{}, + definitionProvider: true, + declarationProvider: true, + implementationProvider: true, + referencesProvider: true, + documentHighlightProvider: false, + documentSymbolProvider: true, + workspaceSymbolProvider: true, + codeActionProvider: false, + %% codeLensProvider: false, + documentFormattingProvider:false, + %% documentOnTypeFormattingProvider: false, + renameProvider: false, + % documentLinkProvider: false, + % colorProvider: true, + foldingRangeProvider: false, + executeCommandProvider: _{commands: ["query", "assert"]}, + semanticTokensProvider: _{legend: _{tokenTypes: TokenTypes, + tokenModifiers: TokenModifiers}, + range: true, + % [TODO] implement deltas + full: _{delta: false}}, + workspace: _{workspaceFolders: _{supported: true, + changeNotifications: true}} + } +) :- + token_types(TokenTypes), + token_modifiers(TokenModifiers). + +:- dynamic loaded_source/1. + +% messages (with a response) +handle_msg("initialize", Msg, + _{id: Id, result: _{capabilities: ServerCapabilities} }) :- + _{id: Id, params: Params} :< Msg, !, + ( Params.rootUri \== null + -> ( atom_concat('file://', RootPath, Params.rootUri), + directory_source_files(RootPath, Files, [recursive(true)]), + maplist([F]>>assert(loaded_source(F)), Files) ) + ; true ), + server_capabilities(ServerCapabilities). +handle_msg("shutdown", Msg, _{id: Id, result: null}) :- + _{id: Id} :< Msg, + debug(server, "recieved shutdown message", []). + +% CALL: textDocument/hover +% IN: params:{position:{character:11,line:56},textDocument:{uri:file://}}} +% OUT: {id:21,result:{contents:{kind:plaintext,value:}}} +handle_msg("textDocument/hover", Msg, _{id: Id, result: Response}) :- + _{params: _{position: _{character: Char0, line: Line0}, + textDocument: _{uri: Doc}}, id: Id} :< Msg, + atom_concat('file://', Path, Doc), + Line1 is Line0 + 1, + ( help_at_position(Path, Line1, Char0, Help) + -> Response = _{contents: _{kind: plaintext, value: Help}} + ; Response = null). + +% CALL: textDocument/documentSymbol +% IN: params:{textDocument:{uri:file://}} +% OUT: {id:1,result:[ +% {kind:12,location:{range:{end:{character:0,line:37},start:{character:1,line:35}},uri:file://},name:called_at/4}, +% {kind:12,location:{range:{end:{character:0,line:66},start:{character:1,line:64}},uri:file://},},name:defined_at/3} ... ]} +handle_msg("textDocument/documentSymbol", Msg, _{id: Id, result: []}) :- + _{id: Id, params: _{textDocument: _{uri: Doc}}} :< Msg. +% handle_msg("textDocument/documentSymbol", Msg, _{id: Id, result: Symbols}) :- +% _{id: Id, params: _{textDocument: _{uri: Doc}}} :< Msg, +% atom_concat('file://', Path, Doc), !, +% xref_source(Path), +% findall( +% Symbol, +% ( xref_defined(Path, Goal, local(Line)), +% succ(Line, NextLine), +% succ(Line0, Line), +% functor(Goal, Name, Arity), +% format(string(GoalName), "~w/~w", [Name, Arity]), +% Symbol = _{name: GoalName, +% kind: 12, % function +% location: +% _{uri: Doc, +% range: _{start: _{line: Line0, character: 1}, +% end: _{line: NextLine, character: 0}}}} +% ), +% Symbols). + +% CALL: method:textDocument/definition +% IN: params:{position:{character:55,line:174},textDocument:{uri:file://}}} +% OUT: {id:42,result:null} % need to find a better example +% handle_msg("textDocument/definition", Msg, _{id: Id, result: Location}) :- +% _{id: Id, params: Params} :< Msg, +% _{textDocument: _{uri: Doc}, +% position: _{line: Line0, character: Char0}} :< Params, +% atom_concat('file://', Path, Doc), +% succ(Line0, Line1), +% clause_in_file_at_position(Name/Arity, Path, line_char(Line1, Char0)), +% defined_at(Path, Name/Arity, Location). +% handle_msg("textDocument/definition", Msg, _{id: Msg.id, result: null}) :- !. + +% handle_msg("textDocument/references", Msg, _{id: Id, result: Locations}) :- +% _{id: Id, params: Params} :< Msg, +% _{textDocument: _{uri: Uri}, +% position: _{line: Line0, character: Char0}} :< Params, +% atom_concat('file://', Path, Uri), +% succ(Line0, Line1), +% clause_in_file_at_position(Clause, Path, line_char(Line1, Char0)), +% findall( +% Location, +% ( loaded_source(Doc), +% atom_concat('file://', Doc, DocUri), +% called_at(Doc, Clause, Caller, Loc), +% relative_ref_location(DocUri, Caller, Loc, Location) +% ), +% Locations), !. +% handle_msg("textDocument/references", Msg, _{id: Msg.id, result: null}) :- !. + +% handle_msg("textDocument/completion", Msg, _{id: Id, result: Completions}) :- +% _{id: Id, params: Params} :< Msg, +% _{textDocument: _{uri: Uri}, +% position: _{line: Line0, character: Char0}} :< Params, +% atom_concat('file://', Path, Uri), +% succ(Line0, Line1), +% completions_at(Path, line_char(Line1, Char0), Completions). + +handle_msg("textDocument/semanticTokens", Msg, Response) :- + handle_msg("textDocument/semanticTokens/full", Msg, Response). +% handle_msg("textDocument/semanticTokens/full", Msg, +% _{id: Id, result: _{data: Highlights}}) :- +% _{id: Id, params: Params} :< Msg, +% _{textDocument: _{uri: Uri}} :< Params, +% atom_concat('file://', Path, Uri), !, +% xref_source(Path), +% file_colours(Path, Highlights). + +% handle_msg("textDocument/semanticTokens/range", Msg, +% _{id: Id, result: _{data: Highlights}}) :- +% _{id: Id, params: Params} :< Msg, +% _{textDocument: _{uri: Uri}, range: Range} :< Params, +% _{start: _{line: StartLine0, character: StartChar}, +% end: _{line: EndLine0, character: EndChar}} :< Range, +% atom_concat('file://', Path, Uri), !, +% succ(StartLine0, StartLine), succ(EndLine0, EndLine), +% xref_source(Path), +% file_range_colours(Path, +% line_char(StartLine, StartChar), +% line_char(EndLine, EndChar), +% Highlights). + +% notifications (no response) + +% CALL: textDocument/didOpen +% IN: params:{textDocument:{languageId:prolog,text:,uri:file://,version:1}} +% OUT: {method:textDocument/publishDiagnostics,params:{diagnostics:[ +% {message:Singleton variable Offset,range:{end:{character:21,line:319},start:{character:15,line:319}},severity:2,source:prolog_xref}, +% {message:Singleton variable SubPos,range:{end:{character:29,line:319},start:{character:23,line:319}},severity:2,source:prolog_xref} ... ] +% ,uri:file://}} +handle_msg("textDocument/didOpen", Msg, Resp) :- + _{params: _{textDocument: TextDoc}} :< Msg, + _{uri: FileUri} :< TextDoc, + atom_concat('file://', Path, FileUri), + ( loaded_source(Path) ; assertz(loaded_source(Path)) ), + check_errors_resp(FileUri, Resp). + +handle_msg("textDocument/didChange", Msg, false) :- + _{params: _{textDocument: TextDoc, + contentChanges: Changes}} :< Msg, + _{uri: Uri} :< TextDoc, + atom_concat('file://', Path, Uri), + handle_doc_changes(Path, Changes). + +handle_msg("textDocument/didSave", Msg, Resp) :- + _{params: Params} :< Msg, + check_errors_resp(Params.textDocument.uri, Resp). + +handle_msg("textDocument/didClose", Msg, false) :- + _{params: _{textDocument: TextDoc}} :< Msg, + _{uri: FileUri} :< TextDoc, + atom_concat('file://', Path, FileUri), + retractall(loaded_source(Path)). + +handle_msg("initialized", Msg, false) :- + debug(server, "initialized ~w", [Msg]). + +handle_msg("$/setTrace", _Msg, false). + +handle_msg("$/cancelRequest", Msg, false) :- + debug(server, "Cancel request Msg ~w", [Msg]). + +handle_msg("exit", _Msg, false) :- + debug(server, "recieved exit, shutting down", []), + halt. + +% wildcard +handle_msg(_, Msg, _{id: Id, error: _{code: -32603, message: "Unimplemented"}}) :- + _{id: Id} :< Msg, !, + debug(server, "unknown message ~w", [Msg]). +handle_msg(_, Msg, false) :- + debug(server, "unknown notification ~w", [Msg]). + +check_errors_resp(FileUri, _{method: "textDocument/publishDiagnostics", + params: _{uri: FileUri, diagnostics: Errors}}) :- + atom_concat('file://', Path, FileUri), + check_errors(Path, Errors). +check_errors_resp(_, false) :- + debug(server, "Failed checking errors", []). diff --git a/src/packs/lsp_server_metta/vscode/README.md b/src/packs/lsp_server_metta/vscode/README.md new file mode 100644 index 00000000000..e6ca5c8c6ec --- /dev/null +++ b/src/packs/lsp_server_metta/vscode/README.md @@ -0,0 +1,3 @@ +# MeTTa LSP + +VSCode client for MeTTa LSP server diff --git a/src/packs/lsp_server_metta/vscode/extension.js b/src/packs/lsp_server_metta/vscode/extension.js new file mode 100644 index 00000000000..677e22dc3f2 --- /dev/null +++ b/src/packs/lsp_server_metta/vscode/extension.js @@ -0,0 +1,42 @@ +const vscode = require('vscode'); +const lsp = require('vscode-languageclient'); + +function activate(context) { + + let serverOptions = { + run: {command: "swipl", + args: ["-g", "use_module(library(lsp_server_metta)).", + "-g", "lsp_server_metta:main", + "-t", "halt", + "--", "stdio"]}, + debug: {command: "swipl", + args: ["-g", "use_module(library(syslog)).", + "-g", "openlog(metta_lsp, [], user).", + "-g", "use_module(library(debug)).", + "-g", "debug(server(high)).", + "-g", "use_module(library(lsp_server_metta)).", + "-g", "lsp_server_metta:main", + "-t", "halt", + "--", "stdio"]} + }; + + let clientOptions = { + documentSelector: [{scheme: "file", language: "metta"}], + }; + + let client = new lsp.LanguageClient( + 'metta-lsp', + 'MeTTa Language Client', + serverOptions, + clientOptions + ); + + context.subscriptions.push(client.start()); +} +exports.activate = activate; + +function deactivate(context) { + if (!context.client) { return; } + context.client.stop(); +} +exports.deactivate = deactivate; diff --git a/src/packs/lsp_server_metta/vscode/metta.config.json b/src/packs/lsp_server_metta/vscode/metta.config.json new file mode 100644 index 00000000000..288c272d8a3 --- /dev/null +++ b/src/packs/lsp_server_metta/vscode/metta.config.json @@ -0,0 +1,24 @@ +{ + "comments": { + "lineComment": ";" + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ] +} diff --git a/src/packs/lsp_server_metta/vscode/package.json b/src/packs/lsp_server_metta/vscode/package.json new file mode 100644 index 00000000000..d05940ed0fd --- /dev/null +++ b/src/packs/lsp_server_metta/vscode/package.json @@ -0,0 +1,51 @@ +{ + "name": "metta-lsp", + "displayName": "metta-lsp", + "description": "A language server for MeTTa", + "author": "Roy Ward", + "license": "LGPL-3.0", + "publisher": "RoyWard", + "version": "0.0.2", + "categories": [ + "Other" + ], + "keywords": [ + "mettalog" + ], + "engines": { + "vscode": "^1.54.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/royward/lsp_server_metta" + }, + "activationEvents": [ + "onLanguage:metta" + ], + "main": "./extension", + "contributes": { + "languages": [ + { + "id": "metta", + "aliases": [ + "metta", + "mettalog", + "MeTTa", + "MeTTaLog", + "meTTa", + "meTTaLog" + ], + "configuration": "./metta.config.json", + "extensions": [ + ".metta" + ] + } + ] + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "vscode": "^1.1.6" + } +}