diff --git a/include/erlang_ls.hrl b/include/erlang_ls.hrl index 99d4887da..5d01a53ea 100644 --- a/include/erlang_ls.hrl +++ b/include/erlang_ls.hrl @@ -139,14 +139,6 @@ , message := binary() }. -%%------------------------------------------------------------------------------ -%% Command -%%------------------------------------------------------------------------------ --type command() :: #{ title := binary() - , command := binary() - , arguments => [any()] - }. - %%------------------------------------------------------------------------------ %% Insert Text Format %%------------------------------------------------------------------------------ @@ -608,7 +600,7 @@ , kind => code_action_kind() , diagnostics => [diagnostic()] , edit => workspace_edit() - , command => command() + , command => els_command:command() }. %%------------------------------------------------------------------------------ diff --git a/src/els_code_action_provider.erl b/src/els_code_action_provider.erl index bc8acd364..b07eefef6 100644 --- a/src/els_code_action_provider.erl +++ b/src/els_code_action_provider.erl @@ -47,17 +47,15 @@ replace_lines_action(Uri, Title, Kind, Lines, Range) -> , <<"end">> := #{ <<"character">> := _EndCol , <<"line">> := EndLine } } = Range, - PrefixedCommand - = els_execute_command_provider:add_server_prefix(<<"replace-lines">>), #{ title => Title , kind => Kind , command => - els_protocol:command( Title - , PrefixedCommand - , [#{ uri => Uri - , lines => Lines - , from => StartLine - , to => EndLine }]) + els_command:make_command( Title + , <<"replace-lines">> + , [#{ uri => Uri + , lines => Lines + , from => StartLine + , to => EndLine }]) }. -spec make_code_action(uri(), diagnostic()) -> [map()]. diff --git a/src/els_code_lens.erl b/src/els_code_lens.erl new file mode 100644 index 000000000..53a1af23e --- /dev/null +++ b/src/els_code_lens.erl @@ -0,0 +1,86 @@ +%%============================================================================== +%% Code Lens: Behaviour and API +%%============================================================================== + +-module(els_code_lens). + +%%============================================================================== +%% Callback Functions +%%============================================================================== + +-callback command() -> els_command:command_id(). +-callback is_default() -> boolean(). +-callback lenses(els_dt_document:item()) -> [lens()]. + +%%============================================================================== +%% API +%%============================================================================== + +-export([ available_lenses/0 + , default_lenses/0 + , enabled_lenses/0 + , lenses/2 + ]). + +%%============================================================================== +%% Constructors +%%============================================================================== + +-export([ make_lens/3 ]). + +%%============================================================================== +%% Includes +%%============================================================================== + +-include("erlang_ls.hrl"). + +%%============================================================================== +%% Type Definitions +%%============================================================================== + +-type lens() :: #{ range := range() + , command => els_command:command() + , data => any() + }. +-type lens_id() :: binary(). +-export_type([ lens/0 + , lens_id/0 + ]). + +%%============================================================================== +%% API +%%============================================================================== + +-spec available_lenses() -> [lens_id()]. +available_lenses() -> + [<<"server-info">>]. + +-spec default_lenses() -> [lens_id()]. +default_lenses() -> + [Id || Id <- available_lenses(), (cb_module(Id)):is_default()]. + +-spec enabled_lenses() -> [lens_id()]. +enabled_lenses() -> + els_config:get(code_lenses). + +-spec lenses(lens_id(), els_dt_document:item()) -> [lens()]. +lenses(Id, Document) -> + CbModule = cb_module(Id), + CbModule:lenses(Document). + +%%============================================================================== +%% Constructors +%%============================================================================== + +-spec make_lens(range(), els_command:command(), any()) -> lens(). +make_lens(Range, Command, Data) -> + #{ range => Range + , command => Command + , data => Data + }. + +%% @doc Return the callback module for a given Code Lens Identifier +-spec cb_module(els_code_lens:lens_id()) -> module(). +cb_module(Id0) -> + Id = re:replace(Id0, "-", "_", [global, {return, binary}]), + binary_to_atom(<<"els_code_lens_", Id/binary>>, utf8). diff --git a/src/els_code_lens_provider.erl b/src/els_code_lens_provider.erl index fcbfd68ff..04792d5ff 100644 --- a/src/els_code_lens_provider.erl +++ b/src/els_code_lens_provider.erl @@ -30,27 +30,9 @@ handle_request({document_codelens, Params}, State) -> %%============================================================================== %% Internal Functions %%============================================================================== --spec lenses(uri()) -> [map()]. +-spec lenses(uri()) -> [els_code_lens:lens()]. lenses(Uri) -> - {ok, _Document} = els_utils:lookup_document(Uri), - Command = els_execute_command_provider:add_server_prefix(<<"info">>), - Root = filename:basename(els_uri:path(els_config:get(root_uri))), - Lenses = [#{ range => one_line_range(1) - , command => - make_command( <<"Erlang LS (in ", Root/binary, ") info">> - , Command - , [#{ uri => Uri }]) - }], - Lenses. - --spec one_line_range(non_neg_integer()) -> range(). -one_line_range(Line) -> - Range = #{from => {Line, 1}, to => {Line + 1, 1}}, - els_protocol:range(Range). - --spec make_command(binary(), binary(), [any()]) -> command(). -make_command(Title, Command, Args) -> - #{ title => Title - , command => Command - , arguments => Args - }. + {ok, Document} = els_utils:lookup_document(Uri), + lists:flatten( + [els_code_lens:lenses(Id, Document) || + Id <- els_code_lens:enabled_lenses()]). diff --git a/src/els_code_lens_server_info.erl b/src/els_code_lens_server_info.erl new file mode 100644 index 000000000..126482b2f --- /dev/null +++ b/src/els_code_lens_server_info.erl @@ -0,0 +1,29 @@ +%%============================================================================== +%% Code Lens: server_info +%%============================================================================== + +-module(els_code_lens_server_info). + +-export([ command/0 + , is_default/0 + , lenses/1 + ]). + +-behaviour(els_code_lens). + +-spec command() -> els_command:command_id(). +command() -> + <<"server-info">>. + +-spec is_default() -> boolean(). +is_default() -> + false. + +%% @doc Given a Document, returns the available lenses +-spec lenses(els_dt_document:item()) -> [els_code_lens:lens()]. +lenses(_Document) -> + Root = filename:basename(els_uri:path(els_config:get(root_uri))), + Title = <<"Erlang LS (in ", Root/binary, ") info">>, + Range = els_range:line(1), + Command = els_command:make_command(Title, command(), []), + [ els_code_lens:make_lens(Range, Command, []) ]. diff --git a/src/els_command.erl b/src/els_command.erl new file mode 100644 index 000000000..e1a8e6b50 --- /dev/null +++ b/src/els_command.erl @@ -0,0 +1,72 @@ +%%============================================================================== +%% Command +%%============================================================================== +-module(els_command). + +%%============================================================================== +%% API +%%============================================================================== +-export([ with_prefix/1 + , without_prefix/1 + ]). + +%%============================================================================== +%% Constructors +%%============================================================================== + +-export([ make_command/3 ]). + +%%============================================================================== +%% Type Definitions +%%============================================================================== + +-type command() :: #{ title := binary() + , command := command_id() + , arguments => [any()] + }. +-type command_id() :: binary(). +-export_type([ command/0 + , command_id/0 + ]). + +%%============================================================================== +%% API +%%============================================================================== + +%% @doc Add a server-unique prefix to a command. +-spec with_prefix(command_id()) -> command_id(). +with_prefix(Id) -> + Prefix = server_prefix(), + <>. + +%% @doc Strip a server-unique prefix from a command. +-spec without_prefix(command_id()) -> command_id(). +without_prefix(Id0) -> + case binary:split(Id0, <<":">>) of + [_, Id] -> Id; + [Id] -> Id + end. + +%%============================================================================== +%% Constructors +%%============================================================================== + +-spec make_command(binary(), command_id(), [any()]) -> command(). +make_command(Title, CommandId, Args) -> + #{ title => Title + , command => with_prefix(CommandId) + , arguments => Args + }. + +%%============================================================================== +%% Internal Functions +%%============================================================================== + +%% @doc Generate a prefix unique to this running erlang_ls server. +%% +%% This is needed because some clients have a global namespace for all +%% registered commands, and we need to be able to run multiple +%% erlang_ls instances at the same time against a single client. +-spec server_prefix() -> binary(). +server_prefix() -> + els_utils:to_binary(os:getpid()). diff --git a/src/els_config.erl b/src/els_config.erl index fb0d8af1d..b079a18fc 100644 --- a/src/els_config.erl +++ b/src/els_config.erl @@ -36,6 +36,7 @@ -type key() :: apps_dirs | apps_paths | capabilities + | code_lenses | deps_dirs | deps_paths | include_dirs @@ -51,6 +52,7 @@ -type path() :: file:filename(). -type state() :: #{ apps_dirs => [path()] , apps_paths => [path()] + , code_lenses => [els_code_lens:lens()] , deps_dirs => [path()] , deps_paths => [path()] , include_dirs => [path()] @@ -87,6 +89,7 @@ do_initialize(RootUri, Capabilities, {ConfigPath, Config}) -> , Config , ?DEFAULT_EXCLUDED_OTP_APPS ), + CodeLenses = maps:get("code_lenses", Config, els_code_lens:default_lenses()), ExcludePathsSpecs = [[OtpPath, "lib", P ++ "*"] || P <- OtpAppsExclude], ExcludePaths = els_utils:resolve_paths(ExcludePathsSpecs, RootPath, true), lager:info("Excluded OTP Applications: ~p", [OtpAppsExclude]), @@ -108,6 +111,7 @@ do_initialize(RootUri, Capabilities, {ConfigPath, Config}) -> ok = set(deps_paths , project_paths(RootPath, DepsDirs, false)), ok = set(include_paths , include_paths(RootPath, IncludeDirs, false)), ok = set(otp_paths , otp_paths(OtpPath, false) -- ExcludePaths), + ok = set(code_lenses , CodeLenses), %% All (including subdirs) paths used to search files with file:path_open/3 ok = set( search_paths , lists:append([ project_paths(RootPath, AppsDirs, true) diff --git a/src/els_execute_command_provider.erl b/src/els_execute_command_provider.erl index b7e69e0d1..f521f451b 100644 --- a/src/els_execute_command_provider.erl +++ b/src/els_execute_command_provider.erl @@ -5,8 +5,6 @@ -export([ handle_request/2 , is_enabled/0 , options/0 - , add_server_prefix/1 - , strip_server_prefix/1 ]). -include("erlang_ls.hrl"). @@ -20,22 +18,23 @@ is_enabled() -> true. -spec options() -> map(). options() -> - #{ commands => [ add_server_prefix(<<"replace-lines">>) - , add_server_prefix(<<"info">>)] }. + #{ commands => [ els_command:with_prefix(<<"replace-lines">>) + , els_command:with_prefix(<<"server-info">>)] }. -spec handle_request(any(), els_provider:state()) -> {any(), els_provider:state()}. handle_request({workspace_executecommand, Params}, State) -> #{ <<"command">> := PrefixedCommand } = Params, Arguments = maps:get(<<"arguments">>, Params, []), - Result = execute_command(strip_server_prefix(PrefixedCommand), Arguments), + Result = execute_command( els_command:without_prefix(PrefixedCommand) + , Arguments), {Result, State}. %%============================================================================== %% Internal Functions %%============================================================================== --spec execute_command(binary(), [any()]) -> [map()]. +-spec execute_command(els_command:command_id(), [any()]) -> [map()]. execute_command(<<"replace-lines">> , [#{ <<"uri">> := Uri , <<"lines">> := Lines @@ -47,8 +46,7 @@ execute_command(<<"replace-lines">> }, els_server:send_request(Method, Params), []; -execute_command(<<"info">> - , [#{ <<"uri">> := _Uri }] = _Arguments) -> +execute_command(<<"server-info">>, _Arguments) -> {ok, Version} = application:get_key(?APP, vsn), BinVersion = list_to_binary(Version), Root = filename:basename(els_uri:path(els_config:get(root_uri))), @@ -70,26 +68,3 @@ execute_command(Command, Arguments) -> lager:info("Unsupported command: [Command=~p] [Arguments=~p]" , [Command, Arguments]), []. - - -%% @doc Strip a server-unique prefix from a command. --spec strip_server_prefix(binary()) -> binary(). -strip_server_prefix(PrefixedCommand) -> - case binary:split(PrefixedCommand, <<":">>) of - [_, Command] -> Command; - [Command] -> Command - end. - -%% @doc Add a server-unique prefix to a command. --spec add_server_prefix(binary()) -> binary(). -add_server_prefix(Command) -> - Prefix = server_prefix(), - <>. - -%% @doc Generate a prefix unique to this running erlang_ls server. This is -%% needed because some clients have a global namespace for all registered -%% commands, and we need to be able to run multiple erlang_ls instances at the -%% same time against a single client. --spec server_prefix() -> binary(). -server_prefix() -> - els_utils:to_binary(os:getpid()). diff --git a/src/els_methods.erl b/src/els_methods.erl index f6c5c8234..3f0dc251c 100644 --- a/src/els_methods.erl +++ b/src/els_methods.erl @@ -407,14 +407,14 @@ textdocument_codeaction(Params, State) -> {document_codeaction, Params}), {response, Response, State}. +%%============================================================================== %% textDocument/codeLens %%============================================================================== -spec textdocument_codelens(params(), state()) -> result(). textdocument_codelens(Params, State) -> Provider = els_code_lens_provider, - Response = els_provider:handle_request(Provider, - {document_codelens, Params}), + Response = els_provider:handle_request(Provider, {document_codelens, Params}), {response, Response, State}. %%============================================================================== diff --git a/src/els_protocol.erl b/src/els_protocol.erl index 26d68585b..ad11327bf 100644 --- a/src/els_protocol.erl +++ b/src/els_protocol.erl @@ -15,7 +15,6 @@ %% Data Structures -export([ range/1 - , command/3 ]). %%============================================================================== @@ -70,13 +69,6 @@ range(#{ from := {FromL, FromC}, to := {ToL, ToC} }) -> , 'end' => #{line => ToL - 1, character => ToC - 1} }. --spec command(binary(), binary(), [any()]) -> command(). -command(Title, Command, Args) -> - #{ title => Title - , command => Command - , arguments => Args - }. - %%============================================================================== %% Internal Functions %%============================================================================== diff --git a/src/els_range.erl b/src/els_range.erl index 36b38783b..5761838a6 100644 --- a/src/els_range.erl +++ b/src/els_range.erl @@ -3,6 +3,7 @@ -include("erlang_ls.hrl"). -export([ compare/2 + , line/1 , range/4 ]). @@ -17,6 +18,12 @@ compare( #{from := FromA, to := ToA} compare(_, _) -> false. +%% @doc Return a range data structure for a given line number +-spec line(non_neg_integer()) -> range(). +line(Line) -> + Range = #{from => {Line, 1}, to => {Line + 1, 1}}, + els_protocol:range(Range). + -spec range(pos() | {pos(), pos()}, poi_kind(), any(), any()) -> poi_range(). range({{Line, Column}, {ToLine, ToColumn}}, Name, _, _Data) when Name =:= export; diff --git a/test/els_code_action_SUITE.erl b/test/els_code_action_SUITE.erl index 4351e2e12..0e3d9490a 100644 --- a/test/els_code_action_SUITE.erl +++ b/test/els_code_action_SUITE.erl @@ -70,8 +70,7 @@ add_underscore_to_unused_var(Config) -> , source => <<"Compiler">> }, Range = els_protocol:range(#{from => {80, 1}, to => {81, 1}}), - PrefixedCommand - = els_execute_command_provider:add_server_prefix(<<"replace-lines">>), + PrefixedCommand = els_command:with_prefix(<<"replace-lines">>), #{result := Result} = els_client:document_codeaction(Uri, Range, [Diag]), Expected = [ #{ command => #{ arguments => [ #{ from => 79 diff --git a/test/els_code_lens_SUITE.erl b/test/els_code_lens_SUITE.erl index d6c685801..e2a304a5e 100644 --- a/test/els_code_lens_SUITE.erl +++ b/test/els_code_lens_SUITE.erl @@ -11,7 +11,8 @@ ]). %% Test cases --export([ get_erlang_ls_info/1 +-export([ default_lenses/1 + , server_info/1 ]). %%============================================================================== @@ -49,28 +50,44 @@ end_per_suite(Config) -> els_test_utils:end_per_suite(Config). -spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(server_info, Config) -> + meck:new(els_code_lens_server_info, [passthrough, no_link]), + meck:expect(els_code_lens_server_info, is_default, 0, true), + els_test_utils:init_per_testcase(server_info, Config); init_per_testcase(TestCase, Config) -> els_test_utils:init_per_testcase(TestCase, Config). -spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(server_info, Config) -> + els_test_utils:end_per_testcase(server_info, Config), + meck:unload(els_code_lens_server_info), + ok; end_per_testcase(TestCase, Config) -> els_test_utils:end_per_testcase(TestCase, Config). %%============================================================================== %% Testcases %%============================================================================== --spec get_erlang_ls_info(config()) -> ok. -get_erlang_ls_info(Config) -> + +-spec default_lenses(config()) -> ok. +default_lenses(Config) -> + Uri = ?config(code_navigation_uri, Config), + #{result := Result} = els_client:document_codelens(Uri), + ?assertEqual([], Result), + ok. + +-spec server_info(config()) -> ok. +server_info(Config) -> Uri = ?config(code_navigation_uri, Config), #{result := Result} = els_client:document_codelens(Uri), - PrefixedCommand - = els_execute_command_provider:add_server_prefix(<<"info">>), + PrefixedCommand = els_command:with_prefix(<<"server-info">>), Title = <<"Erlang LS (in code_navigation) info">>, Expected = - [ #{ command => #{ arguments => [ #{ uri => Uri } ] + [ #{ command => #{ arguments => [] , command => PrefixedCommand , title => Title } + , data => [] , range => #{'end' => #{character => 0, line => 1}, start => #{character => 0, line => 0}} diff --git a/test/els_execute_command_SUITE.erl b/test/els_execute_command_SUITE.erl index 83f3cba8e..83cc7acf9 100644 --- a/test/els_execute_command_SUITE.erl +++ b/test/els_execute_command_SUITE.erl @@ -64,8 +64,7 @@ end_per_testcase(TestCase, Config) -> -spec erlang_ls_info(config()) -> ok. erlang_ls_info(Config) -> Uri = ?config(code_navigation_uri, Config), - PrefixedCommand - = els_execute_command_provider:add_server_prefix(<<"info">>), + PrefixedCommand = els_command:with_prefix(<<"server-info">>), #{result := Result} = els_client:workspace_executecommand(PrefixedCommand, [#{uri => Uri}]), Expected = [], @@ -84,17 +83,16 @@ erlang_ls_info(Config) -> -spec strip_server_prefix(config()) -> ok. strip_server_prefix(_Config) -> - PrefixedCommand - = els_execute_command_provider:add_server_prefix(<<"info">>), - ?assertEqual(<<"info">> - , els_execute_command_provider:strip_server_prefix(PrefixedCommand)), + PrefixedCommand = els_command:with_prefix(<<"server-info">>), + ?assertEqual( <<"server-info">> + , els_command:without_prefix(PrefixedCommand)), - ?assertEqual(<<"info">> - , els_execute_command_provider:strip_server_prefix(<<"123:info">>)), + ?assertEqual( <<"server-info">> + , els_command:without_prefix(<<"123:server-info">>)), - ?assertEqual(<<"info">> - , els_execute_command_provider:strip_server_prefix(<<"info">>)), + ?assertEqual( <<"server-info">> + , els_command:without_prefix(<<"server-info">>)), - ?assertEqual(<<"info:f">> - , els_execute_command_provider:strip_server_prefix(<<"13:info:f">>)), + ?assertEqual( <<"server-info:f">> + , els_command:without_prefix(<<"13:server-info:f">>)), ok. diff --git a/test/prop_statem.erl b/test/prop_statem.erl index 95388a135..e46ed53dc 100644 --- a/test/prop_statem.erl +++ b/test/prop_statem.erl @@ -95,8 +95,8 @@ initialize_post(#{shutdown := true}, _Args, Res) -> true; initialize_post(_S, _Args, Res) -> PrefixedCommands - = [ els_execute_command_provider:add_server_prefix(<<"replace-lines">>) - , els_execute_command_provider:add_server_prefix(<<"info">>)], + = [ els_command:with_prefix(<<"replace-lines">>) + , els_command:with_prefix(<<"server-info">>)], Expected = #{ capabilities => #{ hoverProvider => true , completionProvider =>