ni: better display of module documentation
[nit.git] / src / ni.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # ni or nit index, is a command tool used to display documentation
16 module ni
17
18 import model_utils
19
20 private class Pager
21 var content = new Buffer
22 fun add(text: String) do addn("{text}\n")
23 fun addn(text: String) do content.append(text.escape)
24 fun add_rule do add("\n---\n")
25 fun render do sys.system("echo \"{content}\" | pager -r")
26 end
27
28 # Main class of the nit index tool
29 # NitIndex build the model using the toolcontext argument
30 # then wait for query on std in to display documentation
31 class NitIndex
32 private var toolcontext: ToolContext
33 private var model: Model
34 private var mbuilder: ModelBuilder
35 private var mainmodule: MModule
36 private var arguments: Array[String]
37
38 init(toolcontext: ToolContext) do
39 # We need a model to collect stufs
40 self.toolcontext = toolcontext
41 self.toolcontext.option_context.options.clear
42 self.arguments = toolcontext.option_context.rest
43
44 if arguments.is_empty or arguments.length > 2 then
45 print "usage: ni path/to/module.nit [expression]"
46 toolcontext.option_context.usage
47 exit(1)
48 end
49
50 model = new Model
51 mbuilder = new ModelBuilder(model, toolcontext)
52
53 # Here we load an process std modules
54 #var dir = "NIT_DIR".environ
55 #var mmodules = modelbuilder.parse_and_build(["{dir}/lib/standard/standard.nit"])
56 var mmodules = mbuilder.parse_and_build([arguments.first])
57 if mmodules.is_empty then return
58 assert mmodules.length == 1
59 self.mainmodule = mmodules.first
60 end
61
62 fun start do
63 if arguments.length == 1 then
64 welcome
65 prompt
66 else
67 seek(arguments[1])
68 end
69 end
70
71 fun welcome do
72 print "Welcome in the Nit Index."
73 print "\nCommands:"
74 print "\tname\t\tlookup module, class and property with the corresponding 'name'"
75 print "\tparam: Type\tlookup methods using the corresponding 'Type' as parameter"
76 print "\treturn: Type\tlookup methods returning the corresponding 'Type'"
77 print "\tEnter a blank line to exit.\n"
78 print "\nLoaded modules:"
79 var mmodules = new Array[MModule]
80 mmodules.add_all(model.mmodules)
81 var sorter = new MModuleNameSorter
82 sorter.sort(mmodules)
83 for m in mmodules do
84 print "\t{m.name}"
85 end
86 end
87
88 fun prompt do
89 printn ">> "
90 seek(stdin.read_line)
91 end
92
93 fun seek(entry: String) do
94 if entry.is_empty then exit(0)
95 var flag = false
96 # seek return types
97 if entry.has_prefix("return:") then
98 var ret = entry.split_with(":")[1].replace(" ", "")
99 var matches = seek_returns(ret)
100 if not matches.is_empty then
101 flag = true
102 props_fulldoc(matches)
103 end
104 else if entry.has_prefix("param:") then
105 var param = entry.split_with(":")[1].replace(" ", "")
106 var matches = seek_params(param)
107 if not matches.is_empty then
108 flag = true
109 props_fulldoc(matches)
110 end
111 else
112 # seek for modules
113 var mmatches = new List[MModule]
114 for m in model.mmodules do
115 if m.name == entry then
116 flag = true
117 mmatches.add(m)
118 end
119 end
120 if not mmatches.is_empty then modules_fulldoc(mmatches)
121 # seek for classes
122 var cmatches = new List[MClass]
123 for c in model.mclasses do
124 if c.name == entry then
125 flag = true
126 cmatches.add(c)
127 end
128 end
129 if not cmatches.is_empty then classes_fulldoc(cmatches)
130 # seek for properties
131 var matches = new List[MProperty]
132 for p in model.mproperties do
133 if p.name == entry then
134 flag = true
135 matches.add(p)
136 end
137 end
138 if not matches.is_empty then props_fulldoc(matches)
139 end
140 # no matches
141 if not flag then print "Nothing known about '{entry}'"
142 if arguments.length == 1 then prompt
143 end
144
145 private fun modules_fulldoc(mmodules: List[MModule]) do
146 var pager = new Pager
147 for mmodule in mmodules do
148 # name and prototype
149 pager.add("# {mmodule.namespace}\n".bold)
150 # comment
151 if mbuilder.mmodule2nmodule.has_key(mmodule) then
152 var nmodule = mbuilder.mmodule2nmodule[mmodule]
153 if not nmodule.n_moduledecl.n_doc == null then
154 for comment in nmodule.n_moduledecl.n_doc.comment do pager.add(comment.green)
155 end
156 end
157 pager.add("{mmodule.prototype}")
158 pager.add_rule
159 # imports
160 var msorter = new MModuleNameSorter
161 var ms = mmodule.in_importation.greaters.to_a
162 if ms.length > 1 then
163 msorter.sort(ms)
164 pager.add("# imported modules".bold)
165 pager.addn("\t")
166 for i in [0..ms.length[ do
167 if ms[i] == mmodule then continue
168 pager.addn(ms[i].name)
169 if i < ms.length - 1 then pager.addn(", ")
170 end
171 pager.add("\n")
172 end
173 # clients
174 ms = mmodule.in_importation.smallers.to_a
175 if ms.length > 1 then
176 msorter.sort(ms)
177 pager.add("# known modules".bold)
178 pager.addn("\t")
179 for i in [0..ms.length[ do
180 if ms[i] == mmodule then continue
181 pager.addn(ms[i].name)
182 if i < ms.length - 1 then pager.addn(", ")
183 end
184 pager.add("\n")
185 end
186 # local classes and interfaces
187 var sorter = new MClassDefNameSorter
188 var intro_mclassdefs = new Array[MClassDef]
189 var redef_mclassdefs = new Array[MClassDef]
190 for mclassdef in mmodule.mclassdefs do
191 if mclassdef.is_intro then
192 intro_mclassdefs.add(mclassdef)
193 else
194 redef_mclassdefs.add(mclassdef)
195 end
196 end
197 # intro
198 if not intro_mclassdefs.is_empty then
199 sorter.sort(intro_mclassdefs)
200 pager.add("\n# introduced classes".bold)
201 for mclassdef in intro_mclassdefs do
202 pager.add("")
203 var nclass = mbuilder.mclassdef2nclassdef[mclassdef]
204 if nclass isa AStdClassdef and not nclass.n_doc == null and not nclass.n_doc.short_comment.is_empty then
205 pager.add("\t{nclass.n_doc.short_comment.green}")
206 end
207 pager.add("\t{mclassdef.mclass.prototype}")
208 #TODO add redefs?
209 end
210 end
211 # redefs
212 if not redef_mclassdefs.is_empty then
213 sorter.sort(redef_mclassdefs)
214 pager.add("\n# refined classes".bold)
215 for mclassdef in redef_mclassdefs do
216 pager.add("")
217 #TODO intro comment?
218 var nclass = mbuilder.mclassdef2nclassdef[mclassdef]
219 if nclass isa AStdClassdef and not nclass.n_doc == null and not nclass.n_doc.short_comment.is_empty then
220 pager.add("\t# {nclass.n_doc.short_comment.green}")
221 end
222 pager.add("\t{mclassdef.mclass.prototype}")
223 pager.add("\t\t" + "introduced in {mclassdef.mclass.intro.mmodule.namespace.bold}".gray)
224 for odef in mclassdef.mclass.mclassdefs do
225 if odef.is_intro or odef == mclassdef or mclassdef.mmodule == mmodule then continue
226 pager.add("\t\t" + "refined in {mclassdef.mmodule.namespace.bold}".gray)
227 end
228 end
229 end
230 #TODO add inherited classes?
231 end
232 pager.render
233 end
234
235 private fun classes_fulldoc(mclasses: List[MClass]) do
236 var pager = new Pager
237 for mclass in mclasses do
238 var nclass = mbuilder.mclassdef2nclassdef[mclass.intro].as(AStdClassdef)
239
240 pager.add("# {mclass.namespace}\n".bold)
241 pager.add("{mclass.prototype}")
242 if not nclass.n_doc == null then
243 pager.add_rule
244 pager.addn(nclass.n_doc.comment.green)
245 end
246 pager.add_rule
247 if not mclass.parameter_types.is_empty then
248 pager.add("# formal types".bold)
249 for ft, bound in mclass.parameter_types do
250 pager.add("")
251 pager.add("\t{ft.to_s.green}: {bound}")
252 end
253 end
254 if not mclass.virtual_types.is_empty then
255 pager.add("# virtual types".bold)
256 for vt in mclass.virtual_types do
257 pager.add("")
258 mpropdef_fulldoc(pager, vt.intro)
259 end
260 end
261 pager.add_rule
262
263 var cats = new HashMap[String, Collection[MMethod]]
264 cats["constructors"] = mclass.constructors
265 cats["introduced methods"] = mclass.intro_methods
266 cats["refined methods"] = mclass.redef_methods
267 cats["inherited methods"] = mclass.inherited_methods
268
269 for cat, list in cats do
270 if not list.is_empty then
271 #sort list
272 var sorted = new Array[MMethod]
273 sorted.add_all(list)
274 var sorter = new MPropertyNameSorter
275 sorter.sort(sorted)
276 pager.add("\n# {cat}".bold)
277 for mprop in sorted do
278 pager.add("")
279 mpropdef_fulldoc(pager, mprop.intro)
280 end
281 end
282 end
283 pager.add_rule
284 end
285 pager.render
286 end
287
288 private fun props_fulldoc(raw_mprops: List[MProperty]) do
289 var pager = new Pager
290 # group by module
291 var cats = new HashMap[MClass, Array[MProperty]]
292 for mprop in raw_mprops do
293 if not mbuilder.mpropdef2npropdef.has_key(mprop.intro) then continue
294 if mprop isa MAttribute then continue
295 var mclass = mprop.intro_mclassdef.mclass
296 if not cats.has_key(mclass) then cats[mclass] = new Array[MProperty]
297 cats[mclass].add(mprop)
298 end
299 #sort groups
300 var sorter = new MClassNameSorter
301 var sorted = new Array[MClass]
302 sorted.add_all(cats.keys)
303 sorter.sort(sorted)
304 # display
305 for mclass in sorted do
306 var mprops = cats[mclass]
307 pager.add("# {mclass.namespace}".bold)
308 var sorterp = new MPropertyNameSorter
309 sorterp.sort(mprops)
310 for mprop in mprops do
311 pager.add("")
312 mpropdef_fulldoc(pager, mprop.intro)
313 end
314 pager.add_rule
315 end
316 pager.render
317 end
318
319 private fun seek_returns(entry: String): List[MProperty] do
320 # TODO how to match with generic types?
321 var matches = new List[MProperty]
322 for mprop in model.mproperties do
323 var intro = mprop.intro
324 if intro isa MMethodDef then
325 if intro.msignature.return_mtype != null and intro.msignature.return_mtype.to_s == entry then matches.add(mprop)
326 else if intro isa MAttributeDef then
327 if intro.static_mtype.to_s == entry then matches.add(mprop)
328 end
329 end
330 return matches
331 end
332
333 private fun seek_params(entry: String): List[MProperty] do
334 # TODO how to match with generic types?
335 var matches = new List[MProperty]
336 for mprop in model.mproperties do
337 var intro = mprop.intro
338 if intro isa MMethodDef then
339 var mparameters = intro.msignature.mparameters
340 for mparameter in mparameters do
341 if mparameter.mtype.to_s == entry then matches.add(mprop)
342 end
343 else if intro isa MAttributeDef then
344 if intro.static_mtype.to_s == entry then matches.add(mprop)
345 end
346 end
347 return matches
348 end
349
350 private fun mpropdef_fulldoc(pager: Pager, mpropdef: MPropDef) do
351 if mbuilder.mpropdef2npropdef.has_key(mpropdef) then
352 var nprop = mbuilder.mpropdef2npropdef[mpropdef]
353 if not nprop.n_doc == null and not nprop.n_doc.short_comment.is_empty then
354 pager.add("\t# {nprop.n_doc.short_comment}")
355 end
356 end
357 pager.add("\t{mpropdef}")
358 pager.add("\t\t" + "introduced in {mpropdef.mproperty.intro_mclassdef.namespace}".gray)
359 for mpdef in mpropdef.mproperty.mpropdefs do
360 if not mpdef.is_intro then
361 pager.add("\t\t" + "refined in {mpdef.mclassdef.namespace}".gray)
362 end
363 end
364 end
365 end
366
367 # Printing facilities
368
369 redef class MModule
370 # prototype of the module
371 # module ownername::name
372 private fun prototype: String do return "module {name}"
373
374 # namespace of the module
375 # ownername::name
376 private fun namespace: String do
377 if public_owner == null then
378 return self.name
379 else
380 return "{public_owner.namespace}::{self.name}"
381 end
382 end
383 end
384
385 redef class MClass
386 # return the generic signature of the class
387 # [E, F]
388 private fun signature: String do
389 if arity > 0 then
390 return "[{intro.parameter_names.join(", ")}]"
391 else
392 return ""
393 end
394 end
395
396 # return the prototype of the class
397 # class name is displayed with colors depending on visibility
398 # abstract interface Foo[E]
399 private fun prototype: String do
400 var res = new Buffer
401 res.append("{kind} ")
402 if visibility.to_s == "public" then res.append("{name}{signature}".bold.green)
403 if visibility.to_s == "private" then res.append("{name}{signature}".bold.red)
404 if visibility.to_s == "protected" then res.append("{name}{signature}".bold.yellow)
405 return res.to_s
406 end
407
408 private fun namespace: String do
409 if not intro_mmodule.public_owner == null then
410 return "{intro_mmodule.public_owner.name}::{name}"
411 else
412 return "{intro_mmodule.name}::{name}"
413 end
414 end
415 end
416
417 redef class MClassDef
418 private fun namespace: String do
419 return "{mmodule.full_name}::{mclass.name}"
420 end
421 end
422
423 redef class MMethodDef
424 redef fun to_s do
425 var res = new Buffer
426 if not is_intro then res.append("redef ")
427 if not mproperty.is_init then res.append("fun ")
428 if mproperty.visibility.to_s == "public" then res.append(mproperty.name.green)
429 if mproperty.visibility.to_s == "private" then res.append(mproperty.name.red)
430 if mproperty.visibility.to_s == "protected" then res.append(mproperty.name.yellow)
431 if msignature != null then res.append(msignature.to_s)
432 # FIXME: modifiers should be accessible via the model
433 #if self isa ADeferredMethPropdef then ret = "{ret} is abstract"
434 #if self isa AInternMethPropdef then ret = "{ret} is intern"
435 #if self isa AExternMethPropdef then ret = "{ret} is extern"
436 return res.to_s
437 end
438 end
439
440 redef class MVirtualTypeDef
441 redef fun to_s do
442 var res = new Buffer
443 if mproperty.visibility.to_s == "public" then res.append(mproperty.name.green)
444 if mproperty.visibility.to_s == "private" then res.append(mproperty.name.red)
445 if mproperty.visibility.to_s == "protected" then res.append(mproperty.name.yellow)
446 res.append(": {bound.to_s}")
447 return res.to_s
448 end
449 end
450
451 redef class MSignature
452 redef fun to_s do
453 var res = new Buffer
454 if not mparameters.is_empty then
455 res.append("(")
456 for i in [0..mparameters.length[ do
457 res.append(mparameters[i].to_s)
458 if i < mparameters.length - 1 then res.append(", ")
459 end
460 res.append(")")
461 end
462 if return_mtype != null then
463 res.append(": {return_mtype.to_s}")
464 end
465 return res.to_s
466 end
467 end
468
469 redef class MParameter
470 redef fun to_s do
471 var res = new Buffer
472 res.append("{name}: {mtype}")
473 if is_vararg then res.append("...")
474 return res.to_s
475 end
476 end
477
478 redef class MNullableType
479 redef fun to_s do return "nullable {mtype}"
480 end
481
482 redef class MGenericType
483 redef fun to_s do
484 var res = new Buffer
485 res.append("{mclass.name}[")
486 for i in [0..arguments.length[ do
487 res.append(arguments[i].to_s)
488 if i < arguments.length - 1 then res.append(", ")
489 end
490 res.append("]")
491 return res.to_s
492 end
493 end
494
495 redef class MParameterType
496 redef fun to_s do return mclass.intro.parameter_names[rank]
497 end
498
499 redef class MVirtualType
500 redef fun to_s do return mproperty.intro.to_s
501 end
502
503 redef class ADoc
504 private fun comment: List[String] do
505 var res = new List[String]
506 for t in n_comment do
507 res.add(t.text.replace("\n", ""))
508 end
509 return res
510 end
511
512 private fun short_comment: String do
513 return n_comment.first.text.replace("\n", "")
514 end
515 end
516
517 redef class AAttrPropdef
518 private fun read_accessor: String do
519 var ret = "fun "
520 #FIXME bug with standard::stream::FDStream::fd
521 var name = mreadpropdef.mproperty.name
522 if mpropdef.mproperty.visibility.to_s == "public" then ret = "{ret}{name.green}"
523 if mpropdef.mproperty.visibility.to_s == "private" then ret = "{ret}{name.red}"
524 if mpropdef.mproperty.visibility.to_s == "protected" then ret = "{ret}{name.yellow}"
525 ret = "{ret}: {n_type.to_s}"
526 if n_kwredef != null then ret = "redef {ret}"
527 return ret
528 end
529
530 private fun write_accessor: String do
531 var ret = "fun "
532 var name = "{mreadpropdef.mproperty.name}="
533 if n_readable != null and n_readable.n_visibility != null then
534 if n_readable.n_visibility isa APublicVisibility then ret = "{ret}{name.green}"
535 if n_readable.n_visibility isa APrivateVisibility then ret = "{ret}{name.red}"
536 if n_readable.n_visibility isa AProtectedVisibility then ret = "{ret}{name.yellow}"
537 else
538 ret = "{ret}{name.red}"
539 end
540 ret = "{ret}({mreadpropdef.mproperty.name}: {n_type.to_s})"
541 if n_kwredef != null then ret = "redef {ret}"
542 return ret
543 end
544 end
545
546 # Redef String class to add a function to color the string
547 redef class String
548
549 private fun add_escape_char(escapechar: String): String do
550 return "{escapechar}{self}\\033[0m"
551 end
552
553 private fun esc: Char do return 27.ascii
554 private fun gray: String do return add_escape_char("{esc}[30m")
555 private fun red: String do return add_escape_char("{esc}[31m")
556 private fun green: String do return add_escape_char("{esc}[32m")
557 private fun yellow: String do return add_escape_char("{esc}[33m")
558 private fun blue: String do return add_escape_char("{esc}[34m")
559 private fun purple: String do return add_escape_char("{esc}[35m")
560 private fun cyan: String do return add_escape_char("{esc}[36m")
561 private fun light_gray: String do return add_escape_char("{esc}[37m")
562 private fun bold: String do return add_escape_char("{esc}[1m")
563 private fun underline: String do return add_escape_char("{esc}[4m")
564
565 private fun escape: String
566 do
567 var b = new Buffer
568 for c in self do
569 if c == '\n' then
570 b.append("\\n")
571 else if c == '\0' then
572 b.append("\\0")
573 else if c == '"' then
574 b.append("\\\"")
575 else if c == '\\' then
576 b.append("\\\\")
577 else if c == '`' then
578 b.append("'")
579 else if c.ascii < 32 then
580 b.append("\\{c.ascii.to_base(8, false)}")
581 else
582 b.add(c)
583 end
584 end
585 return b.to_s
586 end
587 end
588
589 # Create a tool context to handle options and paths
590 var toolcontext = new ToolContext
591 toolcontext.process_options
592
593 # Here we launch the nit index
594 var ni = new NitIndex(toolcontext)
595 ni.start
596
597 # TODO seek subclasses and super classes <.<class> >.<class>
598 # TODO seek subclasses and super types <:<type> >:<type>
599 # TODO seek with regexp
600 # TODO standardize namespaces with private option