nitx: update to doc commands parser
[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 # Parser used to process doc commands
92 var parser: DocCommandParser is lazy do
93 var parser = new DocCommandParser
94 parser.allowed_commands = ["doc", "comment", "list", "param", "return",
95 "new", "call", "code"]
96 return parser
97 end
98
99 # Processes the query string and performs it.
100 fun do_query(str: String) do
101 if str == ":q" then
102 exit 0
103 else if str == ":h" then
104 help
105 return
106 end
107 var query = parser.parse(str)
108 if query == null then
109 query = new CommentCommand(str)
110 query.arg = str
111 end
112 var res = query.perform(self, doc)
113 var suggest = null
114 if res.is_empty then
115 suggest = query.suggest(self, doc)
116 end
117 var page = query.make_results(self, res, suggest)
118 print page.write_to_string
119 end
120 end
121
122 redef class DocCommand
123
124 # Looks up the `doc` model and returns possible matches.
125 fun perform(nitx: Nitx, doc: DocModel): Array[NitxMatch] is abstract
126
127 # Looks up the `doc` model and returns possible suggestions.
128 fun suggest(nitx: Nitx, doc: DocModel): nullable Array[MEntity] do
129 return find_suggestions(doc, arg)
130 end
131
132 # Pretty prints the results for the console.
133 fun make_results(nitx: Nitx, results: Array[NitxMatch], suggest: nullable Array[MEntity]): DocPage do
134 var page = new DocPage("results", "Results")
135 page.root.add_child(new QueryResultArticle("results", "Results", self, results, suggest))
136 return page
137 end
138
139 # Lookup mentities based on a `query` string.
140 #
141 # 1- lookup by first name (returns always one value)
142 # 2- lookup by name (can return conflicts)
143 fun find_mentities(doc: DocModel, query: String): Array[MEntityMatch] do
144 var res = new Array[MEntityMatch]
145
146 # First lookup by full_name
147 var mentity = doc.mentity_by_full_name(query)
148 if mentity != null then
149 res.add new MEntityMatch(self, mentity)
150 return res
151 end
152
153 # If no results, lookup by name
154 for m in doc.mentities_by_name(query) do
155 res.add new MEntityMatch(self, m)
156 end
157
158 return res
159 end
160
161 # Suggest some mentities based on a `query` string.
162 fun find_suggestions(doc: DocModel, query: String): Array[MEntity] do
163 return doc.find(query, 3)
164 end
165 end
166
167 # Something that matches a `DocCommand`.
168 abstract class NitxMatch
169
170 # Query matched by `self`.
171 var query: DocCommand
172
173 # Pretty prints `self` for console.
174 fun make_list_item: String is abstract
175 end
176
177 # A match between a `DocCommand` and a `MEntity`.
178 class MEntityMatch
179 super NitxMatch
180
181 # MEntity matched.
182 var mentity: MEntity
183
184 redef fun make_list_item do return mentity.cs_list_item
185 end
186
187 redef class CommentCommand
188 redef fun perform(nitx, doc) do return find_mentities(doc, arg)
189
190 redef fun make_results(nitx, results, suggest) do
191 var len = results.length
192 if len == 1 then
193 var res = results.first.as(MEntityMatch)
194 var mentity = res.mentity
195 var page = new DocPage("resultats", "Results")
196 var article = new DefinitionArticle("results", "Results", mentity)
197 article.cs_title = mentity.name
198 article.cs_subtitle = mentity.cs_declaration
199 page.root.add_child article
200 return page
201 else
202 return super
203 end
204 end
205 end
206
207 # A query to search signatures using a specific `MType` as parameter.
208 redef class ParamCommand
209 redef fun perform(nitx, doc) do
210 var res = new Array[NitxMatch]
211 var mtype_name = arg
212 for mproperty in doc.mproperties do
213 if not mproperty isa MMethod then continue
214 var msignature = mproperty.intro.msignature
215 if msignature != null then
216 for mparam in msignature.mparameters do
217 if mparam.mtype.name == mtype_name then
218 res.add new MEntityMatch(self, mproperty)
219 end
220 end
221 end
222 end
223 return res
224 end
225 end
226
227 # A query to search signatures using a specific `MType` as return.
228 redef class ReturnCommand
229 redef fun perform(nitx, doc) do
230 var res = new Array[NitxMatch]
231 var mtype_name = arg
232 for mproperty in doc.mproperties do
233 if not mproperty isa MMethod then continue
234 var msignature = mproperty.intro.msignature
235 if msignature != null then
236 var mreturn = msignature.return_mtype
237 if mreturn != null and mreturn.name == mtype_name then
238 res.add new MEntityMatch(self, mproperty)
239 end
240 end
241 end
242 return res
243 end
244 end
245
246 # A query to search methods creating new instances of a specific `MType`.
247 redef class NewCommand
248 redef fun perform(nitx, doc) do
249 var res = new Array[NitxMatch]
250 var mtype_name = arg
251 for mpropdef in doc.mpropdefs do
252 var visitor = new TypeInitVisitor(mtype_name)
253 var npropdef = nitx.ctx.modelbuilder.mpropdef2node(mpropdef)
254 if npropdef == null then continue
255 visitor.enter_visit(npropdef)
256 for i in visitor.inits do
257 res.add new MEntityMatch(self, mpropdef)
258 end
259 end
260 return res
261 end
262 end
263
264 # A query to search methods calling a specific `MProperty`.
265 redef class CallCommand
266 redef fun perform(nitx, doc) do
267 var res = new Array[NitxMatch]
268 var mprop_name = arg
269 for mpropdef in doc.mpropdefs do
270 var visitor = new MPropertyCallVisitor
271 var npropdef = nitx.ctx.modelbuilder.mpropdef2node(mpropdef)
272 if npropdef == null then continue
273 visitor.enter_visit(npropdef)
274 for mprop in visitor.calls do
275 if mprop.name != mprop_name then continue
276 res.add new MEntityMatch(self, mpropdef)
277 end
278 end
279 return res
280 end
281 end
282
283 # A query to search a Nitdoc documentation page by its name.
284 redef class ArticleCommand
285 redef fun perform(nitx, doc) do
286 var res = new Array[NitxMatch]
287 var name = arg
288 for page in doc.pages.values do
289 if name == "*" then # FIXME dev only
290 res.add new PageMatch(self, page)
291 else if page.title == name then
292 res.add new PageMatch(self, page)
293 else if page isa MEntityPage and page.mentity.cs_namespace == name then
294 res.add new PageMatch(self, page)
295 end
296 end
297 return res
298 end
299
300 redef fun make_results(nitx, results, suggest) do
301 var len = results.length
302 # FIXME how to render the pager for one worded namespaces like "core"?
303 if len == 1 then
304 var page = results.first.as(PageMatch).page
305 var pager = new Pager
306 pager.add page.write_to_string
307 pager.render
308 return page
309 else
310 return super
311 end
312 end
313 end
314
315 # A match between a `DocPage` and a `MEntity`.
316 class PageMatch
317 super NitxMatch
318
319 # `DocPage` matched.
320 var page: DocPage
321
322 redef fun make_list_item do
323 var page = self.page
324 if page isa MEntityPage then
325 return page.mentity.cs_list_item
326 end
327 return " * {page.title}"
328 end
329 end
330
331 # Search in class or module hierarchy of a `MEntity`.
332 #
333 # It actually searches for pages about the mentity and extracts the
334 # pre-calculated hierarchies by the `doc_post` phase.
335 abstract class HierarchiesQuery
336 super DocCommand
337
338 redef fun make_results(nitx, results, suggest) do
339 var page = new DocPage("hierarchy", "Hierarchy")
340 for result in results do
341 if not result isa PageMatch then continue
342 var rpage = result.page
343 if not rpage isa MClassPage then continue
344 page.root.add_child build_article(rpage)
345 end
346 return page
347 end
348
349 # Build an article containing the hierarchy list depending on subclasses.
350 private fun build_article(page: MClassPage): DocArticle is abstract
351 end
352
353 # List all parents of a `MClass`.
354 class AncestorsQuery
355 super HierarchiesQuery
356
357 redef fun build_article(page) do
358 return new MEntitiesListArticle(
359 "ancerstors",
360 "Ancestors for {page.mentity.name}",
361 page.ancestors.to_a)
362 end
363 end
364
365 # List direct parents of a `MClass`.
366 class ParentsQuery
367 super HierarchiesQuery
368
369 redef fun build_article(page) do
370 return new MEntitiesListArticle(
371 "parents",
372 "Parents for {page.mentity.name}",
373 page.parents.to_a)
374 end
375 end
376
377 # List direct children of a `MClass`.
378 class ChildrenQuery
379 super HierarchiesQuery
380
381 redef fun build_article(page) do
382 return new MEntitiesListArticle(
383 "children",
384 "Children for {page.mentity.name}",
385 page.children.to_a)
386 end
387 end
388
389 # List all descendants of a `MClass`.
390 class DescendantsQuery
391 super HierarchiesQuery
392
393 redef fun build_article(page) do
394 return new MEntitiesListArticle(
395 "descendants",
396 "Descendants for {page.mentity.name}",
397 page.children.to_a)
398 end
399 end
400
401 # A query to search source code from a file name.
402 redef class CodeCommand
403 # FIXME refactor this!
404 redef fun perform(nitx, doc) do
405 var res = new Array[NitxMatch]
406 var name = arg
407 # if name is an existing sourcefile, opens it
408 if name.file_exists then
409 var fr = new FileReader.open(name)
410 var content = fr.read_all
411 fr.close
412 res.add new CodeMatch(self, name, content)
413 return res
414 end
415 # else, lookup the model by name
416 for match in find_mentities(doc, name) do
417 if match.mentity isa MClass then continue
418 if match.mentity isa MProperty then continue
419 res.add new CodeMatch(self, match.mentity.cs_location, match.mentity.cs_source_code)
420 end
421 return res
422 end
423
424 redef fun make_results(nitx, results, suggest) do
425 var page = new DocPage("results", "Code Results")
426 for res in results do
427 page.add new CodeQueryArticle("results", "Results", self, res.as(CodeMatch))
428 end
429 return page
430 end
431 end
432
433 # A match between a piece of code and a string.
434 class CodeMatch
435 super NitxMatch
436
437 # Location of the code match.
438 var location: String
439
440 # Piece of code matched.
441 var content: String
442
443 redef fun make_list_item do return "* {location}"
444 end
445
446 ## exploration
447
448 # Visitor looking for initialized `MType` (new T).
449 #
450 # See `NewQuery`.
451 private class TypeInitVisitor
452 super Visitor
453
454 # `MType` name to look for.
455 var mtype_name: String
456
457 var inits = new HashSet[MType]
458 redef fun visit(node)
459 do
460 node.visit_all(self)
461 # look for init
462 if not node isa ANewExpr then return
463 var mtype = node.n_type.mtype
464 if mtype != null and mtype.name == mtype_name then inits.add(mtype)
465 end
466 end
467
468 # Visitor looking for calls to a `MProperty` (new T).
469 #
470 # See `CallQuery`.
471 private class MPropertyCallVisitor
472 super Visitor
473
474 var calls = new HashSet[MProperty]
475 redef fun visit(node)
476 do
477 node.visit_all(self)
478 if not node isa ASendExpr then return
479 calls.add node.callsite.as(not null).mproperty
480 end
481 end
482
483 # display
484
485 # A `DocArticle` that displays query results.
486 private class QueryResultArticle
487 super DocArticle
488
489 # Query linked to the results to display.
490 var query: DocCommand
491
492 # Results to display.
493 var results: Array[NitxMatch]
494
495 # Optional suggestion when no matches where found
496 var suggest: nullable Array[MEntity] = null is optional
497
498 redef fun render_title do
499 var len = results.length
500 if len == 0 then
501 addn "No result found for '{query.string}'..."
502 var suggest = self.suggest
503 if suggest != null and suggest.not_empty then
504 add "\nDid you mean "
505 var i = 0
506 for s in suggest do
507 add "`{s.full_name}`"
508 if i == suggest.length - 2 then add ", "
509 if i == suggest.length - 1 then add " or "
510 i += 1
511 end
512 add "?"
513 end
514 else
515 add "# {len} result(s) for '{query.string}'".green.bold
516 end
517 end
518
519 redef fun render_body do
520 addn ""
521 for result in results do
522 addn ""
523 addn result.make_list_item
524 end
525 end
526 end
527
528 # An article that displays a piece of code.
529 private class CodeQueryArticle
530 super DocArticle
531
532 # The query linked to the result to display.
533 var query: DocCommand
534
535 # The result to display.
536 var result: CodeMatch
537
538 redef fun render_body do
539 addn ""
540 addn "in {result.location}".gray.bold
541 addn ""
542 add result.content
543 end
544 end
545
546 # A Pager is used to display data into a unix `less` container.
547 private class Pager
548
549 # Content to display.
550 var content = new FlatBuffer
551
552 # Adds text to the pager.
553 fun add(text: String) do
554 content.append(escape(text))
555 end
556
557 fun render do sys.system("echo \"{content}\" | less -r")
558
559 fun escape(str: String): String
560 do
561 var b = new FlatBuffer
562 for c in str.chars do
563 if c == '\n' then
564 b.append("\\n")
565 else if c == '\0' then
566 b.append("\\0")
567 else if c == '"' then
568 b.append("\\\"")
569 else if c == '\\' then
570 b.append("\\\\")
571 else if c == '`' then
572 b.append("'")
573 else if c.code_point < 32 then
574 b.append("\\{c.code_point.to_base(8)}")
575 else
576 b.add(c)
577 end
578 end
579 return b.to_s
580 end
581 end