diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..40ca6526c --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.rebar3 +_* +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +_build +.idea +*.iml +rebar3.crashdump diff --git a/LICENSE b/LICENSE index 261eeb9e9..445adf0ad 100644 --- a/LICENSE +++ b/LICENSE @@ -175,18 +175,7 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2018, Roberto Aloi. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index e01a01329..a2df71509 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,51 @@ -# erlang-ls -The Erlang Language Server Protocol Implementation +erlang_ls +===== -# Get in Touch -This project is still in a very early stage. To get in touch, feel free to join the #language-server channel in the Erlanger Slack. +An Erlang server using Microsoft's Language Server Protocol 3.0. + +Get in Touch +---- + +This project is still in a very early stage. To get +in touch, feel free to join the #language-server channel in the +Erlanger Slack. + +Dev Quickstart +----- + + $ rebar3 shell + application:ensure_all_started(erlang_ls). + Opts = [{msgs, 1000}, {time, 99999}, {print_file, "/tmp/redbug"}], + redbug:start("erlang_ls_protocol->return", Opts). + +Emacs Setup +----- + + ;; Language Server Protocol Tests + (require 'lsp-mode) + + ;; Enable code completion + (require 'company-lsp) + (push 'company-lsp company-backends) + + ;; Connect to an already started language server + (lsp-define-tcp-client + lsp-erlang-mode + "erlang" + (lambda () default-directory) + '("/usr/local/opt/coreutils/libexec/gnubin/false") + "localhost" + 9000) + + (add-hook 'erlang-mode #'lsp-erlang-mode-enable) + +Manual enable the server for a buffer: + +M-x company-mode +M-x company-lsp +M-x lsp-erlang-mode-enable + +References +----- + +https://microsoft.github.io/language-server-protocol/ diff --git a/rebar.config b/rebar.config new file mode 100644 index 000000000..e24b83e8c --- /dev/null +++ b/rebar.config @@ -0,0 +1,6 @@ +{erl_opts, [debug_info]}. +{deps, [ {ranch, "1.5.0"} + , {jsx, "2.9.0"} + , {cowlib, "2.3.0"} + , {redbug, "1.2.0"} + ]}. diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 000000000..c292e1f93 --- /dev/null +++ b/rebar.lock @@ -0,0 +1,12 @@ +{"1.1.0", +[{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.3.0">>},0}, + {<<"jsx">>,{pkg,<<"jsx">>,<<"2.9.0">>},0}, + {<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},0}, + {<<"redbug">>,{pkg,<<"redbug">>,<<"1.2.0">>},0}]}. +[ +{pkg_hash,[ + {<<"cowlib">>, <<"BBD58EF537904E4F7C1DD62E6AA8BC831C8183CE4EFA9BD1150164FE15BE4CAA">>}, + {<<"jsx">>, <<"D2F6E5F069C00266CAD52FB15D87C428579EA4D7D73A33669E12679E203329DD">>}, + {<<"ranch">>, <<"F04166F456790FEE2AC1AA05A02745CC75783C2BFB26D39FAF6AEFC9A3D3A58A">>}, + {<<"redbug">>, <<"75C09B306471CEE01AC4BDB727C33CA671A48D7AB7ACAF090C557F2E5BC02620">>}]} +]. diff --git a/src/erlang_ls.app.src b/src/erlang_ls.app.src new file mode 100644 index 000000000..68567db96 --- /dev/null +++ b/src/erlang_ls.app.src @@ -0,0 +1,19 @@ +{application, erlang_ls, + [{description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, { erlang_ls_app, []}}, + {applications, + [ kernel + , stdlib + , ranch + , jsx + , cowlib + ]}, + {env,[]}, + {modules, []}, + + {maintainers, []}, + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/src/erlang_ls_app.erl b/src/erlang_ls_app.erl new file mode 100644 index 000000000..4cad367d5 --- /dev/null +++ b/src/erlang_ls_app.erl @@ -0,0 +1,40 @@ +%%============================================================================== +%% Application Callback Module +%%============================================================================== +-module(erlang_ls_app). + +%%============================================================================== +%% Behaviours +%%============================================================================== +-behaviour(application). + +%%============================================================================== +%% Exports +%%============================================================================== + +%% Application Callbacks +-export([ start/2 + , stop/1 + ]). + +%%============================================================================== +%% Defines +%%============================================================================== +-define(DEFAULT_PORT, 9000). + +%%============================================================================== +%% Application Callbacks +%%============================================================================== +-spec start(normal, any()) -> {ok, pid()}. +start(_StartType, _StartArgs) -> + {ok, _} = ranch:start_listener( erlang_ls + , ranch_tcp + , [{port, ?DEFAULT_PORT}] + , erlang_ls_protocol + , [] + ), + erlang_ls_sup:start_link(). + +-spec stop(any()) -> ok. +stop(_State) -> + ok. diff --git a/src/erlang_ls_protocol.erl b/src/erlang_ls_protocol.erl new file mode 100644 index 000000000..56898ea41 --- /dev/null +++ b/src/erlang_ls_protocol.erl @@ -0,0 +1,199 @@ +%%============================================================================== +%% The Language Server Protocol Implementation +%%============================================================================== +-module(erlang_ls_protocol). + +%%============================================================================== +%% Behaviours +%%============================================================================== +-behaviour(ranch_protocol). +-behaviour(gen_statem). + +%%============================================================================== +%% Exports +%%============================================================================== + +%% ranch_protocol callbacks +-export([ start_link/4 ]). + +%% gen_statem callbacks +-export([ callback_mode/0 + , code_change/4 + , init/1 + , terminate/3 + ]). + +%% gen_statem state functions +-export([ connected/3 + ]). + +%%============================================================================== +%% Defines +%%============================================================================== +-define(JSONRPC_VSN, <<"2.0">>). + +%%============================================================================== +%% Record Definitions +%%============================================================================== +-record(state, { socket + , transport + , text + }). + +%%============================================================================== +%% Type Definitions +%%============================================================================== +-type state() :: #state{}. +-type request() :: #{}. +-type response() :: #{}. +-type result() :: #{}. + +%%============================================================================== +%% ranch_protocol callbacks +%%============================================================================== +-spec start_link(ramnch:ref(), any(), module(), any()) -> {ok, pid()}. +start_link(Ref, Socket, Transport, Opts) -> + {ok, proc_lib:spawn_link(?MODULE, init, [{Ref, Socket, Transport, Opts}])}. + +%%============================================================================== +%% gen_statem callbacks +%%============================================================================== +-spec callback_mode() -> state_functions. +callback_mode() -> + state_functions. + +-spec init({ranch:ref(), any(), module(), any()}) -> no_return(). +init({Ref, Socket, Transport, _Opts}) -> + ok = ranch:accept_ack(Ref), + ok = Transport:setopts(Socket, [{active, true}, {packet, 0}]), + gen_statem:enter_loop( ?MODULE + , [] + , connected + , #state{ socket = Socket + , transport = Transport + } + ). + +-spec code_change(any(), atom(), state(), any()) -> {ok, atom(), state()}. +code_change(_OldVsn, StateName, State, _Extra) -> + {ok, StateName, State}. + +-spec terminate(any(), atom(), state()) -> any(). +terminate(_Reason, _StateName, #state{ socket = Socket + , transport = Transport + }) -> + Transport:close(Socket), + ok. + +%%============================================================================== +%% gen_statem State Functions +%%============================================================================== +-spec connected(gen_statem:event_type(), any(), state()) -> any(). +connected(info, {tcp, Socket, Data}, #state{ socket = Socket } = State) -> + handle_request(Data, State); +connected(info, {tcp_closed, _Socket}, _State) -> + {stop, normal}; +connected(info, {tcp_error, _, Reason}, _State) -> + {stop, Reason}. + +%%============================================================================== +%% Internal Functions +%%============================================================================== +-spec handle_request(binary(), state()) -> any(). +handle_request(Data, #state{ socket = Socket + , transport = Transport + } = State) -> + Request = parse_data(Data), + Method = maps:get(<<"method">>, Request), + Params = maps:get(<<"params">>, Request), + case handle_request(Method, Params, State) of + {Result, NewState} -> + RequestId = maps:get(<<"id">>, Request), + Response = build_response(RequestId, Result), + reply(Socket, Transport, Response), + {keep_state, NewState}; + {NewState} -> + {keep_state, NewState} + end. + +-spec handle_request(binary(), map(), state()) -> + {result(), state()} | {state()}. +handle_request(<<"initialize">>, _Params, State) -> + Result = #{ capabilities => + #{ completionProvider => + #{ resolveProvider => false + , triggerCharacters => [<<":">>] + } + , textDocumentSync => 1 + } + }, + {Result, State}; +handle_request(<<"initialized">>, _, State) -> + {State}; +handle_request(<<"textDocument/didOpen">>, Params, State) -> + TextDocument = maps:get(<<"textDocument">>, Params), + Text = maps:get(<<"text">> , TextDocument), + {State#state{ text = Text }}; +handle_request(<<"textDocument/didChange">>, Params, State) -> + ContentChanges = maps:get(<<"contentChanges">>, Params), + case ContentChanges of + [] -> {State}; + [#{<<"text">> := Text}] -> {State#state{ text = Text }} + end; +handle_request(<<"textDocument/hover">>, _Params, State) -> + {null, State}; +handle_request(<<"textDocument/completion">>, Params, #state{ text = Text + } = State) -> + Position = maps:get(<<"position">> , Params), + Line = maps:get(<<"line">> , Position), + Character = maps:get(<<"character">>, Position), + Completions = get_completions(Text, Line, Character), + Result = [#{label => C} || C <- Completions], + {Result, State}; +handle_request(Method, _Params, State) -> + erlang:display({not_implemented, Method}), + {State}. + +-spec parse_data(binary()) -> request(). +parse_data(Data) -> + {_Headers, Body} = cow_http:parse_headers(Data), + jsx:decode(Body, [return_maps]). + +-spec build_response(integer(), result()) -> response(). +build_response(RequestId, Result) -> + #{ jsonrpc => ?JSONRPC_VSN + , result => Result + , id => RequestId + }. + +-spec reply(any(), module(), response()) -> ok. +reply(Socket, Transport, Response) -> + Body = jsx:encode(Response), + Headers = io_lib:format("Content-Length: ~p\r\n", [byte_size(Body)]), + Data = [Headers, "\r\n", Body], + Transport:send(Socket, Data). + +-spec get_completions(binary(), integer(), integer()) -> [binary()]. +get_completions(Text, Line, Character) -> + LineText = get_line_text(Text, Line), + LineBeforeChar = binary:part(LineText, {0, Character - 1}), + {ok, Tokens, _} = erl_scan:string(binary_to_list(LineBeforeChar)), + [H| _] = lists:reverse(Tokens), + Info = case H of + {atom, _, Atom} -> + try Atom:module_info(exports) + catch _:_ -> [] + end; + _ -> + [] + end, + [function_name_to_binary(M, A) || {M, A} <- Info]. + +-spec get_line_text(binary(), integer()) -> binary(). +get_line_text(Text, Line) -> + Lines = binary:split(Text, <<"\n">>, [global]), + lists:nth(Line + 1, Lines). + +-spec function_name_to_binary(module(), non_neg_integer()) -> binary(). +function_name_to_binary(Module, Arity) -> + list_to_binary(io_lib:format("~p/~p", [Module, Arity])). diff --git a/src/erlang_ls_sup.erl b/src/erlang_ls_sup.erl new file mode 100644 index 000000000..fd89faa59 --- /dev/null +++ b/src/erlang_ls_sup.erl @@ -0,0 +1,38 @@ +%%============================================================================== +%% Top Level Supervisor +%%============================================================================== +-module(erlang_ls_sup). + +%%============================================================================== +%% Behaviours +%%============================================================================== +-behaviour(supervisor). + +%%============================================================================== +%% Exports +%%============================================================================== + +%% API +-export([ start_link/0 ]). + +%% Supervisor Callbacks +-export([ init/1 ]). + +%%============================================================================== +%% Defines +%%============================================================================== +-define(SERVER, ?MODULE). + +%%============================================================================== +%% API +%%============================================================================== +-spec start_link() -> {ok, pid()}. +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +%%============================================================================== +%% Supervisor callbacks +%%============================================================================== +-spec init([]) -> {ok, {supervisor:sup_flags(), supervisor:child_spec()}}. +init([]) -> + {ok, { {one_for_all, 0, 1}, []} }.