doc/commands: parser stores commands usage and help
[nit.git] / src / doc / doc_phases / doc_console.nit
index 27fd455..6a5d045 100644 (file)
 module doc_console
 
 import semantize
+import doc_commands
+import doc_poset
 import doc::console_templates
+import model::model_index
 
 # Nitx handles console I/O.
 #
@@ -57,12 +60,24 @@ class Nitx
 
        # Displays the list of available commands.
        fun help do
-               print "\nCommands:"
-               print "\tname\t\tlookup module, class and property with the corresponding 'name'"
+               print "\nCommands:\n"
+               print "\tname\t\t\tlookup module, class and property with the corresponding 'name'"
                print "\tdoc: <name::space>\tdisplay the documentation page of 'namespace'"
-               print "\tparam: <Type>\tlookup methods using the corresponding 'Type' as parameter"
-               print "\t:h\t\tdisplay this help message"
-               print "\t:q\t\tquit interactive mode"
+               print "\nType lookup:"
+               print "\tparam: <Type>\t\tlookup methods using the corresponding 'Type' as parameter"
+               print "\treturn: <Type>\t\tlookup methods returning the corresponding 'Type'"
+               print "\tnew: <Type>\t\tlookup methods creating new instances of 'Type'"
+               print "\tcall: <name>\t\tlookup methods calling 'name'"
+               print "\nHierarchy lookup:"
+               print "\tparents: <Class>\tlist direct parents of 'Class'"
+               print "\tancestors: <Class>\tlist all ancestors of 'Class'"
+               print "\tchildren: <Class>\tlist direct children of 'Class'"
+               print "\tdescendants: <Class>\tlist all descendants of 'Class'"
+               print "\nCode lookup:"
+               print "\tcode: <name>\t\tdisplay the source code associated to the 'name' entity"
+               print "\n"
+               print "\t:h\t\t\tdisplay this help message"
+               print "\t:q\t\t\tquit interactive mode"
                print ""
        end
 
@@ -73,107 +88,93 @@ class Nitx
                prompt
        end
 
+       # Parser used to process doc commands
+       var parser: DocCommandParser is lazy do
+               var parser = new DocCommandParser
+               parser.allowed_commands = ["doc", "comment", "list", "param", "return",
+                       "new", "call", "code"]
+               return parser
+       end
+
        # Processes the query string and performs it.
        fun do_query(str: String) do
-               var query = parse_query(str)
+               if str == ":q" then
+                       exit 0
+               else if str == ":h" then
+                       help
+                       return
+               end
+               var query = parser.parse(str)
+               if query == null then
+                       query = new CommentCommand(str)
+                       query.arg = str
+               end
                var res = query.perform(self, doc)
-               var page = query.make_results(self, res)
-               print page.write_to_string
-       end
-
-       # Returns an `NitxQuery` from a raw query string.
-       fun parse_query(str: String): NitxQuery do
-               var query = new NitxQuery(str)
-               if query isa NitxCommand then
-                       query.execute(self)
+               var suggest = null
+               if res.is_empty then
+                       suggest = query.suggest(self, doc)
                end
-               return query
+               var page = query.make_results(self, res, suggest)
+               print page.write_to_string
        end
 end
 
-# A query performed on Nitx.
-#
-# Queries are responsible to collect matching results and render them as a
-# DocPage.
-#
-# Used as a factory to concrete instances.
-interface NitxQuery
-
-       # Original query string.
-       fun query_string: String is abstract
-
-       # Query factory.
-       #
-       # Will return a concrete instance of NitxQuery.
-       new(query_string: String) do
-               if query_string == ":q" then
-                       return new NitxQuit
-               else if query_string == ":h" then
-                       return new NitxHelp
-               else if query_string.has_prefix("comment:") then
-                       return new CommentQuery(query_string)
-               else if query_string.has_prefix("doc:") then
-                       return new DocQuery(query_string)
-               else if query_string.has_prefix("param:") then
-                       return new ParamQuery(query_string)
-               end
-               return new CommentQuery("comment: {query_string}")
-       end
+redef class DocCommand
 
        # Looks up the `doc` model and returns possible matches.
        fun perform(nitx: Nitx, doc: DocModel): Array[NitxMatch] is abstract
 
+       # Looks up the `doc` model and returns possible suggestions.
+       fun suggest(nitx: Nitx, doc: DocModel): nullable Array[MEntity] do
+               return find_suggestions(doc, arg)
+       end
+
        # Pretty prints the results for the console.
-       fun make_results(nitx: Nitx, results: Array[NitxMatch]): DocPage do
-               var page = new DocPage("Results")
-               page.root.add_child(new QueryResultArticle(self, results))
+       fun make_results(nitx: Nitx, results: Array[NitxMatch], suggest: nullable Array[MEntity]): DocPage do
+               var page = new DocPage("results", "Results")
+               page.root.add_child(new QueryResultArticle("results", "Results", self, results, suggest))
                return page
        end
 
