1:- module(lsp_utils, [called_at/3,
    2                      defined_at/3,
    3                      name_callable/2,
    4                      relative_ref_location/4,
    5                      help_at_position/4,
    6                      clause_in_file_at_position/3,
    7                      clause_variable_positions/3,
    8                      seek_to_line/2,
    9                      linechar_offset/3,
   10                      url_path/2
   11                     ]).

LSP Utils

Module with a bunch of helper predicates for looking through prolog source and stuff.

author
- James Cash */
   20:- use_module(library(apply_macros)).   21:- use_module(library(apply), [maplist/3, exclude/3]).   22:- use_module(library(prolog_xref)).   23:- use_module(library(prolog_source), [read_source_term_at_location/3]).   24:- use_module(library(help), [help_html/3, help_objects/3]).   25:- use_module(library(lynx/html_text), [html_text/1]).   26:- use_module(library(solution_sequences), [distinct/2]).   27:- use_module(library(lists), [append/3, member/2, selectchk/4]).   28:- use_module(library(sgml), [load_html/3]).   29:- use_module(library(yall)).   30
   31:- include('_lsp_path_add.pl').   32
   33:- use_module(lsp(lsp_reading_source), [ file_lines_start_end/2,
   34                                         read_term_positions/2,
   35                                         read_term_positions/4,
   36                                         find_in_term_with_positions/5,
   37                                         position_to_match/3,
   38                                         file_offset_line_position/4 ]).   39
   40:- if(current_predicate(xref_called/5)).
 called_at(+Path:atom, +Clause:term, -Locations:list) is det
