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