ni: use command ":q" to quit, blank line do nothing
[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 ':q' to exit\n"
78 print "Loaded 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 print ""
87 end
88
89 fun prompt do
90 printn ">> "
91 seek(stdin.read_line)
92 end
93
94 fun seek(entry: String) do
95 if entry.is_empty then
96 prompt
97 return
98 end
99 if entry == ":q" then exit(0)
100 var pager = new Pager
101 # seek return types
102 if entry.has_prefix("return:") then
103 var ret = entry.split_with(":")[1].replace(" ", "")
104 var matches = seek_returns(ret)
105 props_fulldoc(pager, matches)
106 else if entry.has_prefix("param:") then
107 var param = entry.split_with(":")[1].replace(" ", "")
108 var matches = seek_params(param)
109 props_fulldoc(pager, matches)
110 else
111 # seek for modules
112 var mmatches = new List[MModule]
113 for m in model.mmodules do
114 if m.name == entry then mmatches.add(m)
115 end
116 if not mmatches.is_empty then modules_fulldoc(pager, mmatches)
117 # seek for classes
118 var cmatches = new List[MClass]
119 for c in model.mclasses do
120 if c.name == entry then cmatches.add(c)
121 end
122 if not cmatches.is_empty then classes_fulldoc(pager, cmatches)
123 # seek for properties
124 var matches = new List[MProperty]
125 for p in model.mproperties do
126 if p.name == entry then matches.add(p)
127 end
128 if not matches.is_empty then props_fulldoc(pager, matches)
129 end
130 # no matches
131 if pager.content.is_empty then
132 print "Nothing known about '{entry}'"
133 else
134 pager.render
135 end
136 if arguments.length == 1 then prompt
137 end
138
139 private fun modules_fulldoc(pager: Pager, mmodules: List[MModule]) do
140 for mmodule in mmodules do
141 # name and prototype
142 pager.add("# {mmodule.namespace}\n".bold)
143 # comment
144 if mbuilder.mmodule2nmodule.has_key(mmodule) then
145 var nmodule = mbuilder.mmodule2nmodule[mmodule]
146 if not nmodule.n_moduledecl.n_doc == null then
147 for comment in nmodule.n_moduledecl.n_doc.comment do pager.add(comment.green)
148 end
149 end
150 pager.add("{mmodule.prototype}\n")
151 # imports
152 var msorter = new MModuleNameSorter
153 var ms = mmodule.in_importation.greaters.to_a
154 if ms.length > 1 then
155 msorter.sort(ms)
156 pager.add("## imported modules".bold)
157 pager.addn("\t")
158 for i in [0..ms.length[ do
159 if ms[i] == mmodule then continue
160 pager.addn(ms[i].name)
161 if i < ms.length - 1 then pager.addn(", ")
162 end
163 pager.add("\n")
164 end
165 # clients
166 ms = mmodule.in_importation.smallers.to_a
167 if ms.length > 1 then
168 msorter.sort(ms)
169 pager.add("## known modules".bold)
170 pager.addn("\t")
171 for i in [0..ms.length[ do
172 if ms[i] == mmodule then continue
173 pager.addn(ms[i].name)
174 if i < ms.length - 1 then pager.addn(", ")
175 end
176 pager.add("\n")
177 end
178 # local classes and interfaces
179 var sorter = new MClassDefNameSorter
180 var intro_mclassdefs = new Array[MClassDef]
181 var redef_mclassdefs = new Array[MClassDef]
182 for mclassdef in mmodule.mclassdefs do
183 if mclassdef.is_intro then
184 intro_mclassdefs.add(mclassdef)
185 else
186 redef_mclassdefs.add(mclassdef)
187 end
188 end
189 # intro
190 if not intro_mclassdefs.is_empty then
191 sorter.sort(intro_mclassdefs)
192 pager.add("\n## introduced classes".bold)
193 for mclassdef in intro_mclassdefs do
194 pager.add("")
195 var nclass = mbuilder.mclassdef2nclassdef[mclassdef]
196 if nclass isa AStdClassdef and not nclass.n_doc == null and not nclass.n_doc.short_comment.is_empty then
197 pager.add("\t{nclass.n_doc.short_comment.green}")
198 end
199 pager.add("\t{mclassdef.mclass.prototype}")
200 #TODO add redefs?
201 end
202 end
203 # redefs
204 if not redef_mclassdefs.is_empty then
205 sorter.sort(redef_mclassdefs)
206 pager.add("\n## refined classes".bold)
207 for mclassdef in redef_mclassdefs do
208 pager.add("")
209 #TODO intro comment?
210 var nclass = mbuilder.mclassdef2nclassdef[mclassdef]
211 if nclass isa AStdClassdef and not nclass.n_doc == null and not nclass.n_doc.short_comment.is_empty then
212 pager.add("\t# {nclass.n_doc.short_comment.green}")
213 end
214 pager.add("\t{mclassdef.mclass.prototype}")
215 pager.add("\t\t" + "introduced in {mclassdef.mclass.intro.mmodule.namespace.bold}".gray)
216 for odef in mclassdef.mclass.mclassdefs do
217 if odef.is_intro or odef == mclassdef or mclassdef.mmodule == mmodule then continue
218 pager.add("\t\t" + "refined in {mclassdef.mmodule.namespace.bold}".gray)
219 end
220 end
221 end
222 #TODO add inherited classes?
223 pager.add_rule
224 end
225 end
226
227 private fun classes_fulldoc(pager: Pager, mclasses: List[MClass]) do
228 for mclass in mclasses do
229 # title
230 pager.add("# {mclass.namespace}\n".bold)
231 # comment
232 if mbuilder.mclassdef2nclassdef.has_key(mclass.intro) then
233 var nclass = mbuilder.mclassdef2nclassdef[mclass.intro]
234 if nclass isa AStdClassdef and not nclass.n_doc == null then
235 for comment in nclass.n_doc.comment do pager.add(comment.green)
236 end
237 end
238 pager.addn("{mclass.prototype}")
239 if mclass.in_hierarchy(mainmodule).direct_greaters.length > 1 then
240 var supers = mclass.in_hierarchy(mainmodule).direct_greaters.to_a
241 pager.addn(" super ")
242 for i in [0..supers.length[ do
243 if supers[i] == mclass then continue
244 pager.addn(supers[i].name)
245 if i < mclass.in_hierarchy(mainmodule).direct_greaters.length -1 then pager.addn(", ")
246 end
247 pager.add("\n")
248 end
249 # formal types
250 if not mclass.parameter_types.is_empty then
251 pager.add("## formal types".bold)
252 for ft, bound in mclass.parameter_types do
253 pager.add("")
254 pager.add("\t{ft.to_s.green}: {bound}")
255 end
256 pager.add("")
257 end
258 # get properties
259 var cats = new ArrayMap[String, Set[MPropDef]]
260 cats["virtual types"] = new HashSet[MPropDef]
261 cats["constructors"] = new HashSet[MPropDef]
262 cats["introduced methods"] = new HashSet[MPropDef]
263 cats["refined methods"] = new HashSet[MPropDef]
264
265 for mclassdef in mclass.mclassdefs do
266 for mpropdef in mclassdef.mpropdefs do
267 if mpropdef isa MAttributeDef then continue
268 if mpropdef isa MVirtualTypeDef then cats["virtual types"].add(mpropdef)
269 if mpropdef isa MMethodDef then
270 if mpropdef.mproperty.is_init then
271 cats["constructors"].add(mpropdef)
272 else if mpropdef.is_intro then
273 cats["introduced methods"].add(mpropdef)
274 else
275 cats["refined methods"].add(mpropdef)
276 end
277 end
278 end
279 end
280 # local mproperties
281 for cat, list in cats do
282 if not list.is_empty then
283 #sort list
284 var sorted = new Array[MPropDef]
285 sorted.add_all(list)
286 var sorter = new MPropDefNameSorter
287 sorter.sort(sorted)
288 pager.add("## {cat}".bold)
289 for mpropdef in sorted do
290 pager.add("")
291 if mbuilder.mpropdef2npropdef.has_key(mpropdef) then
292 var nprop = mbuilder.mpropdef2npropdef[mpropdef]
293 if not nprop.n_doc == null and not nprop.n_doc.comment.is_empty then
294 for comment in nprop.n_doc.comment do pager.add("\t{comment.green}")
295 else
296 nprop = mbuilder.mpropdef2npropdef[mpropdef.mproperty.intro]
297 if not nprop.n_doc == null and not nprop.n_doc.comment.is_empty then
298 for comment in nprop.n_doc.comment do pager.add("\t{comment.green}")
299 end
300 end
301 end
302 pager.add("\t{mpropdef}")
303 mainmodule.linearize_mpropdefs(mpropdef.mproperty.mpropdefs)
304 var previous_defs = new Array[MPropDef]
305 for def in mpropdef.mproperty.mpropdefs do
306 if def == mpropdef then continue
307 if def.is_intro then continue
308 if mclass.in_hierarchy(mainmodule) < def.mclassdef.mclass then
309 previous_defs.add(def)
310 end
311 end
312 if not mpropdef.is_intro then
313 pager.add("\t\t" + "introduced by {mpropdef.mproperty.intro.mclassdef.namespace.bold}".gray)
314 end
315 if not previous_defs.is_empty then
316 for def in previous_defs do pager.add("\t\t" + "inherited from {def.mclassdef.namespace.bold}".gray)
317 end
318 end
319 pager.add("")
320 end
321 end
322 # inherited mproperties
323 var inhs = new ArrayMap[MClass, Array[MProperty]]
324 var ancestors = mclass.in_hierarchy(mainmodule).greaters.to_a
325 mainmodule.linearize_mclasses(ancestors)
326 for a in ancestors do
327 if a == mclass then continue
328 for c in a.mclassdefs do
329 for p in c.intro_mproperties do
330 if p.intro_mclassdef == c then
331 if not inhs.has_key(a) then inhs[a] = new Array[MProperty]
332 inhs[a].add(p)
333 end
334 end
335 end
336 end
337 if not inhs.is_empty then
338 pager.add("## inherited properties".bold)
339 for a, ps in inhs do
340 pager.add("\n\tfrom {a.namespace.bold}: {ps.join(", ")}")
341 end
342 end
343 pager.add_rule
344 end
345 end
346
347 private fun props_fulldoc(pager: Pager, raw_mprops: List[MProperty]) do
348 # group by module
349 var cats = new HashMap[MModule, Array[MProperty]]
350 for mprop in raw_mprops do
351 if mprop isa MAttribute then continue
352 var key = mprop.intro.mclassdef.mmodule
353 if not cats.has_key(key) then cats[key] = new Array[MProperty]
354 cats[key].add(mprop)
355 end
356 #sort groups
357 var sorter = new MModuleNameSorter
358 var sorted = new Array[MModule]
359 sorted.add_all(cats.keys)
360 sorter.sort(sorted)
361 # display
362 for mmodule in sorted do
363 var mprops = cats[mmodule]
364 pager.add("# matches in module {mmodule.namespace.bold}")
365 var sorterp = new MPropertyNameSorter
366 sorterp.sort(mprops)
367 for mprop in mprops do
368 pager.add("")
369 if mbuilder.mpropdef2npropdef.has_key(mprop.intro) then
370 var nprop = mbuilder.mpropdef2npropdef[mprop.intro]
371 if not nprop.n_doc == null and not nprop.n_doc.comment.is_empty then
372 for comment in nprop.n_doc.comment do pager.add("\t{comment.green}")
373 end
374 end
375 pager.add("\t{mprop.intro}")
376 pager.add("\t\t" + "introduced in {mprop.intro_mclassdef.namespace.bold}".gray)
377 var mpropdefs = mprop.mpropdefs
378 mainmodule.linearize_mpropdefs(mpropdefs)
379 for mpdef in mpropdefs do
380 if not mpdef.is_intro then
381 pager.add("\t\t" + "refined in {mpdef.mclassdef.namespace.bold}".gray)
382 end
383 end
384 end
385 pager.add_rule
386 end
387 end
388
389 private fun seek_returns(entry: String): List[MProperty] do
390 # TODO how to match with generic types?
391 var matches = new List[MProperty]
392 for mprop in model.mproperties do
393 var intro = mprop.intro
394 if intro isa MMethodDef then
395 if intro.msignature.return_mtype != null and intro.msignature.return_mtype.to_s == entry then matches.add(mprop)
396 else if intro isa MAttributeDef then
397 if intro.static_mtype.to_s == entry then matches.add(mprop)
398 end
399 end
400 return matches
401 end
402
403 private fun seek_params(entry: String): List[MProperty] do
404 # TODO how to match with generic types?
405 var matches = new List[MProperty]
406 for mprop in model.mproperties do
407 var intro = mprop.intro
408 if intro isa MMethodDef then
409 var mparameters = intro.msignature.mparameters
410 for mparameter in mparameters do
411 if mparameter.mtype.to_s == entry then matches.add(mprop)
412 end
413 else if intro isa MAttributeDef then
414 if intro.static_mtype.to_s == entry then matches.add(mprop)
415 end
416 end
417 return matches
418 end
419 end
420
421 # Printing facilities
422
423 redef class MModule
424 # prototype of the module
425 # module ownername::name
426 private fun prototype: String do return "module {name}"
427
428 # namespace of the module
429 # ownername::name
430 private fun namespace: String do
431 if public_owner == null then
432 return self.name
433 else
434 return "{public_owner.namespace}::{self.name}"
435 end
436 end
437 end
438
439 redef class MClass
440 # return the generic signature of the class
441 # [E, F]
442 private fun signature: String do
443 if arity > 0 then
444 return "[{intro.parameter_names.join(", ")}]"
445 else
446 return ""
447 end
448 end
449
450 # return the prototype of the class
451 # class name is displayed with colors depending on visibility
452 # abstract interface Foo[E]
453 private fun prototype: String do
454 var res = new Buffer
455 res.append("{kind} ")
456 if visibility.to_s == "public" then res.append("{name}{signature}".bold.green)
457 if visibility.to_s == "private" then res.append("{name}{signature}".bold.red)
458 if visibility.to_s == "protected" then res.append("{name}{signature}".bold.yellow)
459 return res.to_s
460 end
461
462 private fun namespace: String do
463 return "{intro_mmodule.namespace}::{name}"
464 end
465 end
466
467 redef class MClassDef
468 private fun namespace: String do
469 return "{mmodule.full_name}::{mclass.name}"
470 end
471 end
472
473 redef class MProperty
474 redef fun to_s do
475 if visibility.to_s == "public" then return name.green
476 if visibility.to_s == "private" then return name.red
477 if visibility.to_s == "protected" then return name.yellow
478 return name.bold
479 end
480 end
481
482 redef class MMethodDef
483 redef fun to_s do
484 var res = new Buffer
485 if not is_intro then res.append("redef ")
486 if not mproperty.is_init then res.append("fun ")
487 res.append(mproperty.to_s.bold)
488 if msignature != null then res.append(msignature.to_s)
489 # FIXME: modifiers should be accessible via the model
490 #if self isa ADeferredMethPropdef then ret = "{ret} is abstract"
491 #if self isa AInternMethPropdef then ret = "{ret} is intern"
492 #if self isa AExternMethPropdef then ret = "{ret} is extern"
493 return res.to_s
494 end
495 end
496
497 redef class MVirtualTypeDef
498 redef fun to_s do
499 var res = new Buffer
500 res.append("type ")
501 res.append(mproperty.to_s.bold)
502 res.append(": {bound.to_s}")
503 return res.to_s
504 end
505 end
506
507 redef class MSignature
508 redef fun to_s do
509 var res = new Buffer
510 if not mparameters.is_empty then
511 res.append("(")
512 for i in [0..mparameters.length[ do
513 res.append(mparameters[i].to_s)
514 if i < mparameters.length - 1 then res.append(", ")
515 end
516 res.append(")")
517 end
518 if return_mtype != null then
519 res.append(": {return_mtype.to_s}")
520 end
521 return res.to_s
522 end
523 end
524
525 redef class MParameter
526 redef fun to_s do
527 var res = new Buffer
528 res.append("{name}: {mtype}")
529 if is_vararg then res.append("...")
530 return res.to_s
531 end
532 end
533
534 redef class MNullableType
535 redef fun to_s do return "nullable {mtype}"
536 end
537
538 redef class MGenericType
539 redef fun to_s do
540 var res = new Buffer
541 res.append("{mclass.name}[")
542 for i in [0..arguments.length[ do
543 res.append(arguments[i].to_s)
544 if i < arguments.length - 1 then res.append(", ")
545 end
546 res.append("]")
547 return res.to_s
548 end
549 end
550
551 redef class MParameterType
552 redef fun to_s do return mclass.intro.parameter_names[rank]
553 end
554
555 redef class MVirtualType
556 redef fun to_s do return mproperty.intro.to_s
557 end
558
559 redef class ADoc
560 private fun comment: List[String] do
561 var res = new List[String]
562 for t in n_comment do
563 res.add(t.text.replace("\n", ""))
564 end
565 return res
566 end
567
568 private fun short_comment: String do
569 return n_comment.first.text.replace("\n", "")
570 end
571 end
572
573 redef class AAttrPropdef
574 private fun read_accessor: String do
575 var ret = "fun "
576 #FIXME bug with standard::stream::FDStream::fd
577 var name = mreadpropdef.mproperty.name
578 if mpropdef.mproperty.visibility.to_s == "public" then ret = "{ret}{name.green}"
579 if mpropdef.mproperty.visibility.to_s == "private" then ret = "{ret}{name.red}"
580 if mpropdef.mproperty.visibility.to_s == "protected" then ret = "{ret}{name.yellow}"
581 ret = "{ret}: {n_type.to_s}"
582 if n_kwredef != null then ret = "redef {ret}"
583 return ret
584 end
585
586 private fun write_accessor: String do
587 var ret = "fun "
588 var name = "{mreadpropdef.mproperty.name}="
589 if n_readable != null and n_readable.n_visibility != null then
590 if n_readable.n_visibility isa APublicVisibility then ret = "{ret}{name.green}"
591 if n_readable.n_visibility isa APrivateVisibility then ret = "{ret}{name.red}"
592 if n_readable.n_visibility isa AProtectedVisibility then ret = "{ret}{name.yellow}"
593 else
594 ret = "{ret}{name.red}"
595 end
596 ret = "{ret}({mreadpropdef.mproperty.name}: {n_type.to_s})"
597 if n_kwredef != null then ret = "redef {ret}"
598 return ret
599 end
600 end
601
602 # Redef String class to add a function to color the string
603 redef class String
604
605 private fun add_escape_char(escapechar: String): String do
606 return "{escapechar}{self}\\033[0m"
607 end
608
609 private fun esc: Char do return 27.ascii
610 private fun gray: String do return add_escape_char("{esc}[30m")
611 private fun red: String do return add_escape_char("{esc}[31m")
612 private fun green: String do return add_escape_char("{esc}[32m")
613 private fun yellow: String do return add_escape_char("{esc}[33m")
614 private fun blue: String do return add_escape_char("{esc}[34m")
615 private fun purple: String do return add_escape_char("{esc}[35m")
616 private fun cyan: String do return add_escape_char("{esc}[36m")
617 private fun light_gray: String do return add_escape_char("{esc}[37m")
618 private fun bold: String do return add_escape_char("{esc}[1m")
619 private fun underline: String do return add_escape_char("{esc}[4m")
620
621 private fun escape: String
622 do
623 var b = new Buffer
624 for c in self do
625 if c == '\n' then
626 b.append("\\n")
627 else if c == '\0' then
628 b.append("\\0")
629 else if c == '"' then
630 b.append("\\\"")
631 else if c == '\\' then
632 b.append("\\\\")
633 else if c == '`' then
634 b.append("'")
635 else if c.ascii < 32 then
636 b.append("\\{c.ascii.to_base(8, false)}")
637 else
638 b.add(c)
639 end
640 end
641 return b.to_s
642 end
643 end
644
645 # Create a tool context to handle options and paths
646 var toolcontext = new ToolContext
647 toolcontext.process_options
648
649 # Here we launch the nit index
650 var ni = new NitIndex(toolcontext)
651 ni.start
652
653 # TODO seek subclasses and super classes <.<class> >.<class>
654 # TODO seek subclasses and super types <:<type> >:<type>
655 # TODO seek with regexp
656 # TODO standardize namespaces with private option