Find the callers and locations of the goal Clause, starting from the file Path. Locations will be a list of all the callers and locations that the Clause is called from as LSP-formatted dicts.
   45called_at(Path, Clause, Locations) :-
   46    setof(L, Path^Clause^Locs^(
   47                 called_at_(Path, Clause, Locs),
   48                 member(L, Locs)
   49             ),
   50          Locations), !.
   51called_at(Path, Clause, Locations) :-
   52    name_callable(Clause, Callable),
   53    xref_source(Path),
   54    xref_called(Path, Callable, _By, _, CallerLine),
   55    % we couldn't find the definition, but we know it's in that form, so give that at least
   56    succ(CallerLine0, CallerLine),
   57    Locations = [_{range: _{start: _{line: CallerLine0, character: 0},
   58                            end: _{line: CallerLine, character: 0}}}].
   59
   60called_at_(Path, Clause, Locations) :-
   61    name_callable(Clause, Callable),
   62    xref_source(Path),
   63    xref_called(Path, Callable, _By, _, CallerLine),
   64    file_lines_start_end(Path, LineCharRange),
   65    file_offset_line_position(LineCharRange, Offset, CallerLine, 0),
   66    read_term_positions(Path, Offset, Offset, TermInfos),
   67    Clause = FuncName/Arity,
   68    find_occurences_of_callable(Path, FuncName, Arity, TermInfos, Matches, []),
   69    maplist(position_to_match(LineCharRange), Matches, Locations).
   70called_at_(Path, Clause, Locations) :-
   71    xref_source(Path),
   72    Clause = FuncName/Arity,
   73    DcgArity is Arity + 2,
   74    DcgClause = FuncName/DcgArity,
   75    name_callable(DcgClause, DcgCallable),
   76    xref_defined(Path, DcgCallable, dcg),
   77    name_callable(DcgClause, DcgCallable),
   78    xref_called(Path, DcgCallable, _By, _, CallerLine),
   79    file_lines_start_end(Path, LineCharRange),
   80    file_offset_line_position(LineCharRange, Offset, CallerLine, 0),
   81    read_term_positions(Path, Offset, Offset, TermInfos),
   82    find_occurences_of_callable(Path, FuncName, DcgArity, TermInfos, Matches, Tail0),
   83    % also look for original arity in a dcg context
   84    % TODO: modify this to check that it's inside a DCG if it has this
   85    % arity...but not in braces?
   86    find_occurences_of_callable(Path, FuncName, Arity, TermInfos, Tail0, []),
   87    maplist(position_to_match(LineCharRange), Matches, Locations).
   88:- else.   89called_at(Path, Callable, By, Ref) :-
   90    xref_called(Path, Callable, By),
   91    xref_defined(Path, By, Ref).
   92:- endif.   93
   94find_occurences_of_callable(_, _, _, [], Tail, Tail).
   95find_occurences_of_callable(Path, FuncName, Arity, [TermInfo|TermInfos], Matches, Tail) :-
   96    FindState = in_meta(false),
   97    find_in_term_with_positions(term_matches_callable(FindState, Path, FuncName, Arity),
   98                                TermInfo.term, TermInfo.subterm, Matches, Tail0),
   99    find_occurences_of_callable(Path, FuncName, Arity, TermInfos, Tail0, Tail).
  100
  101term_matches_callable(FindState, Path, FuncName, Arity, Term, Position) :-
  102    arg(1, Position, Start),
  103    arg(2, Position, End),
  104    ( arg(1, FindState, in_meta(_, MStart, MEnd)),
  105      once( Start > MEnd ; End < MStart )
  106    -> nb_setarg(1, FindState, false)
  107    ; true ),
  108    term_matches_callable_(FindState, Path, FuncName, Arity, Term, Position).
  109
  110term_matches_callable_(_, _, FuncName, Arity, Term, _) :-
  111    nonvar(Term), Term = FuncName/Arity.
  112term_matches_callable_(_, _, FuncName, Arity, Term, _) :-
  113    nonvar(Term),
  114    functor(T, FuncName, Arity),
  115    Term = T, !.
  116term_matches_callable_(State, _, FuncName, Arity, Term, _) :-
  117    nonvar(Term),
  118    % TODO check the argument
  119    arg(1, State, in_meta(N, _, _)),
  120    MArity is Arity - N,
  121    functor(T, FuncName, MArity),
  122    Term = T, !.
  123term_matches_callable_(State, Path, _, _, Term, Position) :-
  124    nonvar(Term), compound(Term),
  125    compound_name_arity(Term, ThisName, ThisArity),
  126    name_callable(ThisName/ThisArity, Callable),
  127    xref_meta(Path, Callable, Called),
  128    member(E, Called), nonvar(E), E = _+N, integer(N),
  129    arg(1, Position, Start),
  130    arg(2, Position, End),
  131    nb_setarg(1, State, in_meta(N, Start, End)),
  132    fail.
 url_path(?FileUrl:atom, ?Path:atom) is det
