e8cb2811feccb0c1d6576ac52f84397b16f151af
[nit.git] / src / doc / doc_phases / doc_console.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 # Nitx related components
16 #
17 # This module is a place holder for `nitx` related services.
18 # No `doc_phase` can be found here, only components used by Nitx tool.
19 module doc_console
20
21 import semantize
22 import doc_commands
23 import doc_poset
24 import doc::console_templates
25 import model::model_index
26
27 # Nitx handles console I/O.
28 #
29 # Using `prompt`, the command line can be turned on an interactive tool.
30 class Nitx
31
32 # ToolContext used to access options.
33 var ctx: ToolContext
34
35 # DocModel that contains the informations to display.
36 var doc: DocModel
37
38 # Comparator used to sort MEntities.
39 var sorter = new MEntityNameSorter
40
41 # Displays the welcome message and start prompt.
42 fun start do
43 welcome
44 prompt
45 end
46
47 # Displays the welcome message and the list of loaded modules.
48 fun welcome do
49 print "Welcome in the Nit Index."
50 print ""
51 print "Loaded modules:"
52 var mmodules = doc.mmodules.to_a
53 sorter.sort(mmodules)
54 for m in mmodules do
55 print "\t{m.name}"
56 end
57 print ""
58 help
59 end
60
61 # Displays the list of available commands.
62 fun help do
63 print "\nCommands:\n"
64 print "\tname\t\t\tlookup module, class and property with the corresponding 'name'"
65 print "\tdoc: <name::space>\tdisplay the documentation page of 'namespace'"
66 print "\nType lookup:"
67 print "\tparam: <Type>\t\tlookup methods using the corresponding 'Type' as parameter"
68 print "\treturn: <Type>\t\tlookup methods returning the corresponding 'Type'"
69 print "\tnew: <Type>\t\tlookup methods creating new instances of 'Type'"
70 print "\tcall: <name>\t\tlookup methods calling 'name'"
71 print "\nHierarchy lookup:"
72 print "\tparents: <Class>\tlist direct parents of 'Class'"
73 print "\tancestors: <Class>\tlist all ancestors of 'Class'"
74 print "\tchildren: <Class>\tlist direct children of 'Class'"
75 print "\tdescendants: <Class>\tlist all descendants of 'Class'"
76 print "\nCode lookup:"
77 print "\tcode: <name>\t\tdisplay the source code associated to the 'name' entity"
78 print "\n"
79 print "\t:h\t\t\tdisplay this help message"
80 print "\t:q\t\t\tquit interactive mode"
81 print ""
82 end
83
84 # Prompts the user for a command.
85 fun prompt do
86 printn ">> "
87 do_query(sys.stdin.read_line)
88 prompt
89 end
90
91 # Processes the query string and performs it.
92 fun do_query(str: String) do
93 var query = new DocCommand(str)
94 if query isa NitxCommand then
95 query.execute(self)
96 return
97 end
98 var res = query.perform(self, doc)
99 var suggest = null
100 if res.is_empty then
101 suggest = query.suggest(self, doc)
102 end
103 var page = query.make_results(self, res, suggest)
104 print page.write_to_string
105 end
106 end
107
108 redef interface DocCommand
109
110 redef new(query_string) do
111 if query_string == ":q" then
112 return new NitxQuit
113 else if query_string == ":h" then
114 return new NitxHelp
115 end
116 var cmd = super(query_string)
117 if cmd isa UnknownCommand then
118 return new CommentCommand("comment: {query_string}")
119 end
120 return cmd
121 end
122
123 # Looks up the `doc` model and returns possible matches.
124 fun perform(nitx: Nitx, doc: DocModel): Array[NitxMatch] is abstract
125
126 # Looks up the `doc` model and returns possible suggestions.
127 fun suggest(nitx: Nitx, doc: DocModel): nullable Array[MEntity] do
128 return find_suggestions(doc, args.first)
129 end
130
131 # Pretty prints the results for the console.
132 fun make_results(nitx: Nitx, results: Array[NitxMatch], suggest: nullable Array[MEntity]): DocPage do
133 var page = new DocPage("results", "Results")
134 page.root.add_child(new QueryResultArticle("results", "Results", self, results, suggest))
135 return page
136 end
137
138 # Lookup mentities based on a `query` string.
139 #
140 # 1- lookup by first name (returns always one value)
141 # 2- lookup by name (can return conflicts)
142 fun find_mentities(doc: DocModel, query: String): Array[MEntityMatch] do
143 var res = new Array[MEntityMatch]
144
145 # First lookup by full_name
146 var mentity = doc.mentity_by_full_name(query)
147 if mentity != null then
148 res.add new MEntityMatch(self, mentity)
149 return res
150 end
151
152 # If no results, lookup by name
153 for m in doc.mentities_by_name(query) do
154 res.add new MEntityMatch(self, m)
155 end
156
157 return res
158 end
159
160 # Suggest some mentities based on a `query` string.
161 fun find_suggestions(doc: DocModel, query: String): Array[MEntity] do
162 return doc.find(query, 3)
163 end
164 end
165
166 # Something that matches a `DocCommand`.
167 abstract class NitxMatch
168
169 # Query matched by `self`.
170 var query: DocCommand
171
172 # Pretty prints `self` for console.
173 fun make_list_item: String is abstract
174 end
175
176 # A match between a `DocCommand` and a `MEntity`.
177 class MEntityMatch
178 super NitxMatch
179
180 # MEntity matched.
181 var mentity: MEntity
182
183 redef fun make_list_item do return mentity.cs_list_item
184 end
185
186 redef class CommentCommand
187 redef fun perform(nitx, doc) do return find_mentities(doc, args.first)
188
189 redef fun make_results(nitx, results, suggest) do
190 var len = results.length
191 if len == 1 then
192 var res = results.first.as(MEntityMatch)
193 var mentity = res.mentity
194 var page = new DocPage("resultats", "Results")
195 var article = new DefinitionArticle("results", "Results", mentity)
196 article.cs_title = mentity.name
197 article.cs_subtitle = mentity.cs_declaration
198 page.root.add_child article
199 return page
200 else
201 return super
202 end
203 end
204 end
205
206 # A query to search signatures using a specific `MType` as parameter.
207 redef class ParamCommand
208 redef fun perform(nitx, doc) do
209 var res = new Array[NitxMatch]
210 var mtype_name = args.first
211 for mproperty in doc.mproperties do
212 if not mproperty isa MMethod then continue
213 var msignature = mproperty.intro.msignature
214 if msignature != null then
215 for mparam in msignature.mparameters do
216 if mparam.mtype.name == mtype_name then
217 res.add new MEntityMatch(self, mproperty)
218 end
219 end
220 end
221 end
222 return res
223 end
224 end
225
226 # A query to search signatures using a specific `MType` as return.
227 redef class ReturnCommand
228 redef fun perform(nitx, doc) do
229 var res = new Array[NitxMatch]
230 var mtype_name = args.first
231 for mproperty in doc.mproperties do
232 if not mproperty isa MMethod then continue
233 var msignature = mproperty.intro.msignature
234 if msignature != null then
235 var mreturn = msignature.return_mtype
236 if mreturn != null and mreturn.name == mtype_name then
237 res.add new MEntityMatch(self, mproperty)
238 end
239 end
240 end
241 return res
242 end
243 end
244
245 # A query to search methods creating new instances of a specific `MType`.
246 redef class NewCommand
247 redef fun perform(nitx, doc) do
248 var res = new Array[NitxMatch]
249 var mtype_name = args.first
250 for mpropdef in doc.mpropdefs do
251 var visitor = new TypeInitVisitor(mtype_name)
252 var npropdef = nitx.ctx.modelbuilder.mpropdef2node(mpropdef)
253 if npropdef == null then continue
254 visitor.enter_visit(npropdef)
255 for i in visitor.inits do
256 res.add new MEntityMatch(self, mpropdef)
257 end
258 end
259 return res
260 end
261 end
262
263 # A query to search methods calling a specific `MProperty`.
264 redef class CallCommand
265 redef fun perform(nitx, doc) do
266 var res = new Array[NitxMatch]
267 var mprop_name = args.first
268 for mpropdef in doc.mpropdefs do
269 var visitor = new MPropertyCallVisitor
270 var npropdef = nitx.ctx.modelbuilder.mpropdef2node(mpropdef)
271 if npropdef == null then continue
272 visitor.enter_visit(npropdef)
273 for mprop in visitor.calls do
274 if mprop.name != mprop_name then continue
275 res.add new MEntityMatch(self, mpropdef)
276 end
277 end
278 return res
279 end
280 end
281
282 # A query to search a Nitdoc documentation page by its name.
283 redef class ArticleCommand
284 redef fun perform(nitx, doc) do
285 var res = new Array[NitxMatch]
286 var name = args.first
287 for page in doc.pages.values do
288 if name == "*" then # FIXME dev only
289 res.add new PageMatch(self, page)
290 else if page.title == name then
291 res.add new PageMatch(self, page)
292 else if page isa MEntityPage and page.mentity.cs_namespace == name then
293 res.add new PageMatch(self, page)
294 end
295 end
296 return res
297 end
298
299 redef fun make_results(nitx, results, suggest) do
300 var len = results.length
301 # FIXME how to render the pager for one worded namespaces like "core"?
302 if len == 1 then
303 var page = results.first.as(PageMatch).page
304 var pager = new Pager
305 pager.add page.write_to_string
306 pager.render
307 return page
308 else
309 return super
310 end
311 end
312 end
313
314 # A match between a `DocPage` and a `MEntity`.
315 class PageMatch
316 super NitxMatch
317
318 # `DocPage` matched.
319 var page: DocPage
320
321 redef fun make_list_item do
322 var page = self.page
323 if page isa MEntityPage then
324 return page.mentity.cs_list_item
325 end
326 return " * {page.title}"
327 end
328 end
329
330 # Search in class or module hierarchy of a `MEntity`.
331 #
332 # It actually searches for pages about the mentity and extracts the
333 # pre-calculated hierarchies by the `doc_post` phase.
334 abstract class HierarchiesQuery
335 super DocCommand
336
337 redef fun make_results(nitx, results, suggest) do
338 var page = new DocPage("hierarchy", "Hierarchy")
339 for result in results do
340 if not result isa PageMatch then continue
341 var rpage = result.page
342 if not rpage isa MClassPage then continue
343 page.root.add_child build_article(rpage)
344 end
345 return page
346 end
347
348 # Build an article containing the hierarchy list depending on subclasses.
349 private fun build_article(page: MClassPage): DocArticle is abstract
350 end
351
352 # List all parents of a `MClass`.
353 class AncestorsQuery
354 super HierarchiesQuery
355
356 redef fun build_article(page) do
357 return new MEntitiesListArticle(
358 "ancerstors",
359 "Ancestors for {page.mentity.name}",
360 page.ancestors.to_a)
361 end
362 end
363
364 # List direct parents of a `MClass`.
365 class ParentsQuery
366 super HierarchiesQuery
367
368 redef fun build_article(page) do
369 return new MEntitiesListArticle(
370 "parents",
371 "Parents for {page.mentity.name}",
372 page.parents.to_a)
373 end
374 end
375
376 # List direct children of a `MClass`.
377 class ChildrenQuery
378 super HierarchiesQuery
379
380 redef fun build_article(page) do
381 return new MEntitiesListArticle(
382 "children",
383 "Children for {page.mentity.name}",
384 page.children.to_a)
385 end
386 end
387
388 # List all descendants of a `MClass`.
389 class DescendantsQuery
390 super HierarchiesQuery
391
392 redef fun build_article(page) do
393 return new MEntitiesListArticle(
394 "descendants",
395 "Descendants for {page.mentity.name}",
396 page.children.to_a)
397 end
398 end
399
400 # A query to search source code from a file name.
401 redef class CodeCommand
402 # FIXME refactor this!
403 redef fun perform(nitx, doc) do
404 var res = new Array[NitxMatch]
405 var name = args.first
406 # if name is an existing sourcefile, opens it
407 if name.file_exists then
408 var fr = new FileReader.open(name)
409 var content = fr.read_all
410 fr.close
411 res.add new CodeMatch(self, name, content)
412 return res
413 end
414 # else, lookup the model by name
415 for match in find_mentities(doc, name) do
416 if match.mentity isa MClass then continue
417 if match.mentity isa MProperty then continue
418 res.add new CodeMatch(self, match.mentity.cs_location, match.mentity.cs_source_code)
419 end
420 return res
421 end
422
423 redef fun make_results(nitx, results, suggest) do
424 var page = new DocPage("results", "Code Results")
425 for res in results do
426 page.add new CodeQueryArticle("results", "Results", self, res.as(CodeMatch))
427 end
428 return page
429 end
430 end
431
432 # A match between a piece of code and a string.
433 class CodeMatch
434 super NitxMatch
435
436 # Location of the code match.
437 var location: String
438
439 # Piece of code matched.
440 var content: String
441
442 redef fun make_list_item do return "* {location}"
443 end
444
445
446 # A query that contains a nitx command.
447 #
448 # These commands are prefixed with `:` and are used to control the execution of
449 # `nitx` like displaying the help or quiting.
450 interface NitxCommand
451 super DocCommand
452
453 # Executes the command.
454 fun execute(nitx: Nitx) is abstract
455 end
456
457 # Exits nitx.
458 class NitxQuit
459 super NitxCommand
460
461 redef fun execute(nitx) do exit 0
462 end
463
464 # Displays the help message.
465 class NitxHelp
466 super NitxCommand
467
468 redef fun execute(nitx) do nitx.help
469 end
470
471 ## exploration
472
473 # Visitor looking for initialized `MType` (new T).
474 #
475 # See `NewQuery`.
476 private class TypeInitVisitor
477 super Visitor
478
479 # `MType` name to look for.
480 var mtype_name: String
481
482 var inits = new HashSet[MType]
483 redef fun visit(node)
484 do
485 node.visit_all(self)
486 # look for init
487 if not node isa ANewExpr then return
488 var mtype = node.n_type.mtype
489 if mtype != null and mtype.name == mtype_name then inits.add(mtype)
490 end
491 end
492
493 # Visitor looking for calls to a `MProperty` (new T).
494 #
495 # See `CallQuery`.
496 private class MPropertyCallVisitor
497 super Visitor
498
499 var calls = new HashSet[MProperty]
500 redef fun visit(node)
501 do
502 node.visit_all(self)
503 if not node isa ASendExpr then return
504 calls.add node.callsite.as(not null).mproperty
505 end
506 end
507
508 # display
509
510 # A `DocArticle` that displays query results.
511 private class QueryResultArticle
512 super DocArticle
513
514 # Query linked to the results to display.
515 var query: DocCommand
516
517 # Results to display.
518 var results: Array[NitxMatch]
519
520 # Optional suggestion when no matches where found
521 var suggest: nullable Array[MEntity] = null is optional
522
523 redef fun render_title do
524 var len = results.length
525 if len == 0 then
526 addn "No result found for '{query.string}'..."
527 var suggest = self.suggest
528 if suggest != null and suggest.not_empty then
529 add "\nDid you mean "
530 var i = 0
531 for s in suggest do
532 add "`{s.full_name}`"
533 if i == suggest.length - 2 then add ", "
534 if i == suggest.length - 1 then add " or "
535 i += 1
536 end
537 add "?"
538 end
539 else
540 add "# {len} result(s) for '{query.string}'".green.bold
541 end
542 end
543
544 redef fun render_body do
545 addn ""
546 for result in results do
547 addn ""
548 addn result.make_list_item
549 end
550 end
551 end
552
553 # An article that displays a piece of code.
554 private class CodeQueryArticle
555 super DocArticle
556
557 # The query linked to the result to display.
558 var query: DocCommand
559
560 # The result to display.
561 var result: CodeMatch
562
563 redef fun render_body do
564 addn ""
565 addn "in {result.location}".gray.bold
566 addn ""
567 add result.content
568 end
569 end
570
571 # A Pager is used to display data into a unix `less` container.
572 private class Pager
573
574 # Content to display.
575 var content = new FlatBuffer
576
577 # Adds text to the pager.
578 fun add(text: String) do
579 content.append(escape(text))
580 end
581
582 fun render do sys.system("echo \"{content}\" | less -r")
583
584 fun escape(str: String): String
585 do
586 var b = new FlatBuffer
587 for c in str.chars do
588 if c == '\n' then
589 b.append("\\n")
590 else if c == '\0' then
591 b.append("\\0")
592 else if c == '"' then
593 b.append("\\\"")
594 else if c == '\\' then
595 b.append("\\\\")
596 else if c == '`' then
597 b.append("'")
598 else if c.code_point < 32 then
599 b.append("\\{c.code_point.to_base(8)}")
600 else
601 b.add(c)
602 end
603 end
604 return b.to_s
605 end
606 end