Skip to content

Commit

Permalink
Initial draft for a server supporting code completion
Browse files Browse the repository at this point in the history
  • Loading branch information
robertoaloi committed Jun 13, 2018
1 parent d463550 commit ec208c5
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 16 deletions.
18 changes: 18 additions & 0 deletions .gitignore
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
13 changes: 1 addition & 12 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 50 additions & 4 deletions README.md
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/
6 changes: 6 additions & 0 deletions rebar.config
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"}
]}.
12 changes: 12 additions & 0 deletions rebar.lock
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">>}]}
].
19 changes: 19 additions & 0 deletions src/erlang_ls.app.src
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, []}
]}.
40 changes: 40 additions & 0 deletions src/erlang_ls_app.erl
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.
199 changes: 199 additions & 0 deletions src/erlang_ls_protocol.erl
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])).
Loading

0 comments on commit ec208c5

Please sign in to comment.