-       redef fun to_s do return query_string
+       # Lookup mentities based on a `query` string.
+       #
+       # 1- lookup by first name (returns always one value)
+       # 2- lookup by name (can return conflicts)
+       fun find_mentities(doc: DocModel, query: String): Array[MEntityMatch] do
+               var res = new Array[MEntityMatch]
+
+               # First lookup by full_name
+               var mentity = doc.mentity_by_full_name(query)
+               if mentity != null then
+                       res.add new MEntityMatch(self, mentity)
+                       return res
+               end
+
+               # If no results, lookup by name
+               for m in doc.mentities_by_name(query) do
+                       res.add new MEntityMatch(self, m)
+               end
+
+               return res
+       end
+
+       # Suggest some mentities based on a `query` string.
+       fun find_suggestions(doc: DocModel, query: String): Array[MEntity] do
+               return doc.find(query, 3)
+       end
 end
 
-# Something that matches a `NitxQuery`.
+# Something that matches a `DocCommand`.
 abstract class NitxMatch
 
        # Query matched by `self`.
-       var query: NitxQuery
+       var query: DocCommand
 
        # Pretty prints `self` for console.
        fun make_list_item: String is abstract
 end
 
-# A query that contains a meta command.
-#
-# In Nitx, commands are written such as `command: args...`.
-abstract class MetaQuery
-       super NitxQuery
-
-       redef var query_string
-
-       # Meta command used.
-       var command: String is noinit
-
-       # Arguments passed to the `command`.
-       var args = new Array[String]
-
-       init do
-               # parse command
-               var str = new FlatBuffer
-               var i = 0
-               while i < query_string.length do
-                       var c = query_string[i]
-                       i += 1
-                       if c == ':' then break
-                       str.add c
-               end
-               command = str.write_to_string
-               # parse args
-               args.add query_string.substring_from(i).trim
-       end
-end
-
-# A match between a `NitxQuery` and a `MEntity`.
+# A match between a `DocCommand` and a `MEntity`.
 class MEntityMatch
        super NitxMatch
 
@@ -183,26 +184,16 @@ class MEntityMatch
        redef fun make_list_item do return mentity.cs_list_item
 end
 
-# A query to search a `MEntity` comment by its name or namespace.
-class CommentQuery
-       super MetaQuery
+redef class CommentCommand
+       redef fun perform(nitx, doc) do return find_mentities(doc, arg)
 
-       redef fun perform(nitx, doc) do
-               var name = args.first
-               var res = new Array[NitxMatch]
-               for mentity in doc.search_mentities(name) do
-                       res.add new MEntityMatch(self, mentity)
-               end
-               return res
-       end
-
-       redef fun make_results(nitx, results) do
+       redef fun make_results(nitx, results, suggest) do
                var len = results.length
                if len == 1 then
                        var res = results.first.as(MEntityMatch)
                        var mentity = res.mentity
-                       var page = new DocPage("Results")
-                       var article = new DefinitionArticle(mentity)
+                       var page = new DocPage("resultats", "Results")
+                       var article = new DefinitionArticle("results", "Results", mentity)
                        article.cs_title = mentity.name
                        article.cs_subtitle = mentity.cs_declaration
                        page.root.add_child article
@@ -214,12 +205,10 @@ class CommentQuery
 end
 
 # A query to search signatures using a specific `MType` as parameter.
-class ParamQuery
-       super MetaQuery
-
+redef class ParamCommand
        redef fun perform(nitx, doc) do
                var res = new Array[NitxMatch]
-               var mtype_name = args.first
+               var mtype_name = arg
                for mproperty in doc.mproperties do
                        if not mproperty isa MMethod then continue
                        var msignature = mproperty.intro.msignature
@@ -235,37 +224,59 @@ class ParamQuery
        end
 end
 
-# A query to search a Nitdoc documentation page by its name.
-class DocQuery
-       super MetaQuery
+# A query to search signatures using a specific `MType` as return.
+redef class ReturnCommand
+       redef fun perform(nitx, doc) do
+               var res = new Array[NitxMatch]
+               var mtype_name = arg
+               for mproperty in doc.mproperties do
+                       if not mproperty isa MMethod then continue
+                       var msignature = mproperty.intro.msignature
+                       if msignature != null then
+                               var mreturn = msignature.return_mtype
+                               if mreturn != null and mreturn.name == mtype_name then
+                                       res.add new MEntityMatch(self, mproperty)
+                               end
+                       end
+               end
+               return res
+       end
+end
 
