(* Help -- a simple Moscow ML library browser, PS 1995-04-30, 2000-04-22 Invoking `help s' has the following effect: (a) s is normalized to lower case (b) if s is the empty string, then the help browser is invoked with the message !welcome; (c) otherwise, if the term s appears in !specialfiles, then the browser is invoked with the file associated with s; (d) otherwise, if the term appears in one of the index files in !indexfiles, then the help system asks the user to choose which help file to load, in case there are several, and invokes the browser with that file; (e) otherwise, a standard error message is shown. The help system looks for help files in the -stdlib and in the directories in !helpdirs. The browser can open a file on a specified line, attempting to center that lines in the display (assuming the display can hold !displayLines lines at a time). The browser accepts several commands for moving forth and back in the current file. The browser's search facility cyclically searches (case-insensitive) for occurrences of a given string and displays the line in which the string was found, as close to the center of the display (or portion displayed) as possible. Several hacks are being used to avoid loading too many other libraries along with Help. *) (* The number of lines to show interactively: *) val displayLines = ref 24 (* Additional directories to search for help files *) val helpdirs = ref [] : string list ref #ifdef macintosh val slash = #":" #else val slash = #"/" #endif fun joinDirFile dir file = let open String in if dir <> "" andalso sub(dir, size dir - 1) = slash then dir ^ file else dir ^ str slash ^ file end (* Find the standard library directory: *) fun getstdlib () = let open Vector prim_val argv_ : string vector = 0 "command_line"; val stop = length argv_ - 1; fun h i = if i < stop then if sub(argv_, i) = "-stdlib" then sub(argv_, i+1) else h (i+1) else raise Fail "Cannot find the standard libraries!" in h 0 end; (* Full path of the signature index database: *) val indexfiles = ref [joinDirFile (getstdlib ()) "helpsigs.val"] (* Mapping particular search terms to non-.sig files: *) val specialfiles = ref [{term="lib", file="README", title="Overview"}]; (* The help system's response to help "" : *) val welcome = ref #["Moscow ML library browser: \n", "\n", " help \"lib\"; gives an overview of the library units\n", " help \"id\"; provides help on identifier id\n", "\n"] local fun print s = TextIO.print s (* The database reading and searching functions. Included here only * to avoid loading yet another structure. Types MUST agree with unit * Database in mosml/examples/helpsigs. *) datatype component = Str (* structure *) | Exc of string (* exception constructor with name *) | Typ of string (* type constructor with name *) | Val of string (* value with name *) | Con of string (* value constructor with name *) | Term of string * string option (* term and optional kind *) (* An entry consist of a component and the name of its structure: *) type entry = { comp : component, file : string, line : int } datatype 'contents table = Empty | Node of string * 'contents * 'contents table * 'contents table type database = entry list table fun readbase filename = let prim_type in_channel type instream_ = { closed: bool, ic: in_channel } ref prim_val input_value_ : in_channel -> 'a = 1 "intern_val" prim_val fromI : BasicIO.instream -> instream_ = 1 "identity" fun input_value is = let val ref {closed, ic} = fromI is in if closed then raise SysErr("Input stream is closed", NONE) else input_value_ ic end val is = BasicIO.open_in_bin filename val db = input_value is : database in BasicIO.close_in is; db end (* Make sure tilde gets collated as a symbol, before "A": *) fun caseless(#"~", #"~") = EQUAL | caseless(#"~", c2) = if Char.toLower c2 < #"a" then GREATER else LESS | caseless(c1, #"~") = if Char.toLower c1 < #"a" then LESS else GREATER | caseless(c1, c2) = Char.compare(Char.toLower c1, Char.toLower c2) val keycompare = String.collate caseless fun lookup(db : database, sought : string) = let fun look Empty = [] | look (Node(key, value, t1, t2)) = case keycompare(sought, key) of LESS => look t1 | GREATER => look t2 | EQUAL => value in look db end (* Auxiliaries: *) fun min (x, y) = if x < y then x else y : int; fun max (x, y) = if x < y then y else x : int; fun natFromString s = let fun skipWS [] = [] | skipWS (cs as (c::cr)) = if Char.isSpace c then skipWS cr else cs fun decval c = Char.ord c - 48 fun h [] res = SOME res | h (c::cr) res = if Char.isDigit c then h cr (decval c + 10 * res) else SOME res in case skipWS (String.explode s) of [] => NONE | c::cr => if Char.isDigit c then h cr (decval c) else NONE end fun natToString n = (if n > 9 then natToString (n div 10) else "") ^ String.str (Char.chr(48 + n mod 10)) fun normalize [] = [] | normalize (#"\n" :: _) = [] | normalize (c :: cr) = Char.toLower c :: normalize cr fun toLower s = String.implode (normalize (String.explode s)) (* The signature browser: *) fun show name centerline initiallySought (strs : string Vector.vector) = let prim_val sub_ : string -> int -> char = 2 "get_nth_char"; prim_val int_to_string : int -> string = 1 "sml_string_of_int"; val lines = Vector.length strs val sought = ref initiallySought fun instr s str = let val len = String.size s fun eq j k = j >= len orelse sub_ s j = Char.toLower (sub_ str k) andalso eq (j+1) (k+1) val stop = String.size str - len fun cmp k = k<=stop andalso (eq 0 k orelse cmp(k+1)) in cmp 0 end; fun occurshere str = case !sought of NONE => false | SOME s => instr s str fun findline s curr = let fun h i = if i >= lines then NONE else if instr s (Vector.sub(strs, (i+curr) mod lines)) then SOME ((i + curr) mod lines) else h(i+1) in h 0 end val portion = max(!displayLines, 5) - 1 fun wait next = let val prompt = "---- " ^ name ^ "[" ^ int_to_string((100 * next) div lines) ^ "%]: down, up, bottom, top, /(find), next, quit: " fun toend () = (print "\n....\n"; nextpart (lines - portion) portion) fun tobeg () = (print "\n....\n"; nextpart 0 portion) fun up () = (print "\n....\n"; nextpart (next-3*portion div 2) portion) fun down () = if next=lines then toend() else nextpart next (portion div 2) fun find s = case findline s next of NONE => (print ("**** String \"" ^ s ^ "\" not found\n"); wait next) | SOME line => (print "\n....\n"; nextpart (line - portion div 2) portion) fun search chars = let val s = String.implode (normalize chars) in sought := SOME s; find s end fun findnext () = (case !sought of NONE => (print "**** No previous search string\n"; wait next) | SOME s => find s) in print prompt; case String.explode(BasicIO.input_line BasicIO.std_in) of [] => () | #"q" :: _ => () | #"u" :: _ => up () | #"d" :: _ => down () | #"t" :: _ => tobeg () | #"g" :: _ => tobeg () | #"b" :: _ => toend () | #"G" :: _ => toend () | #"/" :: s => search s | #"n" :: s => findnext () | _ => if next=lines then toend () else nextpart next portion end and nextpart first amount = let val start = max(0, min(lines - amount + 1, first)) val stop = min(start + amount, lines) in prt wait start stop end and prt wait i stop = if i >= stop then wait i else let val line = Vector.sub(strs, i) in if occurshere line then print "@>" else print "+ "; print line; prt wait (i+1) stop end in print "\n"; if lines <= portion then prt ignore 0 lines else nextpart (centerline - portion div 2) portion end (* Read a signature file from the standard library: *) fun readfile file = let fun openFile [] = raise Fail ("Help.readFile: help file `" ^ file ^ "' not found") | openFile (dir1 :: dirr) = (BasicIO.open_in (joinDirFile dir1 file)) handle SysErr _ => openFile dirr val is = openFile (getstdlib () :: !helpdirs) fun h () = if BasicIO.end_of_stream is then [] else BasicIO.input_line is :: h () in Vector.fromList (h ()) end; (* Invoke the browser on a particular line of a signature: *) fun showFile sought entry = (case entry of {comp = Str, file, ...} => show file 0 NONE (readfile (file ^ ".sig")) | {comp = Term _, file, line} => show file line NONE (readfile file) | {comp, file, line} => show file line (SOME sought) (readfile (file ^ ".sig"))) handle SysErr _ => raise Fail "Help.showFile: inconsistent help database" (* Let the user select from the menu: *) fun choose sought entries = let val _ = print "\nChoose number to browse, or quit: "; val response = BasicIO.input_line BasicIO.std_in in case natFromString response of NONE => (case String.explode response of [] => () | [#"\n"] => () | #"Q" :: _ => () | #"q" :: _ => () | _ => choose sought entries) | SOME choice => if choice = 0 then () else showFile sought (List.nth(entries, choice - 1)) end handle Subscript => choose sought entries | Overflow => choose sought entries; (* Display the menu of identifiers matching the given one, or * invoke the browser directly if these is only one match: *) fun display sought [] = raise Fail "Help.display" | display sought [entry] = showFile sought entry | display sought (entries as e0::er) = let fun render (entry as {comp, file, ...}) = case comp of Str => "structure " ^ file | Exc id => "exn " ^ file ^ "." ^ id | Typ id => "type " ^ file ^ "." ^ id | Val id => "val " ^ file ^ "." ^ id | Con id => "con " ^ file ^ "." ^ id | Term (id, NONE) => id ^ " (" ^ file ^ ")" | Term (id, SOME kind) => kind ^ " " ^ id ^ " (" ^ file ^ ")" fun maxlen [] max = max | maxlen (e1 :: er) max = let val len = size (render e1) in maxlen er (if len > max then len else max) end val maxwidth = maxlen er (size (render e0)) val boxwidth = 6 + 3 + 3 + maxwidth + 2 val horizontal = StringCvt.padRight #"-" boxwidth " " ^ "\n" fun prline lin [] = () | prline lin (e1 :: rest) = (print " | "; print (StringCvt.padLeft #" " 3 (natToString lin)); print " | "; print (StringCvt.padRight #" " maxwidth (render e1)); print " |\n"; prline (lin+1) rest) in print "\n"; print horizontal; prline 1 entries; print horizontal; choose sought entries end in (* Main help function: search for a string in the signature index database: *) fun defaultBrowser "" = show "help" 0 NONE (!welcome) | defaultBrowser "lib" = show "Overview" 0 NONE (readfile "README") | defaultBrowser id = let fun getdb filename = (readbase filename) handle SysErr _ => raise Fail "Cannot read help database!" val sought = toLower id fun lookone(dbn, res) = lookup(getdb dbn, sought) :: res fun tryspecial [] = (case List.concat(List.foldl lookone [] (!indexfiles)) of [] => print ("\nSorry, no help on identifier `" ^ id ^ "'\n\n") | entries => display sought entries) | tryspecial ({term, file, title} :: rest) = if term = sought then show title 0 NONE (readfile file) else tryspecial rest in tryspecial (!specialfiles) end val browser = ref defaultBrowser fun help s = !browser s end