diff --git a/apps/els_core/include/els_core.hrl b/apps/els_core/include/els_core.hrl index 2ec182a12..41fda871d 100644 --- a/apps/els_core/include/els_core.hrl +++ b/apps/els_core/include/els_core.hrl @@ -548,8 +548,8 @@ }. -type document_ontypeformatting_options() :: false | - #{ first_trigger_character := string() - , more_trigger_character => string() + #{ firstTriggerCharacter := binary() + , moreTriggerCharacter => [binary()] }. %%------------------------------------------------------------------------------ @@ -612,7 +612,8 @@ | {atom(), atom()} %% record_def_field, record_field | string() %% include, include_lib | {atom(), arity()} - | {module(), atom(), arity()}. + | {module(), atom(), arity()} + | pos(). -type poi() :: #{ kind := poi_kind() , id := poi_id() , data := any() diff --git a/apps/els_core/src/els_client.erl b/apps/els_core/src/els_client.erl index 345322a14..b96de6fa4 100644 --- a/apps/els_core/src/els_client.erl +++ b/apps/els_core/src/els_client.erl @@ -452,6 +452,10 @@ request_params({initialize, {RootUri, InitOptions}}) -> #{ <<"contextSupport">> => 'true' } , <<"hover">> => #{ <<"contentFormat">> => ContentFormat } + , <<"documentOnTypeFormattingProvider">> => + #{ firstTriggerCharacter => <<",">> + , moreTriggercharacter => [] + } }, #{ <<"rootUri">> => RootUri , <<"initializationOptions">> => InitOptions diff --git a/apps/els_core/src/els_config.erl b/apps/els_core/src/els_config.erl index 2fd3652ce..a8e68362c 100644 --- a/apps/els_core/src/els_config.erl +++ b/apps/els_core/src/els_config.erl @@ -125,6 +125,7 @@ do_initialize(RootUri, Capabilities, InitOptions, {ConfigPath, Config}) -> = maps:get("compiler_telemetry_enabled", Config, false), IndexingEnabled = maps:get(<<"indexingEnabled">>, InitOptions, true), + FormatOnTypeEnabled = maps:get("format_on_type", Config, false), %% Passed by the LSP client ok = set(root_uri , RootUri), @@ -138,6 +139,7 @@ do_initialize(RootUri, Capabilities, InitOptions, {ConfigPath, Config}) -> ok = set(macros , Macros), ok = set(plt_path , DialyzerPltPath), ok = set(code_reload , CodeReload), + ok = set(format_on_type , FormatOnTypeEnabled), ?LOG_INFO("Config=~p", [Config]), ok = set(runtime, maps:merge( els_config_runtime:default_config() , Runtime)), diff --git a/apps/els_lsp/src/els_dt_document.erl b/apps/els_lsp/src/els_dt_document.erl index e6bf5ebf6..bfcc7ba84 100644 --- a/apps/els_lsp/src/els_dt_document.erl +++ b/apps/els_lsp/src/els_dt_document.erl @@ -21,15 +21,16 @@ , lookup/1 ]). --export([ new/2 +-export([ get_element_at_pos/3 + , new/2 , pois/1 , pois/2 - , get_element_at_pos/3 , uri/1 , functions_at_pos/3 , applications_at_pos/3 , wrapping_functions/2 , wrapping_functions/3 + , text/1 ]). %%============================================================================== @@ -63,6 +64,7 @@ , md5 => binary() , pois => [poi()] }. + -export_type([ id/0 , item/0 , kind/0 @@ -155,6 +157,10 @@ new(Uri, Text, Id, Kind) -> pois(#{ pois := POIs }) -> POIs. +-spec text(item()) -> binary(). +text(#{text := Text}) -> + Text. + %% @doc Returns the list of POIs of the given types for the current %% document -spec pois(item(), [poi_kind()]) -> [poi()]. diff --git a/apps/els_lsp/src/els_formatting_provider.erl b/apps/els_lsp/src/els_formatting_provider.erl index 6994549d7..68cddb190 100644 --- a/apps/els_lsp/src/els_formatting_provider.erl +++ b/apps/els_lsp/src/els_formatting_provider.erl @@ -49,13 +49,17 @@ is_enabled_document() -> true. -spec is_enabled_range() -> boolean(). is_enabled_range() -> - false. + true. %% NOTE: because erlang_ls does not send incremental document changes %% via `textDocument/didChange`, this kind of formatting does not %% make sense. -spec is_enabled_on_type() -> document_ontypeformatting_options(). -is_enabled_on_type() -> false. +is_enabled_on_type() -> + case els_config:get(format_on_type) of + true -> #{firstTriggerCharacter => <<".">>, moreTriggerCharacter => []}; + false -> false + end. -spec handle_request(any(), state()) -> {any(), state()}. handle_request({document_formatting, Params}, State) -> @@ -157,5 +161,71 @@ rangeformat_document(_Uri, _Document, _Range, _Options) -> -spec ontypeformat_document(binary(), map() , number(), number(), string(), formatting_options()) -> {ok, [text_edit()]}. -ontypeformat_document(_Uri, _Document, _Line, _Col, _Char, _Options) -> - {ok, []}. +ontypeformat_document(_Uri, Document, Line, Col, <<".">>, _Options) -> + case find_matching_range(Document, Line) of + [] -> + {ok, []}; + [MatchingRange] -> + {StartLine, _} = Id = els_poi:id(MatchingRange), + Text = els_dt_document:text(Document), + RangeText = els_text:range(Text, Id, {Line, Col}), + % Skip formatting if the . is on a commented line. + case string:trim(els_text:line(Text, Line - 1), both) of + <<"%", _/binary>> -> + {ok, []}; + _ -> + ParseF = + fun(Dir) -> + TmpFile = tmp_file(Dir), + ok = file:write_file(TmpFile, RangeText), + Opts = #{break_indent => 2, output_dir => current}, + RebarState = #{}, + T = rebar3_formatter:new(default_formatter, Opts, RebarState), + rebar3_formatter:format_file(TmpFile, T), + {ok, Bin} = file:read_file(TmpFile), + Bin + end, + %% rebar3_formatter adds a newline, since we terminate on . + %% We want to leave the cursor at the current char rather + %% than jumping to a newline + NewText = + string:trim( + tempdir:mktmp(ParseF), trailing, "\n"), + {ok, + [#{range => + #{start => #{line => StartLine - 1, character => 0}, + 'end' => #{line => Line - 1, character => Col}}, + newText => NewText}]} + end + end; +ontypeformat_document(_Uri, _Document, _Line, _Col, Char, _Options) -> + ?LOG_INFO("Got unhandled character in ontypeformat_document. No formatter " + "configured for char: ~p", + [Char]), + {ok, []}. + +-spec find_foldable_ranges(els_dt_document:item()) -> [poi()]. +find_foldable_ranges(Document) -> + Pois = els_dt_document:pois(Document), + lists:filter(fun (#{kind := folding_range}) -> + true; + (_) -> + false + end, + Pois). + +-spec find_matching_range(els_dt_document:item(), number()) -> [poi()]. +find_matching_range(Document, Line) -> + lists:filter(fun(#{range := #{from := {FromLine, _}, to := {ToLine, _}}}) -> + Line >= FromLine andalso Line =< ToLine + end, + find_foldable_ranges(Document)). + +-spec tmp_file(string()) -> any(). +tmp_file(Dir) -> + Unique = erlang:unique_integer([positive]), + {A, B, C} = os:timestamp(), + N = node(), + filename:join(Dir, + lists:flatten( + io_lib:format("~p-~p.~p.~p.~p", [N, A, B, C, Unique]))). diff --git a/apps/els_lsp/src/els_general_provider.erl b/apps/els_lsp/src/els_general_provider.erl index 52737d78c..1880c959a 100644 --- a/apps/els_lsp/src/els_general_provider.erl +++ b/apps/els_lsp/src/els_general_provider.erl @@ -157,6 +157,8 @@ server_capabilities() -> els_formatting_provider:is_enabled_document() , documentRangeFormattingProvider => els_formatting_provider:is_enabled_range() + , documentOnTypeFormattingProvider => + els_formatting_provider:is_enabled_on_type() , foldingRangeProvider => els_folding_range_provider:is_enabled() , implementationProvider => diff --git a/apps/els_lsp/src/els_poi.erl b/apps/els_lsp/src/els_poi.erl index a4c2faf88..9f3080c20 100644 --- a/apps/els_lsp/src/els_poi.erl +++ b/apps/els_lsp/src/els_poi.erl @@ -10,6 +10,7 @@ -export([ match_pos/2 , sort/1 + , id/1 ]). %%============================================================================== @@ -42,6 +43,10 @@ match_pos(POIs, Pos) -> , to := To }} = POI <- POIs, (From =< Pos) andalso (Pos =< To)]. +-spec id(poi()) -> poi_id(). +id(#{id := Id}) -> + Id. + %% @doc Sorts pois based on their range %% %% Order is defined using els_range:compare/2.