Convert between file:// url and path
  137url_path(Url, Path) :-
  138    current_prolog_flag(windows, true),
  139    % on windows, in neovim at least, textDocument URI looks like
  140    % "file:///C:/foo/bar/baz.pl"; we need to strip off another
  141    % leading slash to get a valid path
  142    atom_concat('file:///', Path, Url), !.
  143url_path(Url, Path) :-
  144    atom_concat('file://', Path, Url).
  145
  146defined_at(Path, Name/Arity, Location) :-
  147    name_callable(Name/Arity, Callable),
  148    xref_source(Path),
  149    xref_defined(Path, Callable, Ref),
  150    url_path(Doc, Path),
  151    relative_ref_location(Doc, Callable, Ref, Location).
  152defined_at(Path, Name/Arity, Location) :-
  153    % maybe it's a DCG?
  154    DcgArity is Arity + 2,
  155    name_callable(Name/DcgArity, Callable),
  156    xref_source(Path),
  157    xref_defined(Path, Callable, Ref),
  158    url_path(Doc, Path),
  159    relative_ref_location(Doc, Callable, Ref, Location).
  160
  161
  162find_subclause(Stream, Subclause, CallerLine, Locations) :-
  163    read_source_term_at_location(Stream, Term, [line(CallerLine),
  164                                                subterm_positions(Poses)]),
  165    findall(Offset, distinct(Offset, find_clause(Term, Offset, Poses, Subclause)),
  166            Offsets),
  167    collapse_adjacent(Offsets, StartOffsets),
  168    maplist(offset_line_char(Stream), StartOffsets, Locations).
  169
  170offset_line_char(Stream, Offset, position(Line, Char)) :-
  171    % seek(Stream, 0, bof, _),
  172    % for some reason, seek/4 isn't zeroing stream line position
  173    set_stream_position(Stream, '$stream_position'(0, 0, 0, 0)),
  174    setup_call_cleanup(
  175        open_null_stream(NullStream),
  176        copy_stream_data(Stream, NullStream, Offset),
  177        close(NullStream)
  178    ),
  179    stream_property(Stream, position(Pos)),
  180    stream_position_data(line_count, Pos, Line),
  181    stream_position_data(line_position, Pos, Char).
  182
  183collapse_adjacent([X|Rst], [X|CRst]) :-
  184    collapse_adjacent(X, Rst, CRst).
  185collapse_adjacent(X, [Y|Rst], CRst) :-
  186    succ(X, Y), !,
  187    collapse_adjacent(Y, Rst, CRst).
  188collapse_adjacent(_, [X|Rst], [X|CRst]) :- !,
  189    collapse_adjacent(X, Rst, CRst).
  190collapse_adjacent(_, [], []).
 name_callable(?Name:functor, ?Callable:term) is det
True when, if Name = Func/Arity, Callable = Func(_, _, ...) with Arity args.
  196name_callable(Name/0, Name) :- atom(Name), !.
  197name_callable(Name/Arity, Callable) :-
  198    length(FakeArgs, Arity),
  199    Callable =.. [Name|FakeArgs], !.
 relative_ref_location(+Path:atom, +Goal:term, +Position:position(int,int), -Location:dict) is semidet
Given Goal found in Path and position Position (from called_at/3), Location is a dictionary suitable for sending as an LSP response indicating the position in a file of Goal.
  205relative_ref_location(Here, _, position(Line0, Char1),
  206                      _{uri: Here, range: _{start: _{line: Line0, character: Char1},
  207                                            end: _{line: Line1, character: 0}}}) :-
  208    !, succ(Line0, Line1).
  209relative_ref_location(Here, _, local(Line1),
  210                      _{uri: Here, range: _{start: _{line: Line0, character: 1},
  211                                            end: _{line: NextLine, character: 0}}}) :-
  212    !, succ(Line0, Line1), succ(Line1, NextLine).
  213relative_ref_location(_, Goal, imported(Path), Location) :-
  214    url_path(ThereUri, Path),
  215    xref_source(Path),
  216    xref_defined(Path, Goal, Loc),
  217    relative_ref_location(ThereUri, Goal, Loc, Location).
 help_at_position(+Path:atom, +Line:integer, +Char:integer, -Help:string) is det
Help is the documentation for the term under the cursor at line Line, character Char in the file Path.
  223help_at_position(Path, Line1, Char0, S) :-
  224    clause_in_file_at_position(Clause, Path, line_char(Line1, Char0)),
  225    predicate_help(Path, Clause, S0),
  226    format_help(S0, S).
 format_help(+Help0, -Help1) is det
