-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial draft for a server supporting code completion
- Loading branch information
1 parent
d463550
commit ec208c5
Showing
9 changed files
with
383 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"} | ||
]}. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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">>}]} | ||
]. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, []} | ||
]}. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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])). |
Oops, something went wrong.