1:- module(lsp_formatter, [ file_format_edits/2,
    2                           file_formatted/2 ]).

LSP Formatter

Module for formatting Prolog source code

author
- James Cash
   12:- use_module(library(readutil), [ read_file_to_string/3 ]).   13:- use_module(library(macros)).   14
   15:- include('_lsp_path_add.pl').   16:- use_module(lsp(lsp_formatter_parser), [ reified_format_for_file/2,
   17                                           emit_reified/2 ]).   18
   19file_format_edits(Path, Edits) :-
   20    read_file_to_string(Path, OrigText, []),
   21    split_string(OrigText, "\n", "", OrigLines),
   22    file_formatted(Path, Formatted),
   23    with_output_to(string(FormattedText),
   24                   emit_reified(current_output, Formatted)),
   25    split_string(FormattedText, "\n", "", FormattedLines),
   26    create_edit_list(OrigLines, FormattedLines, Edits).
   27
   28file_formatted(Path, Formatted) :-
   29    reified_format_for_file(Path, Reified),
   30    apply_format_rules(Reified, Formatted).
   31
   32%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
   33% Formatting rules
   34%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
   35
   36apply_format_rules(Content, Formatted) :-
   37    phrase(formatter_rules, Content, Formatted).
   38
   39formatter_rules -->
   40    collapse_whitespace,
   41    fact_only_one_space,
   42    commas_exactly_one_space,
   43    correct_indentation(_{state: [toplevel], column: 0, leading_spaces: [],
   44                          parens: []}).
   45
   46collapse_whitespace([], []) :- !.
   47collapse_whitespace([white(A), white(B)|InRest], [white(AB)|OutRest]) :- !,
   48    AB is A + B,
   49    collapse_whitespace(InRest, OutRest).
   50collapse_whitespace([In|InRest], [In|OutRest]) :-
   51    collapse_whitespace(InRest, OutRest).
   52
   53fact_only_one_space([], Out) => Out = [].
   54fact_only_one_space([term_end(F, toplevel), white(_), Next|InRest], Out), Next \= newline =>
   55    Out = [term_end(F, toplevel), white(1), Next|OutRest],
   56    fact_only_one_space(InRest, OutRest).
   57fact_only_one_space([Other|InRest], Out) =>
   58    Out = [Other|OutRest],
   59    fact_only_one_space(InRest, OutRest).
   60
   61commas_exactly_one_space([], Out) => Out = [].
   62commas_exactly_one_space([white(_), comma|InRest], Out) =>
   63    commas_exactly_one_space([comma|InRest], Out).
   64commas_exactly_one_space([comma, white(_)|InRest], Out), InRest \= [comment(_)|_] =>
   65    Out = [comma, white(1)|OutRest],
   66    commas_exactly_one_space(InRest, OutRest).
   67commas_exactly_one_space([comma, Next|InRest], Out), Next \= white(_), Next \= newline =>
   68    Out = [comma, white(1), Next|OutRest],
   69    commas_exactly_one_space(InRest, OutRest).
   70commas_exactly_one_space([Other|Rest], Out) =>
   71    Out = [Other|OutRest],
   72    commas_exactly_one_space(Rest, OutRest).
   73
   74#define(toplevel_indent, 4).
   75
   76correct_indentation(_, [], []) :- !.
   77correct_indentation(State0,
   78                    [term_begin(Func, Type, Parens)|InRest],
   79                    [term_begin(Func, Type, Parens)|OutRest]) :-
   80    indent_state_top(State0, toplevel),
   81    Func = ':-', !,
   82    indent_state_push(State0, declaration, State1),
   83    update_state_column(State1, term_begin(Func, Type, Parens), State2),
   84    push_state_open_spaces(State2, InRest, State3),
   85    correct_indentation(State3, InRest, OutRest).
   86correct_indentation(State0,
   87                    [term_begin(Func, Type, Parens)|InRest],
   88                    [term_begin(Func, Type, Parens)|OutRest]) :-
   89    indent_state_top(State0, toplevel), !,
   90    update_state_column(State0, term_begin(Func, Type, Parens), State1),
   91    indent_state_push(State1, defn_head(State1.column, false), State2),
   92    push_state_open_spaces(State2, InRest, State3),
   93    correct_indentation(State3, InRest, OutRest).
   94correct_indentation(State0, [In|InRest], [In|OutRest]) :-
   95    indent_state_top(State0, toplevel),
   96    In = simple(_), !,
   97    indent_state_push(State0, defn_head_neck, State1),
   98    update_state_column(State1, In, State2),
   99    correct_indentation(State2, InRest, OutRest).
  100correct_indentation(State0,
  101                    [term_begin(Neckish, T, P)|InRest],
  102                    [term_begin(Neckish, T, P)|OutRest]) :-
  103    memberchk(Neckish, [':-', '=>', '-->']),
  104    indent_state_top(State0, defn_head_neck), !,
  105    indent_state_pop(State0, State1),
  106    indent_state_push(State1, defn_body, State2),
  107    update_state_column(State2, term_begin(Neckish, T, P), State3),
  108    push_state_open_spaces(State3, InRest, State4),
  109    correct_indentation(State4, InRest, OutRest).
  110correct_indentation(State0, [In|InRest], Out) :-
  111    once((In = term_begin('->', compound, false)
  112         ; In = term_begin('*->', compound, false)
  113         ; In = term_begin(';', compound, false))),
  114    indent_state_top(State0, defn_body_indent), !,
  115    indent_state_pop(State0, State1),
  116    paren_state_top(State1, Indent),
  117    Out = [white(Indent)|OutRest],
  118    update_state_column(State1, white(Indent), State4),
  119    correct_indentation(State4, [In|InRest], OutRest).
  120correct_indentation(State0, [newline|InRest], [newline|Out]) :- !,
  121    ( indent_state_top(State0, defn_body_indent)
  122    -> State1 = State0
  123    ; indent_state_push(State0, defn_body_indent, State1) ),
  124    update_state_column(State1, newline, State2),
  125    correct_indentation(State2, InRest, Out).
  126correct_indentation(State0, [In|InRest], Out) :-
  127    indent_state_top(State0, defn_body_indent), !,
  128    ( In = white(_)
  129    -> correct_indentation(State0, InRest, Out)
  130    ;  insert_whitespace_to_indent(State0, [In|InRest], Out) ).
  131correct_indentation(State0, [In|InRest], [In|OutRest]) :-
  132    In = term_begin(';', compound, false), !,
  133    update_alignment(State0, State1),
  134    update_state_column(State1, In, State2),
  136    copy_current_alignment(State2, CurrentAlign),
  137    indent_state_push(State2, CurrentAlign, State3),
  138    push_state_open_spaces(State3, InRest, State4),
  139    correct_indentation(State4, InRest, OutRest)
  139.
  140correct_indentation(State0, [In|InRest], [In|OutRest]) :-
  141    functor(In, Name, _Arity, _Type),
  142    atom_concat(_, '_begin', Name), !,
  143    % if we've just begun something...
  144    update_alignment(State0, State1),
  145    update_state_column(State1, In, State2),
  146    indent_state_push(State2, begin(State2.column, State1.column), State3),
  147    push_state_open_spaces(State3, InRest, State4),
  148    ( Name == parens_begin
  149    -> paren_state_push(State4, State1.column, State5)
  150    ;  State5 = State4 ),
  151    correct_indentation(State5, InRest, OutRest).
  152correct_indentation(State0, [In|InRest], [In|OutRest]) :-
  153    indent_state_top(State0, defn_head(_, _)),
  154    In = term_end(_, S), S \= toplevel, !,
  155    indent_state_pop(State0, State1),
  156    indent_state_push(State1, defn_head_neck, State2),
  157    update_state_column(State2, In, State3),
  158    pop_state_open_spaces(State3, _, State4),
  159    correct_indentation(State4, InRest, OutRest).
  160correct_indentation(State0, [In|InRest], Out) :-
  161    ending_term(In), !,
  162    indent_state_pop(State0, State1),
  163    update_state_column(State1, In, State2),
  164    pop_state_open_spaces(State2, Spaces, State3),
  165    ( In \= term_end(false, _), In \= term_end(_, toplevel), Spaces > 0
  166    -> Out = [white(Spaces), In|OutRest]
  167    ;  Out = [In|OutRest] ),
  168    ( In == parens_end
  169    -> paren_state_pop(State3, State4)
  170    ;  State3 = State4 ),
  171    correct_indentation(State4, InRest, OutRest).
  172correct_indentation(State0, [In, NextIn|InRest], Out) :-
  173    In = white(_),
  174    ending_term(NextIn), !,
  175    correct_indentation(State0, [NextIn|InRest], Out).
  176correct_indentation(State0, [In|InRest], [In|OutRest]) :-
  177    memberchk(In, [white(_), newline]), !,
  178    update_state_column(State0, In, State1),
  179    correct_indentation(State1, InRest, OutRest).
  180correct_indentation(State0, [In|InRest], [In|OutRest]) :- !,
  181    ( In \= white(_)
  182    -> update_alignment(State0, State1)
  183    ; State1 = State0 ),
  184    update_state_column(State1, In, State2),
  185    correct_indentation(State2, InRest, OutRest).
  186
  187copy_current_alignment(State, Alignment), indent_state_top(State, defn_body) =>
  188    Alignment = align(#toplevel_indent, 4).
  189copy_current_alignment(State, Alignment), indent_state_top(State, align(_, _)) =>
  190    indent_state_top(State, Alignment).
  191copy_current_alignment(State, Alignment), indent_state_top(State, begin(Col, BeganAt)) =>
  192    Alignment = begin(Col, BeganAt).
  193copy_current_alignment(State, Alignment), indent_state_top(State, defn_body_indent) =>
  194    Alignment = align(#toplevel_indent, 4).
  195copy_current_alignment(State, Alignment), indent_state_top(State, defn_head(Column, _Aligned)) =>
  196    Alignment = align(Column, Column).
  197copy_current_alignment(State, Alignment), indent_state_top(State, defn_head_neck) =>
  198    Alignment = align(#toplevel_indent, 4).
  199copy_current_alignment(State, Alignment), indent_state_top(State, declaration) =>
  200    Alignment = align(2, 2).
  201copy_current_alignment(State, Alignment) =>
  202    indent_state_top(State, Alignment).
  203
  204insert_whitespace_to_indent(State0, [In|InRest], Out) :-
  205    indent_state_pop(State0, State1),
  206    ( indent_state_top(State1, begin(_, BeganAt))
  207      % state top = begin means prev line ended with an open paren
  208    -> % so pop that off and align as if one step "back"
  209       indent_state_pop(State1, StateX),
  210       whitespace_indentation_for_state(StateX, PrevIndent),
  211       IncPrevIndent is PrevIndent + 4,
  212       indent_state_push(StateX, align(IncPrevIndent, BeganAt), State2)
  213    ; State2 = State1 ),
  214    update_alignment(State2, State3),
  215    ( ending_term(In)
  216    -> indent_for_end_term(State3, In, State4, Indent)
  217    ; whitespace_indentation_for_state(State3, Indent),
  218      State4 = State3 ),
  219    Out = [white(Indent)|OutRest],
  220    update_state_column(State4, white(Indent), State5),
  221    correct_indentation(State5, [In|InRest], OutRest).
  222
  223indent_for_end_term(State0, In, State, Indent) :-
  224    % for a paren ending a term, align a level up
  225    In = term_end(true, _), !,
  226    indent_state_pop(State0, State_),
  227    pop_state_open_spaces(State0, _, State1),
  228    push_state_open_spaces(State1, 0, State),
  229    whitespace_indentation_for_state(State_, Indent).
  230indent_for_end_term(State0, In, State, Indent) :-
  231    % for a brace ending a dict, align two levels up level up
  232    In = dict_end, !,
  233    indent_state_pop(State0, State_),
  234    indent_state_pop(State_, State__),
  235    pop_state_open_spaces(State0, _, State1),
  236    push_state_open_spaces(State1, 0, State),
  237    whitespace_indentation_for_state(State__, Indent).
  238indent_for_end_term(State0, _In, State, Indent) :-
  239    % for another ending term, align to the open
  240    % if we have alignment infomation.
  241    indent_state_top(State0, Top),
  242    Top = align(_, Indent), !,
  243    pop_state_open_spaces(State0, _, State1),
  244    push_state_open_spaces(State1, 0, State).
  245indent_for_end_term(State0, _In, State, Indent) :-
  246    % otherwise, at top-level, just pop state.
  247    indent_state_pop(State0, State_),
  248    pop_state_open_spaces(State0, _, State1),
  249    push_state_open_spaces(State1, 0, State),
  250    whitespace_indentation_for_state(State_, Indent).
  251
  252ending_term(Term) :-
  253    functor(Term, Name, _, _),
  254    atom_concat(_, '_end', Name).
  255
  256outdent_align(State, Outdented) :-
  257    whitespace_indentation_for_state(State, Indent),
  258    Outdented is Indent - 2.
  259
  260update_alignment(State0, State2) :-
  261    indent_state_top(State0, begin(Col, BeganAt)), !,
  262    indent_state_pop(State0, State1),
  263    AlignCol is max(Col, State1.column),
  264    indent_state_push(State1, align(AlignCol, BeganAt), State2).
  265update_alignment(State0, State2) :-
  266    indent_state_top(State0, defn_head(Col, false)), !,
  267    indent_state_pop(State0, State1),
  268    AlignCol is max(Col, State1.column),
  269    indent_state_push(State1, defn_head(AlignCol, true), State2).
  270update_alignment(State, State).
  271
  272whitespace_indentation_for_state(State, Indent) :-
  273    indent_state_top(State, align(Indent, _)), !.
  274whitespace_indentation_for_state(State, Indent) :-
  275    indent_state_top(State, defn_head(Indent, _)), !.
  276whitespace_indentation_for_state(State, Indent) :-
  277    get_dict(state, State, Stack),
  278    aggregate_all(count,
  279                  ( member(X, Stack),
  280                    memberchk(X, [parens_begin, braces_begin, term_begin(_, _, _)]) ),
  281                  ParensCount),
  282    ( indent_state_contains(State, defn_body)
  283    -> MoreIndent = #toplevel_indent
  284    ;  MoreIndent = 0 ),
  285    Indent is ParensCount * 2 + MoreIndent.
  286
  287indent_state_top(State, Top) :-
  288    _{state: [Top|_]} :< State.
  289
  290indent_state_contains(State, Needle) :-
  291    _{state: Stack} :< State,
  292    memberchk(Needle, Stack).
  293
  294paren_state_push(State0, NewTop, State1) :-
  295    _{parens: OldParens} :< State0,
  296    put_dict(parens, State0, [NewTop|OldParens], State1).
  297
  298paren_state_pop(State0, State1) :-
  299    _{parens: [_|Parens]} :< State0,
  300    put_dict(parens, State0, Parens, State1).
  301
  302paren_state_top(State0, Top) :-
  303    _{parens: [Top|_]} :< State0.
  304
  305indent_state_push(State0, NewTop, State1) :-
  306    _{state: Stack} :< State0,
  307    put_dict(state, State0, [NewTop|Stack], State1).
  308
  309indent_state_pop(State0, State1) :-
  310    _{state: [_|Rest]} :< State0,
  311    put_dict(state, State0, Rest, State1).
  312
  313update_state_column(State0, newline, State1) :- !,
  314    put_dict(column, State0, 0, State1).
  315update_state_column(State0, Term, State1) :-
  316    emit_reified(string(S), [Term]),
  317    string_length(S, Len),
  318    NewCol is State0.column + Len,
  319    put_dict(column, State0, NewCol, State1).
  320
  321push_state_open_spaces(State0, Next, State1) :-
  322    _{leading_spaces: PrevSpaces} :< State0,
  323    ( Next = [white(N)|_]
  324    -> put_dict(leading_spaces, State0, [N|PrevSpaces], State1)
  325    ; put_dict(leading_spaces, State0, [0|PrevSpaces], State1) ).
  326
  327pop_state_open_spaces(State0, Top, State1) :-
  328    _{leading_spaces: [Top|Spaces]} :< State0,
  329    put_dict(leading_spaces, State0, Spaces, State1).
  330
  331%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  332% Create a List of Edits from the Original and Formatted Lines
  333%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  334create_edit_list(Orig, Formatted, Edits) :-
  335    create_edit_list(0, Orig, Formatted, Edits).
  336
  337create_edit_list(_, [], [], []) :- !.
  338create_edit_list(LineNum, [Line|Lines], [], [Edit]) :- !,
  339    length(Lines, NLines),
  340    EndLine is LineNum + NLines,
  341    last([Line|Lines], LastLine),
  342    string_length(LastLine, LastLineLen),
  343    Edit = _{range: _{start: _{line: LineNum, character: 0},
  344                      end: _{line: EndLine, character: LastLineLen}},
  345             newText: ""}.
  346create_edit_list(LineNum, [], [NewLine|NewLines], [Edit|Edits]) :- !,
  347    string_length(NewLine, LenLen),
  348    Edit = _{range: _{start: _{line: LineNum, character: 0},
  349                      end: _{line: LineNum, character: LenLen}},
  350             newText: NewLine},
  351    succ(LineNum, LineNum1),
  352    create_edit_list(LineNum1, [], NewLines, Edits).
  353create_edit_list(LineNum, [OrigLine|OrigRest], [FormattedLine|FormattedRest], Edits) :-
  354    (   OrigLine \= FormattedLine  % Only create an edit if the line has changed
  355    -> string_length(OrigLine, LineLen), %TODO: what should this be?
  356       Edit = _{range: _{start: _{line: LineNum, character: 0},
  357                         end: _{line: LineNum, character: LineLen}},
  358                newText: FormattedLine},
  359       Edits = [Edit|EditRest]
  360    ; EditRest = Edits
  361    ),
  362    succ(LineNum, LineNum1),
  363    create_edit_list(LineNum1, OrigRest, FormattedRest, EditRest).
  364
  365% lsp_formatter:file_formatted('/Users/james/Projects/prolog-lsp/prolog/format_test2.pl', Src), lsp_formatter_parser:emit_reified(user_output, Src).
  366
  367% lsp_formatter:file_formatted('/Users/james/Projects/prolog-lsp/prolog/format_test.pl', Src), setup_call_cleanup(open('/Users/james/tmp/formatted_out.pl', write, S), lsp_formatter_parser:emit_reified(S, Src), close(S)).