sthames42
sthames42

Reputation: 1009

How to build your own Intellisense XML file

For reasons I'd rather not debate, I wanted to create an Intellisense XML file from the XML output of Doxygen for a NuGet package. After studying all I could find about the format of the file, I created an XML Stylesheet (XSLT) to create the Intellisense file. Unfortunately, Visual Studio would not load the file at all. There is no way to diagnose the problem since there does not seem to be any logging or diagnostic capability regarding the loading of the Intellisense database and no help to be found on the Internet.

Using Visual Studio 2019 16.11 and .NET CLI:

  1. Make sure <GenerateDocumentFile> is true in the project file.
    dotnet pack will not include the Intellisense file without this.
  2. Build library.
  3. Run Doxygen to generate XML.
  4. Run SaxonJS xslt3 to translate the Doxygen XML to Intellisense XML.
  5. Copy new Intellisense XML file over the one in the build output folder.
  6. Create the NuGet package (pack).
  7. Confirm package includes XML file in lib folder.
  8. Add package to local NuGet registry.
  9. Install package from local registry to test project.

No joy. Intellisense does not work.

This has become an intellectually exercise, at this point. While it would be nice to have, I have to move on for now. But if anyone has any idea what step I could be missing or any information about how delicate the process is for loading the intellisense database, I welcome any suggestions.

Upvotes: 3

Views: 787

Answers (1)

sthames42
sthames42

Reputation: 1009

Ok, so after making sure all my generated XML file looked exactly like the one generated by VS, looking at the contents of the DLL file to see if there is anything tying the two files together, and even experimenting with making sure the DLL and XML file creation timestamps were the same, turns out the problem was the output file encoding must be UTF-8 without the Byte-Order Mark (BOM)!

So, stupid is as stupid does, I guess, because it's not the first time I've run into something not working because of the wrong file encoding.

After fixing that, all I had to do was move the Intellisense XML file to the build output folder, pack, and publish the library.

For those interested, I use Doxygen so my code documentation can be more readable inside the code. Doxygen offers so many more documentation features, including the use of Markdown, and I've never been a fan of the XML Doc style. XML documentation can be read by Doxygen but, if you're already doing that, you don't need this.

The XML stylesheet I created works fine for what I want but it is, by no means, complete. Also, this is my first attempt at writing XSL so please be kind.

