src/doc_commands: merge Article and Comment commands
[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 match between a `DocPage` and a `MEntity`.
284 class PageMatch
285 super NitxMatch
286
287 # `DocPage` matched.
288 var page: DocPage
289
290 redef fun make_list_item do
291 var page = self.page
292 if page isa MEntityPage then
293 return page.mentity.cs_list_item
294 end
295 return " * {page.title}"
296 end
297 end
298
299 # Search in class or module hierarchy of a `MEntity`.
300 #
301 # It actually searches for pages about the mentity and extracts the
302 # pre-calculated hierarchies by the `doc_post` phase.
303 abstract class HierarchiesQuery
304 super DocCommand
305
306 redef fun make_results(nitx, results, suggest) do
307 var page = new DocPage("hierarchy", "Hierarchy")
308 for result in results do
309 if not result isa PageMatch then continue
310 var rpage = result.page
311 if not rpage isa MClassPage then continue
312 page.root.add_child build_article(rpage)
313 end
314 return page
315 end
316
317 # Build an article containing the hierarchy list depending on subclasses.
318 private fun build_article(page: MClassPage): DocArticle is abstract
319 end
320
321 # List all parents of a `MClass`.
322 class AncestorsQuery
323 super HierarchiesQuery
324
325 redef fun build_article(page) do
326 return new MEntitiesListArticle(
327 "ancerstors",
328 "Ancestors for {page.mentity.name}",
329 page.ancestors.to_a)
330 end
331 end
332
333 # List direct parents of a `MClass`.
334 class ParentsQuery
335 super HierarchiesQuery
336
337 redef fun build_article(page) do
338 return new MEntitiesListArticle(
339 "parents",
340 "Parents for {page.mentity.name}",
341 page.parents.to_a)
342 end
343 end
344
345 # List direct children of a `MClass`.
346 class ChildrenQuery
347 super HierarchiesQuery
348
349 redef fun build_article(page) do
350 return new MEntitiesListArticle(
351 "children",
352 "Children for {page.mentity.name}",
353 page.children.to_a)
354 end
355 end
356
357 # List all descendants of a `MClass`.
358 class DescendantsQuery
359 super HierarchiesQuery
360
361 redef fun build_article(page) do
362 return new MEntitiesListArticle(
363 "descendants",
364 "Descendants for {page.mentity.name}",
365 page.children.to_a)
366 end
367 end
368
369 # A query to search source code from a file name.
370 redef class CodeCommand
371 # FIXME refactor this!
372 redef fun perform(nitx, doc) do
373 var res = new Array[NitxMatch]
374 var name = arg
375 # if name is an existing sourcefile, opens it
376 if name.file_exists then
377 var fr = new FileReader.open(name)
378 var content = fr.read_all
379 fr.close
380 res.add new CodeMatch(self, name, content)
381 return res
382 end
383 # else, lookup the model by name
384 for match in find_mentities(doc, name) do
385 if match.mentity isa MClass then continue
386 if match.mentity isa MProperty then continue
387 res.add new CodeMatch(self, match.mentity.cs_location, match.mentity.cs_source_code)
388 end
389 return res
390 end
391
392 redef fun make_results(nitx, results, suggest) do
393 var page = new DocPage("results", "Code Results")
394 for res in results do
395 page.add new CodeQueryArticle("results", "Results", self, res.as(CodeMatch))
396 end
397 return page
398 end
399 end
400
401 # A match between a piece of code and a string.
402 class CodeMatch
403 super NitxMatch
404
405 # Location of the code match.
406 var location: String
407
408 # Piece of code matched.
409 var content: String
410
411 redef fun make_list_item do return "* {location}"
412 end
413
414 ## exploration
415
416 # Visitor looking for initialized `MType` (new T).
417 #
418 # See `NewQuery`.
419 private class TypeInitVisitor
420 super Visitor
421
422 # `MType` name to look for.
423 var mtype_name: String
424
425 var inits = new HashSet[MType]
426 redef fun visit(node)
427 do
428 node.visit_all(self)
429 # look for init
430 if not node isa ANewExpr then return
431 var mtype = node.n_type.mtype
432 if mtype != null and mtype.name == mtype_name then inits.add(mtype)
433 end
434 end
435
436 # Visitor looking for calls to a `MProperty` (new T).
437 #
438 # See `CallQuery`.
439 private class MPropertyCallVisitor
440 super Visitor
441
442 var calls = new HashSet[MProperty]
443 redef fun visit(node)
444 do
445 node.visit_all(self)
446 if not node isa ASendExpr then return
447 calls.add node.callsite.as(not null).mproperty
448 end
449 end
450
451 # display
452
453 # A `DocArticle` that displays query results.
454 private class QueryResultArticle
455 super DocArticle
456
457 # Query linked to the results to display.
458 var query: DocCommand
459
460 # Results to display.
461 var results: Array[NitxMatch]
462
463 # Optional suggestion when no matches where found
464 var suggest: nullable Array[MEntity] = null is optional
465
466 redef fun render_title do
467 var len = results.length
468 if len == 0 then
469 addn "No result found for '{query.string}'..."
470 var suggest = self.suggest
471 if suggest != null and suggest.not_empty then
472 add "\nDid you mean "
473 var i = 0
474 for s in suggest do
475 add "`{s.full_name}`"
476 if i == suggest.length - 2 then add ", "
477 if i == suggest.length - 1 then add " or "
478 i += 1
479 end
480 add "?"
481 end
482 else
483 add "# {len} result(s) for '{query.string}'".green.bold
484 end
485 end
486
487 redef fun render_body do
488 addn ""
489 for result in results do
490 addn ""
491 addn result.make_list_item
492 end
493 end
494 end
495
496 # An article that displays a piece of code.
497 private class CodeQueryArticle
498 super DocArticle
499
500 # The query linked to the result to display.
501 var query: DocCommand
502
503 # The result to display.
504 var result: CodeMatch
505
506 redef fun render_body do
507 addn ""
508 addn "in {result.location}".gray.bold
509 addn ""
510 add result.content
511 end
512 end
513
514 # A Pager is used to display data into a unix `less` container.
515 private class Pager
516
517 # Content to display.
518 var content = new FlatBuffer
519
520 # Adds text to the pager.
521 fun add(text: String) do
522 content.append(escape(text))
523 end
524
525 fun render do sys.system("echo \"{content}\" | less -r")
526
527 fun escape(str: String): String
528 do
529 var b = new FlatBuffer
530 for c in str.chars do
531 if c == '\n' then
532 b.append("\\n")
533 else if c == '\0' then
534 b.append("\\0")
535 else if c == '"' then
536 b.append("\\\"")
537 else if c == '\\' then
538 b.append("\\\\")
539 else if c == '`' then
540 b.append("'")
541 else if c.code_point < 32 then
542 b.append("\\{c.code_point.to_base(8)}")
543 else
544 b.add(c)
545 end
546 end
547 return b.to_s
548 end
549 end