View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2018-2022, CWI Amsterdam
    7			      SWI-Prolog Solutions b.v.
    8    All rights reserved.
    9
   10    Redistribution and use in source and binary forms, with or without
   11    modification, are permitted provided that the following conditions
   12    are met:
   13
   14    1. Redistributions of source code must retain the above copyright
   15       notice, this list of conditions and the following disclaimer.
   16
   17    2. Redistributions in binary form must reproduce the above copyright
   18       notice, this list of conditions and the following disclaimer in
   19       the documentation and/or other materials provided with the
   20       distribution.
   21
   22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   33    POSSIBILITY OF SUCH DAMAGE.
   34*/
   35
   36:- module(prolog_help,
   37	  [ help/0,
   38	    help/1,                     % +Object
   39	    apropos/1                   % +Search
   40	  ]).   41:- use_module(library(pldoc), []).   42:- autoload(library(apply), [maplist/3]).   43:- autoload(library(error), [must_be/2]).   44:- autoload(library(isub), [isub/4]).   45:- autoload(library(lists), [append/3, sum_list/2]).   46:- autoload(library(pairs), [pairs_values/2]).   47:- autoload(library(porter_stem), [tokenize_atom/2]).   48:- autoload(library(process), [process_create/3]).   49:- autoload(library(sgml), [load_html/3]).   50:- autoload(library(solution_sequences), [distinct/1]).   51:- autoload(library(http/html_write), [html/3, print_html/1]).   52:- autoload(library(lynx/html_text), [html_text/2]).   53:- autoload(pldoc(doc_man), [man_page/4]).   54:- autoload(pldoc(doc_modes), [mode/2]).   55:- autoload(pldoc(doc_words), [doc_related_word/3]).   56:- autoload(pldoc(man_index), [man_object_property/2, doc_object_identifier/2]).   57:- autoload(library(prolog_code), [pi_head/2]).   58:- autoload(library(prolog_xref), [xref_source/2]).   59
   60:- use_module(library(lynx/pldoc_style), []).   61
   62/** <module> Text based manual
   63
   64This module provides help/1 and apropos/1 that   give help on a topic or
   65searches the manual for relevant topics.
   66
   67By default the result of  help/1  is   sent  through  a  _pager_ such as
   68`less`. This behaviour is controlled by the following:
   69
   70  - The Prolog flag `help_pager`, which can be set to one of the
   71    following values:
   72
   73    - false
   74    Never use a pager.
   75    - default
   76    Use default behaviour.  This tries to determine whether Prolog
   77    is running interactively in an environment that allows for
   78    a pager.  If so it examines the environment variable =PAGER=
   79    or otherwise tries to find the `less` program.
   80    - Callable
   81    A Callable term is interpreted as program_name(Arg, ...).  For
   82    example, `less('-r')` would be the default.  Note that the
   83    program name can be an absolute path if single quotes are
   84    used.
   85*/
   86
   87:- meta_predicate
   88    with_pager(0).   89
   90:- multifile
   91    show_html_hook/1.   92
   93% one of `default`, `false`, an executable or executable(options), e.g.
   94% less('-r').
   95:- create_prolog_flag(help_pager, default,
   96		      [ type(term),
   97			keep(true)
   98		      ]).   99
  100%!  help is det.
  101%!  help(+What) is det.
  102%
  103%   Show help for What. What is a   term that describes the topics(s) to
  104%   give help for.  Notations for What are:
  105%
  106%     - Atom
  107%       This ambiguous form is most commonly used and shows all
  108%       matching documents.  For example:
  109%
  110%           ?- help(append).
  111%
  112%     - Name/Arity
  113%       Give help on predicates with matching Name/Arity.  Arity may
  114%       be unbound.
  115%     - Name//Arity
  116%       Give help on the matching DCG rule (non-terminal)
  117%     - f(Name/Arity)
  118%       Give help on the matching Prolog arithmetic functions.
  119%     - c(Name)
  120%       Give help on the matching C interface function
  121%     - section(Label)
  122%       Show the section from the manual with matching Label.
  123%
  124%   If an exact match fails this predicates attempts fuzzy matching and,
  125%   when successful, display the results headed   by  a warning that the
  126%   matches are based on fuzzy matching.
  127%
  128%   If possible, the results are sent  through   a  _pager_  such as the
  129%   `less` program. This behaviour is  controlled   by  the  Prolog flag
  130%   `help_pager`. See section level documentation.
  131%
  132%   @see apropos/1 for searching the manual names and summaries.
  133
  134help :-
  135    notrace(show_matches([help/1, apropos/1], exact-help)).
  136
  137help(What) :-
  138    notrace(help_no_trace(What)).
  139
  140help_no_trace(What) :-
  141    help_objects_how(What, Matches, How),
  142    !,
  143    show_matches(Matches, How-What).
  144help_no_trace(What) :-
  145    print_message(warning, help(not_found(What))).
  146
  147show_matches(Matches, HowWhat) :-
  148    help_html(Matches, HowWhat, HTML),
  149    !,
  150    show_html(HTML).
  151
  152%!  show_html_hook(+HTML:string) is semidet.
  153%
  154%   Hook called to display the  extracted   HTML  document. If this hook
  155%   fails the HTML is rendered  to  the   console  as  plain  text using
  156%   html_text/2.
  157
  158show_html(HTML) :-
  159    show_html_hook(HTML),
  160    !.
  161show_html(HTML) :-
  162    setup_call_cleanup(
  163	open_string(HTML, In),
  164	load_html(stream(In), DOM, []),
  165	close(In)),
  166    page_width(PageWidth),
  167    LineWidth is PageWidth - 4,
  168    with_pager(html_text(DOM, [width(LineWidth)])).
  169
  170help_html(Matches, How, HTML) :-
  171    phrase(html(html([ head([]),
  172		       body([ \match_type(How),
  173			      dl(\man_pages(Matches,
  174					    [ no_manual(fail),
  175					      links(false),
  176					      link_source(false),
  177					      navtree(false),
  178					      server(false)
  179					    ]))
  180			    ])
  181		     ])),
  182	   Tokens),
  183    !,
  184    with_output_to(string(HTML),
  185		   print_html(Tokens)).
  186
  187match_type(exact-_) -->
  188    [].
  189match_type(dwim-For) -->
  190    html(p(class(warning),
  191	   [ 'WARNING: No matches for "', span(class('help-query'), For),
  192	     '" Showing closely related results'
  193	   ])).
  194
  195man_pages([], _) -->
  196    [].
  197man_pages([H|T], Options) -->
  198    man_page(H, Options),
  199    man_pages(T, Options).
  200
  201page_width(Width) :-
  202    tty_width(W),
  203    Width is min(100,max(50,W)).
  204
  205%!  tty_width(-Width) is det.
  206%
  207%   Return the believed width of the terminal.   If we do not know Width
  208%   is bound to 80.
  209
  210tty_width(W) :-
  211    \+ running_under_emacs,
  212    catch(tty_size(_, W), _, fail),
  213    !.
  214tty_width(80).
  215
  216help_objects_how(Spec, Objects, exact) :-
  217    help_objects(Spec, exact, Objects),
  218    !.
  219help_objects_how(Spec, Objects, dwim) :-
  220    help_objects(Spec, dwim, Objects),
  221    !.
  222
  223help_objects(Spec, How, Objects) :-
  224    findall(ID-Obj, help_object(Spec, How, Obj, ID), Objects0),
  225    Objects0 \== [],
  226    sort(1, @>, Objects0, Objects1),
  227    pairs_values(Objects1, Objects2),
  228    sort(Objects2, Objects).
  229
  230help_object(Fuzzy/Arity, How, Name/Arity, ID) :-
  231    match_name(How, Fuzzy, Name),
  232    man_object_property(Name/Arity, id(ID)).
  233help_object(Fuzzy//Arity, How, Name//Arity, ID) :-
  234    match_name(How, Fuzzy, Name),
  235    man_object_property(Name//Arity, id(ID)).
  236help_object(Fuzzy/Arity, How, f(Name/Arity), ID) :-
  237    match_name(How, Fuzzy, Name),
  238    man_object_property(f(Name/Arity), id(ID)).
  239help_object(Fuzzy, How, Name/Arity, ID) :-
  240    atom(Fuzzy),
  241    match_name(How, Fuzzy, Name),
  242    man_object_property(Name/Arity, id(ID)).
  243help_object(Fuzzy, How, Name//Arity, ID) :-
  244    atom(Fuzzy),
  245    match_name(How, Fuzzy, Name),
  246    man_object_property(Name//Arity, id(ID)).
  247help_object(Fuzzy, How, f(Name/Arity), ID) :-
  248    atom(Fuzzy),
  249    match_name(How, Fuzzy, Name),
  250    man_object_property(f(Name/Arity), id(ID)).
  251help_object(Fuzzy, How, c(Name), ID) :-
  252    atom(Fuzzy),
  253    match_name(How, Fuzzy, Name),
  254    man_object_property(c(Name), id(ID)).
  255help_object(SecID, _How, section(Label), ID) :-
  256    atom(SecID),
  257    (   atom_concat('sec:', SecID, Label)
  258    ;   sub_atom(SecID, _, _, 0, '.html'),
  259	Label = SecID
  260    ),
  261    man_object_property(section(_Level,_Num,Label,_File), id(ID)).
  262help_object(Func, How, c(Name), ID) :-
  263    compound(Func),
  264    compound_name_arity(Func, Fuzzy, 0),
  265    match_name(How, Fuzzy, Name),
  266    man_object_property(c(Name), id(ID)).
  267% for currently loaded predicates
  268help_object(Module, _How, Name/Arity, _ID) :-
  269    atom(Module),
  270    current_module(Module),
  271    atom_concat('sec:', Module, SecLabel),
  272    \+ man_object_property(section(_,_,SecLabel,_), _), % not a section
  273    current_predicate_help(Module:Name/Arity).
  274help_object(Name/Arity, _How, Name/Arity, _ID) :-
  275    atom(Name),
  276    current_predicate_help(_:Name/Arity).
  277help_object(Fuzzy, How, Name/Arity, _ID) :-
  278    atom(Fuzzy),
  279    match_name(How, Fuzzy, Name),
  280    current_predicate_help(_:Name/Arity).
  281
  282%!  current_predicate_help(?PI) is nondet.
  283%
  284%   True when we have documentation on  PI.   First  we decide we have a
  285%   definition  for  PI,  then  we  check    whether   or  not  we  have
  286%   documentation for the module in which PI  resides. If not, we switch
  287%   to documentation collect mode and reload the file that defines PI.
  288
  289current_predicate_help(M:Name/Arity) :-
  290    current_predicate(M:Name/Arity),
  291    pi_head(Name/Arity,Head),
  292    \+ predicate_property(M:Head, imported_from(_)),
  293    \+ man_object_property(Name/Arity, _), % must not be indexed already
  294    (   mode(M:_, _)             % Some predicates are documented
  295    ->  true
  296    ;   \+ module_property(M, class(system)),
  297	predicate_property(M:Head,file(File)),
  298	xref_source(File,[comments(store)])
  299    ),
  300    mode(M:Head, _).             % Test that our predicate is documented
  301
  302match_name(exact, Name, Name).
  303match_name(dwim,  Name, Fuzzy) :-
  304    freeze(Fuzzy, dwim_match(Fuzzy, Name)).
  305
  306
  307%!  with_pager(+Goal)
  308%
  309%   Send the current output of Goal through a  pager. If no pager can be
  310%   found we simply dump the output to the current output.
  311
  312with_pager(Goal) :-
  313    pager_ok(Pager, Options),
  314    !,
  315    Catch = error(io_error(_,_), _),
  316    current_output(OldIn),
  317    setup_call_cleanup(
  318	process_create(Pager, Options,
  319		       [stdin(pipe(In))]),
  320	( set_stream(In, tty(true)),
  321	  set_output(In),
  322	  catch(Goal, Catch, true)
  323	),
  324	( set_output(OldIn),
  325	  close(In, [force(true)])
  326	)).
  327with_pager(Goal) :-
  328    call(Goal).
  329
  330pager_ok(_Path, _Options) :-
  331    current_prolog_flag(help_pager, false),
  332    !,
  333    fail.
  334pager_ok(Path, Options) :-
  335    current_prolog_flag(help_pager, default),
  336    !,
  337    stream_property(current_output, tty(true)),
  338    \+ running_under_emacs,
  339    (   distinct((   getenv('PAGER', Pager)
  340		 ;   Pager = less
  341		 )),
  342	absolute_file_name(path(Pager), Path,
  343			   [ access(execute),
  344			     file_errors(fail)
  345			   ])
  346    ->  pager_options(Path, Options)
  347    ).
  348pager_ok(Path, Options) :-
  349    current_prolog_flag(help_pager, Term),
  350    callable(Term),
  351    compound_name_arguments(Term, Pager, Options),
  352    absolute_file_name(path(Pager), Path,
  353			   [ access(execute),
  354			     file_errors(fail)
  355			   ]).
  356
  357pager_options(Path, Options) :-
  358    file_base_name(Path, File),
  359    file_name_extension(Base, _, File),
  360    downcase_atom(Base, Id),
  361    pager_default_options(Id, Options).
  362
  363pager_default_options(less, ['-r']).
  364
  365
  366%!  running_under_emacs
  367%
  368%   True when we believe to be running  in Emacs. Unfortunately there is
  369%   no easy unambiguous way to tell.
  370
  371running_under_emacs :-
  372    current_prolog_flag(emacs_inferior_process, true),
  373    !.
  374running_under_emacs :-
  375    getenv('TERM', dumb),
  376    !.
  377running_under_emacs :-
  378    current_prolog_flag(toplevel_prompt, P),
  379    sub_atom(P, _, _, _, 'ediprolog'),
  380    !.
  381
  382
  383%!  apropos(+Query) is det.
  384%
  385%   Print objects from the  manual  whose   name  or  summary match with
  386%   Query. Query takes one of the following forms:
  387%
  388%     - Type:Text
  389%       Find objects matching Text and filter the results by Type.
  390%       Type matching is a case intensitive _prefix_ match.
  391%       Defined types are `section`, `cfunction`, `function`,
  392%       `iso_predicate`, `swi_builtin_predicate`, `library_predicate`,
  393%       `dcg` and aliases `chapter`, `arithmetic`, `c_function`,
  394%       `predicate`, `nonterminal` and `non_terminal`.  For example:
  395%
  396%           ?- apropos(c:close).
  397%           ?- apropos(f:min).
  398%
  399%     - Text
  400%       Text is broken into tokens.  A topic matches if all tokens
  401%       appear in the name or summary of the topic. Matching is
  402%	case insensitive.  Results are ordered depending on the
  403%	quality of the match.
  404
  405apropos(Query) :-
  406    notrace(apropos_no_trace(Query)).
  407
  408apropos_no_trace(Query) :-
  409    findall(Q-(Obj-Summary), apropos(Query, Obj, Summary, Q), Pairs),
  410    (   Pairs == []
  411    ->  print_message(warning, help(no_apropos_match(Query)))
  412    ;   sort(1, >=, Pairs, Sorted),
  413	length(Sorted, Len),
  414	(   Len > 20
  415	->  length(Truncated, 20),
  416	    append(Truncated, _, Sorted)
  417	;   Truncated = Sorted
  418	),
  419	pairs_values(Truncated, Matches),
  420	print_message(information, help(apropos_matches(Matches, Len)))
  421    ).
  422
  423apropos(Query, Obj, Summary, Q) :-
  424    parse_query(Query, Type, Words),
  425    man_object_property(Obj, summary(Summary)),
  426    apropos_match(Type, Words, Obj, Summary, Q).
  427
  428parse_query(Type:String, Type, Words) :-
  429    !,
  430    must_be(atom, Type),
  431    must_be(text, String),
  432    tokenize_atom(String, Words).
  433parse_query(String, _Type, Words) :-
  434    must_be(text, String),
  435    tokenize_atom(String, Words).
  436
  437apropos_match(Type, Query, Object, Summary, Q) :-
  438    maplist(amatch(Object, Summary), Query, Scores),
  439    match_object_type(Type, Object),
  440    sum_list(Scores, Q).
  441
  442amatch(Object, Summary, Query, Score) :-
  443    (   doc_object_identifier(Object, String)
  444    ;   String = Summary
  445    ),
  446    amatch(Query, String, Score),
  447    !.
  448
  449amatch(Query, To, Quality) :-
  450    doc_related_word(Query, Related, Distance),
  451    sub_atom_icasechk(To, _, Related),
  452    isub(Related, To, false, Quality0),
  453    Quality is Quality0*Distance.
  454
  455match_object_type(Type, _Object) :-
  456    var(Type),
  457    !.
  458match_object_type(Type, Object) :-
  459    downcase_atom(Type, LType),
  460    object_class(Object, Class),
  461    match_object_class(LType, Class).
  462
  463match_object_class(Type, Class) :-
  464    (   TheClass = Class
  465    ;   class_alias(Class, TheClass)
  466    ),
  467    sub_atom(TheClass, 0, _, _, Type),
  468    !.
  469
  470class_alias(section,               chapter).
  471class_alias(function,              arithmetic).
  472class_alias(cfunction,             c_function).
  473class_alias(iso_predicate,         predicate).
  474class_alias(swi_builtin_predicate, predicate).
  475class_alias(library_predicate,     predicate).
  476class_alias(dcg,                   predicate).
  477class_alias(dcg,                   nonterminal).
  478class_alias(dcg,                   non_terminal).
  479
  480class_tag(section,               'SEC').
  481class_tag(function,              '  F').
  482class_tag(iso_predicate,         'ISO').
  483class_tag(swi_builtin_predicate, 'SWI').
  484class_tag(library_predicate,     'LIB').
  485class_tag(dcg,                   'DCG').
  486
  487object_class(section(_Level, _Num, _Label, _File), section).
  488object_class(c(_Name), cfunction).
  489object_class(f(_Name/_Arity), function).
  490object_class(Name/Arity, Type) :-
  491    functor(Term, Name, Arity),
  492    (   current_predicate(system:Name/Arity),
  493	predicate_property(system:Term, built_in)
  494    ->  (   predicate_property(system:Term, iso)
  495	->  Type = iso_predicate
  496	;   Type = swi_builtin_predicate
  497	)
  498    ;   Type = library_predicate
  499    ).
  500object_class(_M:_Name/_Arity, library_predicate).
  501object_class(_Name//_Arity, dcg).
  502object_class(_M:_Name//_Arity, dcg).
  503
  504
  505		 /*******************************
  506		 *            MESSAGES		*
  507		 *******************************/
  508
  509:- multifile prolog:message//1.  510
  511prolog:message(help(not_found(What))) -->
  512    [ 'No help for ~p.'-[What], nl,
  513      'Use ?- apropos(query). to search for candidates.'-[]
  514    ].
  515prolog:message(help(no_apropos_match(Query))) -->
  516    [ 'No matches for ~p'-[Query] ].
  517prolog:message(help(apropos_matches(Pairs, Total))) -->
  518    { tty_width(W),
  519      Width is max(30,W),
  520      length(Pairs, Count)
  521    },
  522    matches(Pairs, Width),
  523    (   {Count =:= Total}
  524    ->  []
  525    ;   [ nl,
  526	  ansi(fg(red), 'Showing ~D of ~D matches', [Count,Total]), nl, nl,
  527	  'Use ?- apropos(Type:Query) or multiple words in Query '-[], nl,
  528	  'to restrict your search.  For example:'-[], nl, nl,
  529	  '  ?- apropos(iso:open).'-[], nl,
  530	  '  ?- apropos(\'open file\').'-[]
  531	]
  532    ).
  533
  534matches([], _) --> [].
  535matches([H|T], Width) -->
  536    match(H, Width),
  537    (   {T == []}
  538    ->  []
  539    ;   [nl],
  540	matches(T, Width)
  541    ).
  542
  543match(Obj-Summary, Width) -->
  544    { Left is min(40, max(20, round(Width/3))),
  545      Right is Width-Left-2,
  546      man_object_summary(Obj, ObjS, Tag),
  547      write_length(ObjS, LenObj, [portray(true), quoted(true)]),
  548      Spaces0 is Left - LenObj - 4,
  549      (   Spaces0 > 0
  550      ->  Spaces = Spaces0,
  551	  SummaryLen = Right
  552      ;   Spaces = 1,
  553	  SummaryLen is Right + Spaces0 - 1
  554      ),
  555      truncate(Summary, SummaryLen, SummaryE)
  556    },
  557    [ ansi([fg(default)], '~w ~p', [Tag, ObjS]),
  558      '~|~*+~w'-[Spaces, SummaryE]
  559%     '~*|~w'-[Spaces, SummaryE]		% Should eventually work
  560    ].
  561
  562truncate(Summary, Width, SummaryE) :-
  563    string_length(Summary, SL),
  564    SL > Width,
  565    !,
  566    Pre is Width-4,
  567    sub_string(Summary, 0, Pre, _, S1),
  568    string_concat(S1, " ...", SummaryE).
  569truncate(Summary, _, Summary).
  570
  571man_object_summary(section(_Level, _Num, Label, _File), Obj, 'SEC') :-
  572    atom_concat('sec:', Obj, Label),
  573    !.
  574man_object_summary(section(0, _Num, File, _Path), File, 'SEC') :- !.
  575man_object_summary(c(Name), Obj, '  C') :- !,
  576    compound_name_arguments(Obj, Name, []).
  577man_object_summary(f(Name/Arity), Name/Arity, '  F') :- !.
  578man_object_summary(Obj, Obj, Tag) :-
  579    (   object_class(Obj, Class),
  580	class_tag(Class, Tag)
  581    ->  true
  582    ;   Tag = '  ?'
  583    ).
  584
  585		 /*******************************
  586		 *            SANDBOX		*
  587		 *******************************/
  588
  589sandbox:safe_primitive(prolog_help:apropos(_)).
  590sandbox:safe_primitive(prolog_help:help(_))