+# A query to search methods creating new instances of a specific `MType`.
+redef class NewCommand
        redef fun perform(nitx, doc) do
                var res = new Array[NitxMatch]
-               var name = args.first
-               for page in doc.pages do
-                       if name == "*" then # FIXME dev only
-                               res.add new PageMatch(self, page)
-                       else if page.title == name then
-                               res.add new PageMatch(self, page)
-                       else if page isa MEntityPage and page.mentity.cs_namespace == name then
-                               res.add new PageMatch(self, page)
+               var mtype_name = arg
+               for mpropdef in doc.mpropdefs do
+                       var visitor = new TypeInitVisitor(mtype_name)
+                       var npropdef = nitx.ctx.modelbuilder.mpropdef2node(mpropdef)
+                       if npropdef == null then continue
+                       visitor.enter_visit(npropdef)
+                       for i in visitor.inits do
+                               res.add new MEntityMatch(self, mpropdef)
                        end
                end
                return res
        end
+end
 
-       redef fun make_results(nitx, results) do
-               var len = results.length
-               # FIXME how to render the pager for one worded namespaces like "standard"?
-               if len == 1 then
-                       var page = results.first.as(PageMatch).page
-                       var pager = new Pager
-                       pager.add page.write_to_string
-                       pager.render
-                       return page
-               else
-                       return super
+# A query to search methods calling a specific `MProperty`.
+redef class CallCommand
+       redef fun perform(nitx, doc) do
+               var res = new Array[NitxMatch]
+               var mprop_name = arg
+               for mpropdef in doc.mpropdefs do
+                       var visitor = new MPropertyCallVisitor
+                       var npropdef = nitx.ctx.modelbuilder.mpropdef2node(mpropdef)
+                       if npropdef == null then continue
+                       visitor.enter_visit(npropdef)
+                       for mprop in visitor.calls do
+                               if mprop.name != mprop_name then continue
+                               res.add new MEntityMatch(self, mpropdef)
+                       end
                end
+               return res
        end
 end
 
@@ -285,57 +296,156 @@ class PageMatch
        end
 end
 
-# A query that contains a nitx command.
+# Search in class or module hierarchy of a `MEntity`.
 #
-# These commands are prefixed with `:` and are used to control the execution of
-# `nitx` like displaying the help or quiting.
-interface NitxCommand
-       super NitxQuery
+# It actually searches for pages about the mentity and extracts the
+# pre-calculated hierarchies by the `doc_post` phase.
+abstract class HierarchiesQuery
+       super DocCommand
+
+       redef fun make_results(nitx, results, suggest) do
+               var page = new DocPage("hierarchy", "Hierarchy")
+               for result in results do
+                       if not result isa PageMatch then continue
+                       var rpage = result.page
+                       if not rpage isa MClassPage then continue
+                       page.root.add_child build_article(rpage)
+               end
+               return page
+       end
 
-       # Executes the command.
-       fun execute(nitx: Nitx) is abstract
+       # Build an article containing the hierarchy list depending on subclasses.
+       private fun build_article(page: MClassPage): DocArticle is abstract
 end
 
-# Exits nitx.
-class NitxQuit
-       super NitxCommand
+# List all parents of a `MClass`.
+class AncestorsQuery
+       super HierarchiesQuery
 
-       redef fun execute(nitx) do exit 0
+       redef fun build_article(page) do
+               return new MEntitiesListArticle(
+                       "ancerstors",
+                       "Ancestors for {page.mentity.name}",
+                       page.ancestors.to_a)
+       end
 end
 
-# Displays the help message.
-class NitxHelp
-       super NitxCommand
+# List direct parents of a `MClass`.
+class ParentsQuery
+       super HierarchiesQuery
 
-       redef fun execute(nitx) do nitx.help
+       redef fun build_article(page) do
+               return new MEntitiesListArticle(
+                       "parents",
+                       "Parents for {page.mentity.name}",
+                       page.parents.to_a)
+       end
 end
 
-## exploration
+# List direct children of a `MClass`.
+class ChildrenQuery
+       super HierarchiesQuery
 
-redef class DocModel
-
-       # Lists all MEntities in the model.
-       private var mentities: Collection[MEntity] is lazy do
-               var res = new HashSet[MEntity]
-               res.add_all mprojects
-               res.add_all mgroups
-               res.add_all mmodules
-               res.add_all mclasses
-               res.add_all mclassdefs
-               res.add_all mproperties
-               res.add_all mpropdefs
-               return res
+       redef fun build_article(page) do
+               return new MEntitiesListArticle(
+                       "children",
+                       "Children for {page.mentity.name}",
+                       page.children.to_a)
        end
+end
+
+# List all descendants of a `MClass`.
+class DescendantsQuery
+       super HierarchiesQuery
 