<?xml version="1.0" encoding="UTF-8"?>
<!--****************************************************************************
* Build Intellisense Documentation XML File from Doxygen Output.
*****************************************************************************-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="xml" version="1.0" indent="yes" standalone="yes" encoding="UTF-8"/>
  <xsl:param name="assemblyName" select="assemblyName"/>
  
  <xsl:template match="/">
    <doc xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <assembly>
          <name><xsl:value-of select="$assemblyName"/></name>
      </assembly>
      <members>
        <!--===============================================================-->
        <!-- Sort class/interface definitions by file path and class name. -->
        <!--===============================================================-->
        <xsl:for-each select="doxygen/compounddef">
          <xsl:sort select="location/@file"/>
          <xsl:sort select="compoundname"/>
          
          <!--====================================================-->
          <!-- Build 'T:' (Type) definition for class/interface. -->
          <!-- Convert `compoundname` (ns::ns::name) to           -->
          <!-- fully-qualified `$classname` (ns.ns.name).         -->
          <!--====================================================-->
          <xsl:if test="@kind='class' or @kind='interface'">
            <xsl:variable name="classname" select="translate(normalize-space(translate(compoundname,':',' ')),' ','.')"/>
            <xsl:call-template name="MakeMember">
              <xsl:with-param name="type" select="'T'"/>
              <xsl:with-param name="name" select="$classname" />
            </xsl:call-template>
            
            <!--============================================================================-->
            <!-- Build definitions for class/interface members. Doxygen may include        -->
            <!-- `memberdef` nodes for inherited members and Intellisense only wants one    -->
            <!-- definition. Filter any members that do not belong to this class/interface. -->
            <!--============================================================================-->
            <xsl:for-each select="sectiondef/memberdef[qualifiedname=concat($classname, '.', name)]">
              <xsl:choose>
                <xsl:when test="@kind='enum'">
                  <xsl:call-template name="MakeMember">
                    <xsl:with-param name="type" select="'T'"/>
                  </xsl:call-template>
                </xsl:when>
                <xsl:when test="@kind='property'">
                  <xsl:call-template name="MakeMember">
                    <xsl:with-param name="type" select="'P'"/>
                  </xsl:call-template>
                </xsl:when>
                <xsl:when test="@kind='variable'">
                  <xsl:call-template name="MakeMember">
                    <xsl:with-param name="type" select="'F'"/>
                  </xsl:call-template>
                </xsl:when>
                
                <!--=======================================================================-->
                <!-- Constructor member functions are named `#ctor` in Intellisense.       -->
                <!-- Change the member name for constructors else use the `qualifiedname`. -->
                <!--=======================================================================-->
                <xsl:when test="@kind='function'">
                  <xsl:call-template name="MakeMember">
                    <xsl:with-param name="type" select="'M'"/>
                    <xsl:with-param name="name">
                      <xsl:choose>
                        <xsl:when test="name = substring($classname, string-length($classname)-string-length(name)+1)">
                          <xsl:value-of select="concat($classname,'.#ctor')"/>
                        </xsl:when>
                        <xsl:otherwise>
                          <xsl:value-of select="qualifiedname"/>
                        </xsl:otherwise>
                      </xsl:choose>
                    </xsl:with-param>
                  </xsl:call-template>
                </xsl:when>
              </xsl:choose>
            </xsl:for-each>
          </xsl:if>
        </xsl:for-each>
      </members>
    </doc>
  </xsl:template>
  
  <!--=========================================-->
  <!-- Nodes to copy and rename, if necessary. -->
  <!--=========================================-->
  <xsl:template match="del">           <xsl:call-template name="copy-node"/></xsl:template>
  <xsl:template match="ins">           <xsl:call-template name="copy-node"/></xsl:template>
  <xsl:template match="para">          <xsl:call-template name="copy-node"/></xsl:template>
  <xsl:template match="small">         <xsl:call-template name="copy-node"/></xsl:template>
  <xsl:template match="bold">          <xsl:call-template name="copy-node"><xsl:with-param name="tag" select="'b'"/></xsl:call-template></xsl:template>
  <xsl:template match="emphasis">      <xsl:call-template name="copy-node"><xsl:with-param name="tag" select="'i'"/></xsl:call-template></xsl:template>
  <xsl:template match="underline">     <xsl:call-template name="copy-node"><xsl:with-param name="tag" select="'u'"/></xsl:call-template></xsl:template>
  <xsl:template match="subscript">     <xsl:call-template name="copy-node"><xsl:with-param name="tag" select="'sub'"/></xsl:call-template></xsl:template>
  <xsl:template match="superscript">   <xsl:call-template name="copy-node"><xsl:with-param name="tag" select="'sup'"/></xsl:call-template></xsl:template>
  <xsl:template match="computeroutput"><xsl:call-template name="copy-node"><xsl:with-param name="tag" select="'c'"/></xsl:call-template></xsl:template>
  <xsl:template match="ulink">         <xsl:call-template name="copy-node"><xsl:with-param name="tag" select="'a'"/></xsl:call-template></xsl:template>
  
  <xsl:template match="preformatted">
    <code><xsl:value-of select="." /></code>
  </xsl:template>
  
  <!--====================================================================-->
  <!-- MakeMember - Builds the member definition from Doxygen `memberdef` -->
  <!-- node. `type` is member type identifier and `name` is member name   -->
  <!-- and defaults to `qualifiedname`. Member ID is `<type>:<name>`.     -->
  <!--====================================================================-->
  <xsl:template name="MakeMember">
    <xsl:param name="type"/>
    <xsl:param name="name" select="qualifiedname"/>
    
    <!--===============================================-->
    <!-- Build member definition only if there is one. -->
    <!--===============================================-->
    <xsl:if test="briefdescription != '' or detaileddescription != ''">
      <member>
        <xsl:attribute name="name">
          <xsl:value-of select="$type"/>:<xsl:value-of select="$name"/><xsl:value-of select="argsstring"/>
        </xsl:attribute>
        
        <!--=================================================================-->
        <!-- Include summary only if there is any description of the member. -->
        <!--                                                                 -->
        <!-- Note: This selection of `detaileddescription` will grab any     -->
        <!-- text nodes before `parameterlist` but will exclude following    -->
        <!-- text nodes. There should be none, anyway.                       -->
        <!--=================================================================-->
        <xsl:variable name="detailed" select="detaileddescription/node()[not(self::para/parameterlist)]"/>
        <xsl:if test="briefdescription != '' or $detailed != ''">
          <summary>
            <xsl:apply-templates select="briefdescription"/>
            <xsl:apply-templates select="detaileddescription/node()[not(self::para/parameterlist)]"/> 
          </summary>
        </xsl:if>
        
        <!--=======================================-->
        <!-- Build list of parameters and returns. -->
        <!--=======================================-->
        <xsl:for-each select="detaileddescription/para/parameterlist[@kind='param']/parameteritem">
          <param><xsl:attribute name="name"><xsl:value-of select="parameternamelist[1]/parametername"/></xsl:attribute>
            <xsl:apply-templates select="parameterdescription/para"/>
          </param>
        </xsl:for-each>
        <xsl:for-each select="detaileddescription/para/simplesect[@kind='return']">
          <return><xsl:apply-templates select="node()"/></return>
        </xsl:for-each>
      </member>
    </xsl:if>
  </xsl:template>
  
  <!--================================================-->
  <!-- copy-node - Simple copy of nodes that come     -->
  <!-- over as is or must be renamed. `tag` is target -->
  <!-- node name and defaults to `local-name()`.      -->
  <!--================================================-->
  <xsl:template name="copy-node">
    <xsl:param name="tag" select="local-name()"/>
    <xsl:element name="{$tag}">
      <xsl:copy-of select="@*"/>
      <xsl:apply-templates select="node()" />
    </xsl:element>
  </xsl:template>
  
