ni: better display of welcome screen
[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 var nmodule = mbuilder.mmodule2nmodule[mmodule]
149 pager.add("# module {mmodule.namespace}\n".bold)
150 if not mmodule.in_importation.direct_greaters.is_empty then
151 pager.add("import ".bold + "{mmodule.in_importation.direct_greaters.join(", ")}\n")
152 end
153 if not mmodule.in_importation.direct_smallers.is_empty then
154 pager.add("known clients: ".bold + "{mmodule.in_importation.direct_smallers.join(", ")}\n")
155 end
156 pager.add_rule
157 pager.addn(nmodule.n_moduledecl.n_doc.comment.green)
158 pager.add_rule
159
160 var cats = new HashMap[String, Collection[MClass]]
161 cats["introduced classes"] = mmodule.intro_mclasses
162 cats["refined classes"] = mmodule.redef_mclasses
163 cats["imported classes"] = mmodule.imported_mclasses
164
165 for cat, list in cats do
166 if not list.is_empty then
167 pager.add("\n# {cat}".bold)
168 #sort list
169 var sorted = new Array[MClass]
170 sorted.add_all(list)
171 var sorter = new MClassNameSorter
172 sorter.sort(sorted)
173 for mclass in sorted do
174 var nclass = mbuilder.mclassdef2nclassdef[mclass.intro].as(AStdClassdef)
175 pager.add("")
176 if not nclass.n_doc == null and not nclass.n_doc.short_comment.is_empty then
177 pager.add("\t# {nclass.n_doc.short_comment}")
178 end
179 if cat == "refined classes" then
180 pager.add("\tredef {mclass.short_doc}")
181 else
182 pager.add("\t{mclass.short_doc}")
183 end
184 if cat != "introduced classes" then
185 pager.add("\t\t" + "introduced in {mmodule.full_name}::{mclass}".gray)
186 end
187 for mclassdef in mclass.mclassdefs do
188 if mclassdef != mclass.intro then
189 pager.add("\t\t" + "refined in {mclassdef.namespace}".gray)
190 end
191 end
192 end
193 end
194 end
195 pager.add_rule
196 end
197 pager.render
198 end
199
200 private fun classes_fulldoc(mclasses: List[MClass]) do
201 var pager = new Pager
202 for mclass in mclasses do
203 var nclass = mbuilder.mclassdef2nclassdef[mclass.intro].as(AStdClassdef)
204
205 pager.add("# {mclass.namespace}\n".bold)
206 pager.add("{mclass.short_doc}")
207 if not nclass.n_doc == null then
208 pager.add_rule
209 pager.addn(nclass.n_doc.comment.green)
210 end
211 pager.add_rule
212 if not mclass.parameter_types.is_empty then
213 pager.add("# formal types".bold)
214 for ft, bound in mclass.parameter_types do
215 pager.add("")
216 pager.add("\t{ft.to_s.green}: {bound}")
217 end
218 end
219 if not mclass.virtual_types.is_empty then
220 pager.add("# virtual types".bold)
221 for vt in mclass.virtual_types do
222 pager.add("")
223 mpropdef_fulldoc(pager, vt.intro)
224 end
225 end
226 pager.add_rule
227
228 var cats = new HashMap[String, Collection[MMethod]]
229 cats["constructors"] = mclass.constructors
230 cats["introduced methods"] = mclass.intro_methods
231 cats["refined methods"] = mclass.redef_methods
232 cats["inherited methods"] = mclass.inherited_methods
233
234 for cat, list in cats do
235 if not list.is_empty then
236 #sort list
237 var sorted = new Array[MMethod]
238 sorted.add_all(list)
239 var sorter = new MPropertyNameSorter
240 sorter.sort(sorted)
241 pager.add("\n# {cat}".bold)
242 for mprop in sorted do
243 pager.add("")
244 mpropdef_fulldoc(pager, mprop.intro)
245 end
246 end
247 end
248 pager.add_rule
249 end
250 pager.render
251 end
252
253 private fun props_fulldoc(raw_mprops: List[MProperty]) do
254 var pager = new Pager
255 # group by module
256 var cats = new HashMap[MClass, Array[MProperty]]
257 for mprop in raw_mprops do
258 if not mbuilder.mpropdef2npropdef.has_key(mprop.intro) then continue
259 if mprop isa MAttribute then continue
260 var mclass = mprop.intro_mclassdef.mclass
261 if not cats.has_key(mclass) then cats[mclass] = new Array[MProperty]
262 cats[mclass].add(mprop)
263 end
264 #sort groups
265 var sorter = new MClassNameSorter
266 var sorted = new Array[MClass]
267 sorted.add_all(cats.keys)
268 sorter.sort(sorted)
269 # display
270 for mclass in sorted do
271 var mprops = cats[mclass]
272 pager.add("# {mclass.namespace}".bold)
273 var sorterp = new MPropertyNameSorter
274 sorterp.sort(mprops)
275 for mprop in mprops do
276 pager.add("")
277 mpropdef_fulldoc(pager, mprop.intro)
278 end
279 pager.add_rule
280 end
281 pager.render
282 end
283
284 private fun seek_returns(entry: String): List[MProperty] do
285 # TODO how to match with generic types?
286 var matches = new List[MProperty]
287 for mprop in model.mproperties do
288 var intro = mprop.intro
289 if intro isa MMethodDef then
290 if intro.msignature.return_mtype != null and intro.msignature.return_mtype.to_s == entry then matches.add(mprop)
291 else if intro isa MAttributeDef then
292 if intro.static_mtype.to_s == entry then matches.add(mprop)
293 end
294 end
295 return matches
296 end
297
298 private fun seek_params(entry: String): List[MProperty] do
299 # TODO how to match with generic types?
300 var matches = new List[MProperty]
301 for mprop in model.mproperties do
302 var intro = mprop.intro
303 if intro isa MMethodDef then
304 var mparameters = intro.msignature.mparameters
305 for mparameter in mparameters do
306 if mparameter.mtype.to_s == entry then matches.add(mprop)
307 end
308 else if intro isa MAttributeDef then
309 if intro.static_mtype.to_s == entry then matches.add(mprop)
310 end
311 end
312 return matches
313 end
314
315 private fun mpropdef_fulldoc(pager: Pager, mpropdef: MPropDef) do
316 if mbuilder.mpropdef2npropdef.has_key(mpropdef) then
317 var nprop = mbuilder.mpropdef2npropdef[mpropdef]
318 if not nprop.n_doc == null and not nprop.n_doc.short_comment.is_empty then
319 pager.add("\t# {nprop.n_doc.short_comment}")
320 end
321 end
322 pager.add("\t{mpropdef}")
323 pager.add("\t\t" + "introduced in {mpropdef.mproperty.intro_mclassdef.namespace}".gray)
324 for mpdef in mpropdef.mproperty.mpropdefs do
325 if not mpdef.is_intro then
326 pager.add("\t\t" + "refined in {mpdef.mclassdef.namespace}".gray)
327 end
328 end
329 end
330 end
331
332 # Printing facilities
333
334 redef class MModule
335 private fun namespace: String do
336 return full_name
337 end
338 end
339
340 redef class MClass
341 redef fun to_s: String do
342 if arity > 0 then
343 return "{name}[{intro.parameter_names.join(", ")}]"
344 else
345 return name
346 end
347 end
348
349 private fun short_doc: String do
350 var ret = ""
351 if is_interface then ret = "interface {ret}"
352 if is_enum then ret = "enum {ret}"
353 if is_class then ret = "class {ret}"
354 if is_abstract then ret = "abstract {ret}"
355 if visibility.to_s == "public" then ret = "{ret}{to_s.green}"
356 if visibility.to_s == "private" then ret = "{ret}{to_s.red}"
357 if visibility.to_s == "protected" then ret = "{ret}{to_s.yellow}"
358 if not parents.is_empty then
359 ret = "{ret} super {parents.join(", ")}"
360 end
361 return ret
362 end
363
364 private fun namespace: String do
365 if not intro_mmodule.public_owner == null then
366 return "{intro_mmodule.public_owner.name}::{name}"
367 else
368 return "{intro_mmodule.name}::{name}"
369 end
370 end
371 end
372
373 redef class MClassDef
374 private fun namespace: String do
375 return "{mmodule.full_name}::{mclass.name}"
376 end
377 end
378
379 redef class MMethodDef
380 redef fun to_s do
381 var res = new Buffer
382 if not is_intro then res.append("redef ")
383 if not mproperty.is_init then res.append("fun ")
384 if mproperty.visibility.to_s == "public" then res.append(mproperty.name.green)
385 if mproperty.visibility.to_s == "private" then res.append(mproperty.name.red)
386 if mproperty.visibility.to_s == "protected" then res.append(mproperty.name.yellow)
387 if msignature != null then res.append(msignature.to_s)
388 # FIXME: modifiers should be accessible via the model
389 #if self isa ADeferredMethPropdef then ret = "{ret} is abstract"
390 #if self isa AInternMethPropdef then ret = "{ret} is intern"
391 #if self isa AExternMethPropdef then ret = "{ret} is extern"
392 return res.to_s
393 end
394 end
395
396 redef class MVirtualTypeDef
397 redef fun to_s do
398 var res = new Buffer
399 if mproperty.visibility.to_s == "public" then res.append(mproperty.name.green)
400 if mproperty.visibility.to_s == "private" then res.append(mproperty.name.red)
401 if mproperty.visibility.to_s == "protected" then res.append(mproperty.name.yellow)
402 res.append(": {bound.to_s}")
403 return res.to_s
404 end
405 end
406
407 redef class MSignature
408 redef fun to_s do
409 var res = new Buffer
410 if not mparameters.is_empty then
411 res.append("(")
412 for i in [0..mparameters.length[ do
413 res.append(mparameters[i].to_s)
414 if i < mparameters.length - 1 then res.append(", ")
415 end
416 res.append(")")
417 end
418 if return_mtype != null then
419 res.append(": {return_mtype.to_s}")
420 end
421 return res.to_s
422 end
423 end
424
425 redef class MParameter
426 redef fun to_s do
427 var res = new Buffer
428 res.append("{name}: {mtype}")
429 if is_vararg then res.append("...")
430 return res.to_s
431 end
432 end
433
434 redef class MNullableType
435 redef fun to_s do return "nullable {mtype}"
436 end
437
438 redef class MGenericType
439 redef fun to_s do
440 var res = new Buffer
441 res.append("{mclass.name}[")
442 for i in [0..arguments.length[ do
443 res.append(arguments[i].to_s)
444 if i < arguments.length - 1 then res.append(", ")
445 end
446 res.append("]")
447 return res.to_s
448 end
449 end
450
451 redef class MParameterType
452 redef fun to_s do return mclass.intro.parameter_names[rank]
453 end
454
455 redef class MVirtualType
456 redef fun to_s do return mproperty.intro.to_s
457 end
458
459 redef class ADoc
460 private fun comment: String do
461 var res = new Buffer
462 for t in n_comment do
463 res.append(t.text.replace("# ", "").replace("#", ""))
464 end
465 return res.to_s
466 end
467
468 private fun short_comment: String do
469 return n_comment.first.text.replace("# ", "").replace("\n", "")
470 end
471 end
472
473 redef class AAttrPropdef
474 private fun read_accessor: String do
475 var ret = "fun "
476 #FIXME bug with standard::stream::FDStream::fd
477 var name = mreadpropdef.mproperty.name
478 if mpropdef.mproperty.visibility.to_s == "public" then ret = "{ret}{name.green}"
479 if mpropdef.mproperty.visibility.to_s == "private" then ret = "{ret}{name.red}"
480 if mpropdef.mproperty.visibility.to_s == "protected" then ret = "{ret}{name.yellow}"
481 ret = "{ret}: {n_type.to_s}"
482 if n_kwredef != null then ret = "redef {ret}"
483 return ret
484 end
485
486 private fun write_accessor: String do
487 var ret = "fun "
488 var name = "{mreadpropdef.mproperty.name}="
489 if n_readable != null and n_readable.n_visibility != null then
490 if n_readable.n_visibility isa APublicVisibility then ret = "{ret}{name.green}"
491 if n_readable.n_visibility isa APrivateVisibility then ret = "{ret}{name.red}"
492 if n_readable.n_visibility isa AProtectedVisibility then ret = "{ret}{name.yellow}"
493 else
494 ret = "{ret}{name.red}"
495 end
496 ret = "{ret}({mreadpropdef.mproperty.name}: {n_type.to_s})"
497 if n_kwredef != null then ret = "redef {ret}"
498 return ret
499 end
500 end
501
502 # Redef String class to add a function to color the string
503 redef class String
504
505 private fun add_escape_char(escapechar: String): String do
506 return "{escapechar}{self}\\033[0m"
507 end
508
509 private fun esc: Char do return 27.ascii
510 private fun red: String do return add_escape_char("{esc}[1;31m")
511 private fun yellow: String do return add_escape_char("{esc}[1;33m")
512 private fun green: String do return add_escape_char("{esc}[1;32m")
513 private fun blue: String do return add_escape_char("{esc}[1;34m")
514 private fun cyan: String do return add_escape_char("{esc}[1;36m")
515 private fun gray: String do return add_escape_char("{esc}[30;1m")
516 private fun bold: String do return add_escape_char("{esc}[1m")
517 private fun underline: String do return add_escape_char("{esc}[4m")
518
519 private fun escape: String
520 do
521 var b = new Buffer
522 for c in self do
523 if c == '\n' then
524 b.append("\\n")
525 else if c == '\0' then
526 b.append("\\0")
527 else if c == '"' then
528 b.append("\\\"")
529 else if c == '\\' then
530 b.append("\\\\")
531 else if c == '`' then
532 b.append("'")
533 else if c.ascii < 32 then
534 b.append("\\{c.ascii.to_base(8, false)}")
535 else
536 b.add(c)
537 end
538 end
539 return b.to_s
540 end
541 end
542
543 # Create a tool context to handle options and paths
544 var toolcontext = new ToolContext
545 toolcontext.process_options
546
547 # Here we launch the nit index
548 var ni = new NitIndex(toolcontext)
549 ni.start
550
551 # TODO seek subclasses and super classes <.<class> >.<class>
552 # TODO seek subclasses and super types <:<type> >:<type>
553 # TODO seek with regexp
554 # TODO standardize namespaces with private option