-       # Search MEntities that match `name` by their name or namespace.
-       private fun search_mentities(name: String): Array[MEntity] do
-               var res = new Array[MEntity]
-               for mentity in mentities do
-                       if mentity.name != name and mentity.cs_namespace != name then continue
-                       res.add mentity
+       redef fun build_article(page) do
+               return new MEntitiesListArticle(
+                       "descendants",
+                       "Descendants for {page.mentity.name}",
+                       page.children.to_a)
+       end
+end
+
+# A query to search source code from a file name.
+redef class CodeCommand
+       # FIXME refactor this!
+       redef fun perform(nitx, doc) do
+               var res = new Array[NitxMatch]
+               var name = arg
+               # if name is an existing sourcefile, opens it
+               if name.file_exists then
+                       var fr = new FileReader.open(name)
+                       var content = fr.read_all
+                       fr.close
+                       res.add new CodeMatch(self, name, content)
+                       return res
+               end
+               # else, lookup the model by name
+               for match in find_mentities(doc, name) do
+                       if match.mentity isa MClass then continue
+                       if match.mentity isa MProperty then continue
+                       res.add new CodeMatch(self, match.mentity.cs_location, match.mentity.cs_source_code)
                end
                return res
        end
+
+       redef fun make_results(nitx, results, suggest) do
+               var page = new DocPage("results", "Code Results")
+               for res in results do
+                       page.add new CodeQueryArticle("results", "Results", self, res.as(CodeMatch))
+               end
+               return page
+       end
+end
+
+# A match between a piece of code and a string.
+class CodeMatch
+       super NitxMatch
+
+       # Location of the code match.
+       var location: String
+
+       # Piece of code matched.
+       var content: String
+
+       redef fun make_list_item do return "* {location}"
+end
+
+## exploration
+
+# Visitor looking for initialized `MType` (new T).
+#
+# See `NewQuery`.
+private class TypeInitVisitor
+       super Visitor
+
+       # `MType` name to look for.
+       var mtype_name: String
+
+       var inits = new HashSet[MType]
+       redef fun visit(node)
+       do
+               node.visit_all(self)
+               # look for init
+               if not node isa ANewExpr then return
+               var mtype = node.n_type.mtype
+               if mtype != null and mtype.name == mtype_name then inits.add(mtype)
+       end
+end
+
+# Visitor looking for calls to a `MProperty` (new T).
+#
+# See `CallQuery`.
+private class MPropertyCallVisitor
+       super Visitor
+
+       var calls = new HashSet[MProperty]
+       redef fun visit(node)
+       do
+               node.visit_all(self)
+               if not node isa ASendExpr then return
+               calls.add node.callsite.as(not null).mproperty
+       end
 end
 
 # display
@@ -345,17 +455,32 @@ private class QueryResultArticle
        super DocArticle
 
        # Query linked to the results to display.
-       var query: NitxQuery
+       var query: DocCommand
 
        # Results to display.
        var results: Array[NitxMatch]
 
+       # Optional suggestion when no matches where found
+       var suggest: nullable Array[MEntity] = null is optional
+
        redef fun render_title do
                var len = results.length
                if len == 0 then
-                       add "No result found for '{query.query_string}'..."
+                       addn "No result found for '{query.string}'..."
+                       var suggest = self.suggest
+                       if suggest != null and suggest.not_empty then
+                               add "\nDid you mean "
+                               var i = 0
+                               for s in suggest do
+                                       add "`{s.full_name}`"
+                                       if i == suggest.length - 2 then add ", "
+                                       if i == suggest.length - 1 then add " or "
+                                       i += 1
+                               end
+                               add "?"
+                       end
                else
-                       add "# {len} result(s) for '{query.query_string}'".green.bold
+                       add "# {len} result(s) for '{query.string}'".green.bold
                end
        end
 
@@ -368,6 +493,24 @@ private class QueryResultArticle
        end
 end
 
+# An article that displays a piece of code.
+private class CodeQueryArticle
+       super DocArticle
+
+       # The query linked to the result to display.
+       var query: DocCommand
+
+       # The result to display.
+       var result: CodeMatch
+
+       redef fun render_body do
+               addn ""
+               addn "in {result.location}".gray.bold
+               addn ""
+               add result.content
+       end
+end
+
 # A Pager is used to display data into a unix `less` container.
 private class Pager
 
@@ -395,8 +538,8 @@ private class Pager
                                b.append("\\\\")
                        else if c == '`' then
                                b.append("'")
-                       else if c.ascii < 32 then
-                               b.append("\\{c.ascii.to_base(8, false)}")
+                       else if c.code_point < 32 then
+                               b.append("\\{c.code_point.to_base(8)}")
                        else
                                b.add(c)
                        end