</xsl:stylesheet>

Doxygen XML Output

This stylesheet expects input of a single file so first all the XML files must be combined. Given XML folder tree in docs folder (default), here is how to combine all the files in the folder tree using SaxonJS:

xslt3 -s:docs\input.xml -xsl:docs\combine.xslt -o:combined.xml

combined.xml file is tree-structured with class/interface definitions containing member definitions.

<doxygen>
  <compounddef kind="<class/interface>">
    <compoundname>ns::ns::classname</compoundname>
    <location file="<relativeFilePath>"/>
    <sectiondef>
      <memberdef kind="<enum/function/property/variable>">
        <name>memberName</name>
        <qualifiedname>ns.ns.className.memberName</qualifiedname>
        <briefdescription>
          <para>
            Brief description generally up to the first period (.)
            from the code comments.
          </para>
        <detaileddescription>
          <para>
            A more detailed description.</para>
          <para>
            <parameterlist kind="param">
              <parameteritem>
                <parameternamelist>
                  <parametername>paramName</parametername>
                </parameternamelist>
                <parameterdescription>
                  <para>
                    A brief description of the parameter.
                  </para>
                </parameterdescription>
              </parameteritem>
            </parameterlist>
            <simplesect kind="return">
              <para>
                Description of the return value.
              </para>
            </simplesect>
          </para>
        </detaileddescription>
      </memberdef>
      <memberdef/>
      ...
    </sectiondef>
  </compounddef>
  <compounddef>
  ...
<doxygen>

Intellisense XML File

Given the combined.xml file created from the Doxygen XML Output, here is how to create the Intellisense XML file using SaxonJS:

xslt3 -s:combined.xml -xsl:BuildIntellisense.xslt -o:<AssemblyName>.xml

This file contains a flat-list of all library members with a unique identifier in the format of <type>:<qualifiedname>, where type is a single character identifier. Members appear to be defined in the order they are found on the disk within the project.

<doc>
  <assembly>
      <name>assemblyName</name>
  </assembly>
  <members>
    <member name="T:ns.ns.className">
      <summary>
        <para>
          From content of `<briefdescription>` and `<detaileddescription>` text nodes.
        </para>
        ...
      </summary>
      <param name="paramName">
        <para>Parameter description</para>
      </param>
      ...
      <returns>Return value description.</returns
    </member>
    <member name="M:ns.ns.className.#ctor">
      <summary>
        <para>
          From content of `<briefdescription>` and `<detaileddescription>` text nodes.
        </para>
        ...
      </summary>
      <param name="paramName">
        <para>Parameter description</para>
      </param>
      ...
      <returns>Return value description.</returns>
    </member>
    ...
  </members>
</doc>

See Also

Update

FYI, while the intellisense file this creates does work, not all elements in the library show the documentation from the file as they should. It works for some but not for others. Does not appear to work for any of the member methods but class definitions, yes. I will continue to try and diagnose this, when I have the time, and will post the results here if I find solutions.

Upvotes: 4

Related Questions