Reformat help string, so the first line is the signature of the predicate.
  231format_help(HelpFull, Help) :-
  232    split_string(HelpFull, "\n", " ", Lines0),
  233    exclude([Line]>>string_concat("Availability: ", _, Line),
  234            Lines0, Lines1),
  235    exclude(=(""), Lines1, Lines2),
  236    Lines2 = [HelpShort|_],
  237    split_string(HelpFull, "\n", " ", HelpLines),
  238    selectchk(HelpShort, HelpLines, "", HelpLines0),
  239    append([HelpShort], HelpLines0, HelpLines1),
  240    atomic_list_concat(HelpLines1, "\n", Help).
  241
  242predicate_help(_, Pred, Help) :-
  243    nonvar(Pred),
  244    help_objects(Pred, exact, Matches), !,
  245    catch(help_html(Matches, exact-exact, HtmlDoc), _, fail),
  246    setup_call_cleanup(open_string(HtmlDoc, In),
  247                       load_html(stream(In), Dom, []),
  248                       close(In)),
  249    with_output_to(string(Help), html_text(Dom)).
  250predicate_help(HerePath, Pred, Help) :-
  251    xref_source(HerePath),
  252    name_callable(Pred, Callable),
  253    xref_defined(HerePath, Callable, Loc),
  254    location_path(HerePath, Loc, Path),
  255    once(xref_comment(Path, Callable, Summary, Comment)),
  256    pldoc_process:parse_comment(Comment, Path:0, Parsed),
  257    memberchk(mode(Signature, Mode), Parsed),
  258    memberchk(predicate(_, Summary, _), Parsed),
  259    format(string(Help), "  ~w is ~w.~n~n~w", [Signature, Mode, Summary]).
  260predicate_help(_, Pred/_Arity, Help) :-
  261    help_objects(Pred, dwim, Matches), !,
  262    catch(help_html(Matches, dwim-Pred, HtmlDoc), _, fail),
  263    setup_call_cleanup(open_string(HtmlDoc, In),
  264                       load_html(stream(In), Dom, []),
  265                       close(In)),
  266    with_output_to(string(Help), html_text(Dom)).
  267
  268location_path(HerePath, local(_), HerePath).
  269location_path(_, imported(Path), Path).
  270
  271linechar_offset(Stream, line_char(Line1, Char0), Offset) :-
  272    seek(Stream, 0, bof, _),
  273    seek_to_line(Stream, Line1),
  274    seek(Stream, Char0, current, Offset).
  275
  276seek_to_line(Stream, N) :-
  277    N > 1, !,
  278    skip(Stream, 0'\n),
  279    NN is N - 1,
  280    seek_to_line(Stream, NN).
  281seek_to_line(_, _).
  282
  283clause_variable_positions(Path, Line, Variables) :-
  284    file_lines_start_end(Path, LineCharRange),
  285    read_term_positions(Path, TermsWithPositions),
  286    % find the top-level term that the offset falls within
  287    file_offset_line_position(LineCharRange, Offset, Line, 0),
  288    member(TermInfo, TermsWithPositions),
  289    SubTermPoses = TermInfo.subterm,
  290    arg(1, SubTermPoses, TermFrom),
  291    arg(2, SubTermPoses, TermTo),
  292    between(TermFrom, TermTo, Offset), !,
  293    find_in_term_with_positions(
  294        [X, _]>>( \+ \+ ( X = '$var'(Name), ground(Name) ) ),
  295        TermInfo.term,
  296        TermInfo.subterm,
  297        VariablesPositions, []
  298    ),
  299    findall(
  300        VarName-Locations,
  301        group_by(
  302            VarName,
  303            Location,
  304            ( member(found_at('$var'(VarName), Location0-_), VariablesPositions),
  305              file_offset_line_position(LineCharRange, Location0, L1, C),
  306              succ(L0, L1),
  307              Location = position(L0, C)
  308            ),
  309            Locations
  310        ),
  311        Variables).
  312
  313clause_in_file_at_position(Clause, Path, Position) :-
  314    xref_source(Path),
  315    findall(Op, xref_op(Path, Op), Ops),
  316    setup_call_cleanup(
  317        open(Path, read, Stream, []),
  318        clause_at_position(Stream, Ops, Clause, Position),
  319        close(Stream)
  320    ).
  321
  322clause_at_position(Stream, Ops, Clause, Start) :-
  323    linechar_offset(Stream, Start, Offset), !,
  324    clause_at_position(Stream, Ops, Clause, Start, Offset).
  325clause_at_position(Stream, Ops, Clause, line_char(Line1, Char), Here) :-
  326    read_source_term_at_location(Stream, Terms, [line(Line1),
  327                                                 subterm_positions(SubPos),
  328                                                 operators(Ops),
  329                                                 error(Error)]),
  330    extract_clause_at_position(Stream, Ops, Terms, line_char(Line1, Char), Here,
  331                               SubPos, Error, Clause).
  332
  333extract_clause_at_position(Stream, Ops, _, line_char(Line1, Char), Here, _,
  334                           Error, Clause) :-
  335    nonvar(Error), !, Line1 > 1,
  336    LineBack is Line1 - 1,
  337    clause_at_position(Stream, Ops, Clause, line_char(LineBack, Char), Here).
  338extract_clause_at_position(_, _, Terms, _, Here, SubPos, _, Clause) :-
  339    once(find_clause(Terms, Here, SubPos, Clause)).
 find_clause(+Term:term, ?Offset:int, +Position:position, ?Subclause) is nondet
True when Subclause is a subclause of Term at offset Offset and Position is the term positions for Term as given by read_term/3 with =subterm_positions(Position)=.
  345find_clause(Term, Offset, F-T, Clause) :-
  346    between(F, T, Offset),
  347    ground(Term), Clause = Term/0.
  348find_clause(Term, Offset, term_position(_, _, FF, FT, _), Name/Arity) :-
  349    between(FF, FT, Offset),
  350    functor(Term, Name, Arity).
  351find_clause(Term, Offset, term_position(F, T, _, _, SubPoses), Clause) :-
  352    between(F, T, Offset),
  353    Term =.. [_|SubTerms],
  354    find_containing_term(Offset, SubTerms, SubPoses, SubTerm, SubPos),
  355    find_clause(SubTerm, Offset, SubPos, Clause).
  356find_clause(Term, Offset, parentheses_term_position(F, T, SubPoses), Clause) :-
  357    between(F, T, Offset),
  358    find_clause(Term, Offset, SubPoses, Clause).
  359find_clause({SubTerm}, Offset, brace_term_position(F, T, SubPos), Clause) :-
  360    between(F, T, Offset),
  361    find_clause(SubTerm, Offset, SubPos, Clause).
  362
  363find_containing_term(Offset, [Term|_], [F-T|_], Term, F-T) :-
  364    between(F, T, Offset).
  365find_containing_term(Offset, [Term|_], [P|_], Term, P) :-
  366    P = term_position(F, T, _, _, _),
  367    between(F, T, Offset), !.
  368find_containing_term(Offset, [Term|_], [PP|_], Term, P) :-
  369    PP = parentheses_term_position(F, T, P),
  370    between(F, T, Offset), !.
  371find_containing_term(Offset, [BTerm|_], [BP|_], Term, P) :-
  372    BP = brace_term_position(F, T, P),
  373    {Term} = BTerm,
  374    between(F, T, Offset).
  375find_containing_term(Offset, [Terms|_], [LP|_], Term, P) :-
  376    LP = list_position(_F, _T, Ps, _),
  377    find_containing_term(Offset, Terms, Ps, Term, P).
  378find_containing_term(Offset, [Dict|_], [DP|_], Term, P) :-
  379    DP = dict_position(_, _, _, _, Ps),
  380    member(key_value_position(_F, _T, _SepF, _SepT, Key, _KeyPos, ValuePos),
  381           Ps),
  382    get_dict(Key, Dict, Value),
  383    find_containing_term(Offset, [Value], [ValuePos], Term, P).
  384find_containing_term(Offset, [_|Ts], [_|Ps], T, P) :-
  385    find_containing_term(Offset, Ts, Ps, T, P)