Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xml documentation comments imported into View Variables #5430

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1343168
docstrings in view variables mvp
Mervill Sep 7, 2024
5e745f2
working with fields and properties
Mervill Sep 8, 2024
457671d
move this code since we'll be expanding it
Mervill Sep 8, 2024
26a7115
Merge branch 'master' into vv-docstring
Mervill Sep 9, 2024
a5a4fdc
boy I sure do love processing xml, yep.
Mervill Sep 10, 2024
349c431
not as much string cleanup as I expected
Mervill Sep 10, 2024
2aa841d
more robustness and cleanup
Mervill Sep 10, 2024
732305a
I met a man without a name in a meadow beyond time and he whispered t…
Mervill Sep 10, 2024
3637c6e
comment
Mervill Sep 10, 2024
a19b647
minor tabs-vs-spaces skirmish, spaces victorious
Mervill Sep 10, 2024
0fda1a7
contemplate the sound of wisdom, recover incremental knowlage about m…
Mervill Sep 11, 2024
7d4cd92
update gitignore
Mervill Sep 11, 2024
cd2e65f
slightly cleaner defensive programming
Mervill Sep 11, 2024
e23bbbd
Add XmlDocTool - a utility program for reducing the size of the gener…
Mervill Sep 11, 2024
2a80d50
Move XmlDocTool into a Robust folder/namespace. use Path.Combine to g…
Mervill Sep 12, 2024
887f63f
add support for the viewvariables custom block in xml comments
Mervill Sep 12, 2024
7a62c0a
Merge branch 'master' into vv-docstring
Mervill Sep 12, 2024
64226ce
error format and return code in xmldoctool
Mervill Sep 13, 2024
63fecf8
xmldoctool output path
Mervill Sep 13, 2024
dc6b30b
run xmldoctool as a post build event
Mervill Sep 13, 2024
e87debc
Merge branch 'master' into vv-docstring
Mervill Sep 13, 2024
0c4376e
better msbuild spellcasting
Mervill Sep 13, 2024
727ecf9
the ritual is complete, the line has folded and become a circle
Mervill Sep 14, 2024
967d33e
update comments
Mervill Sep 14, 2024
77d6a0d
finally add xmldoc tool to the sln
Mervill Sep 14, 2024
d076244
more comments
Mervill Sep 14, 2024
dd940cc
add xmldoc comments to the vv test object
Mervill Sep 14, 2024
4c746b7
make vvdocpath relative so it plays better with linux
Mervill Sep 14, 2024
ac39f18
maybe the linux gotcha has been defeated
Mervill Sep 14, 2024
32a241e
Merge branch 'master' into vv-docstring
Mervill Sep 19, 2024
a346ec0
remove this
Mervill Sep 24, 2024
fb95556
the refresh button in the vv window wont expand verticaly if there is…
Mervill Sep 24, 2024
90790c4
detritus
Mervill Sep 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,6 @@ MSBuild/Robust.Custom.targets
release/
Robust.Docfx/*-site
Robust.Docfx/api

# ignore compiler-generated xml docs
Resources/ViewVariables/
3 changes: 3 additions & 0 deletions MSBuild/Robust.Properties.targets
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@

<!-- serialization generator -->
<Import Project="Robust.Serialization.Generator.targets" Condition="'$(SkipRobustAnalyzer)' != 'true'" />

<!-- viewvariable xml doc generation -->
<Import Project="Robust.ViewVariables.targets" Condition="'$(ViewVariables)' == 'true'" />
</Project>
62 changes: 62 additions & 0 deletions MSBuild/Robust.ViewVariables.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!--
This is the MSBuild file that drives the creation of xml docs for use by View Variables.

Normally this is invoked from `Robust.Properties.targets` when a csproj defines `<ViewVariables>`
in its `<PropertyGroup>`.

The workflow is:

0. Ensure the XmlDocTool is built before any project that uses this system
1. Make sure `..\Resources\ViewVariables\` exists
2. Get MSBuild to generate the xml doc at the path from step 1.
3. Run XmlDocTool on the generated doc

Step two avoids using `<DocumentationFile>` due to a weird quirk of how that facility
works. DocumentationFile always causes two copies of the generated xml file to be
created, one at the path in DocumentationFile and one next to the DLL. This script
uses the somewhat arcane facility `<DocFileItem>` to side-step that problem and create
a single copy of the xml docs in the folder we want.

Project files (.csproj) outside Robust Toolbox need to define these properties:

```
<PropertyGroup>
<ViewVariables>true</ViewVariables>
<VVRobustProjectDir>{path to robust toolbox}</VVRobustProjectDir>
</PropertyGroup>
```

VVRobustProjectDir is required so that projects outside Robust Toolbox can properly find the
XmlDocTool project and the resulting executable. This is perhaps not as seemless as it could be,
but it's good enough for now :tm:

see also
github.com/dotnet/msbuild/issues/1559
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<!-- Add a reference to XmlDocTool so it always gets built before any project that needs it -->
<ProjectReference Include="..\$(VVRobustProjectDir)\Robust.XmlDocTool\Robust.XmlDocTool.csproj">
<!-- Don't copy the build artifacts from this project to the referencing project's output folder -->
<Private>false</Private>
<!-- Indicate that the referencing project isn't actually meant to reference this project's assembly directily -->
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<Target Name="GenerateXmlDoc" BeforeTargets="CoreCompile">
<!-- Ensure the destination folder for the docs exists -->
<MakeDir Directories="..\Resources\ViewVariables\"/>
<PropertyGroup>
<!-- This is just for convenience so the lines below don't get unreasonably long -->
<VVDocPath>..\Resources\ViewVariables\$(MSBuildProjectName).xml</VVDocPath>
</PropertyGroup>
<ItemGroup>
<!-- Slightly arcane invocation of the MSBuild doc generation facility that bypasses the issue explained above -->
<DocFileItem Include="$(VVDocPath)" />
</ItemGroup>
</Target>
<Target Name="TrimXmlDoc" AfterTargets="PostBuildEvent">
<!-- Run XmlDocTool on the generated doc file -->
<Exec Command="$([MSBuild]::NormalizePath('$(ProjectDir)\..\$(VVRobustProjectDir)\bin\XmlDocTool\Robust.XmlDocTool')) $(VVDocPath)" />
</Target>
</Project>
1 change: 1 addition & 0 deletions Robust.Client/Robust.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<NoWarn>NU1701;CA1416</NoWarn>
<OutputPath>../bin/Client</OutputPath>
<RobustILLink>true</RobustILLink>
<ViewVariables>true</ViewVariables>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DiscordRichPresence" PrivateAssets="compile" />
Expand Down
244 changes: 244 additions & 0 deletions Robust.Client/ViewVariables/ClientViewVariablesManager.DocStrings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Xml;
using Robust.Shared.Utility;

namespace Robust.Client.ViewVariables
{
internal sealed partial class ClientViewVariablesManager
{
private const string ViewVariablesResourcesRoot = "/ViewVariables/";

private readonly Dictionary<string, string> _docStrings = new();

public void LoadDocStrings()
{
// todo: lots of clients (most clients actually) wont ever open VV so we
// might be able to just skip loading the doc strings in a majority of cases.

foreach (var resPath in _resManager.ContentFindFiles(ViewVariablesResourcesRoot))
{
using var resStream = _resManager.ContentFileRead(resPath);

var xmlDoc = new XmlDocument();
try
{
xmlDoc.Load(resStream);
}
catch (XmlException ex)
{
Sawmill.Warning($"DocString xml file at `{resPath}` failed to load with exception: {ex}");
continue;
}

if (!TryGetChildNode(xmlDoc, "doc", out var docNode))
{
Sawmill.Warning($"DocString xml file at `{resPath}` lacks `doc` node!");
continue;
}

if (!TryGetChildNode(docNode, "members", out var membersNode))
{
Sawmill.Warning($"DocString xml file at `{resPath}` lacks `members` node!");
continue;
}

foreach (XmlNode memberNode in membersNode.ChildNodes)
{
// skips comment blocks, whitespace between elements
if (memberNode.NodeType != XmlNodeType.Element)
continue;

DebugTools.Assert(memberNode.Name == "member");

if (!TryGetAttributeText(memberNode, "name", out var memberName))
continue;

// skip method definitions
if (memberName.StartsWith("M:"))
continue;

string docString;

if (TryGetChildNode(memberNode, "viewvariables", out var viewVariablesNode))
{
var xmlString = ProcessDocNode(viewVariablesNode);
docString = TrimLines(xmlString);
}
else if (TryGetChildNode(memberNode, "summary", out var summaryNode))
{
var xmlString = ProcessDocNode(summaryNode);
docString = TrimLines(xmlString);
}
else
{
docString = "DocString is invalid.";
}

_docStrings.Add(memberName, docString);
}
}
}

private string ProcessDocNode(XmlNode xmlNode)
{
var sb = new StringBuilder();
foreach (XmlNode childNode in xmlNode.ChildNodes)
{
switch (childNode.NodeType)
{
case XmlNodeType.Text:
sb.Append(childNode.InnerText);
break;
case XmlNodeType.Element:
ProcessXmlElement(childNode, sb);
break;
}
}
return sb.ToString();
}

/// <summary>
/// Try to process the most common tags inside xml comment blocks
/// </summary>
/// <param name="elementNode">The node to process</param>
/// <param name="sb">A <see cref="StringBuilder"/> that the result will be appended to</param>
/// <remarks>
/// Ideally all the tags on <see href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags">this page</see>
/// should be minimally supported.
/// </remarks>
private void ProcessXmlElement(XmlNode elementNode, StringBuilder sb)
{
DebugTools.Assert(elementNode.NodeType == XmlNodeType.Element);
switch (elementNode.Name)
{
case "see":
{
if (!string.IsNullOrEmpty(elementNode.InnerText))
{
sb.Append(elementNode.InnerText);
}
else if (TryGetAttributeText(elementNode, "cref", out var crefText))
{
sb.Append(crefText);
}
else if (TryGetAttributeText(elementNode, "langword", out var langwordText))
{
sb.Append(langwordText);
}
break;
}
case "seealso":
{
DebugTools.Assert(string.IsNullOrEmpty(elementNode.InnerText));
if (TryGetAttributeText(elementNode, "cref", out var crefText))
{
sb.Append(crefText);
}
break;
}
case "b": // bold text
case "i": // italic text
//case "u":
//case "a":
case "c": // text should be formatted as code
case "code": // text should be formatted as /multiline/ code
case "para": // paragraph text
case "value": // value
{
DebugTools.Assert(!string.IsNullOrEmpty(elementNode.InnerText));
sb.Append(elementNode.InnerText);
break;
}
case "br":
{
// line break
break;
}
case "paramref":
{
DebugTools.Assert(string.IsNullOrEmpty(elementNode.InnerText));
if (TryGetAttributeText(elementNode, "name", out var innerText))
{
sb.Append(innerText);
}
break;
}
case "inheritdoc":
{
break;
}
}
}

private static string TrimLines(string inputString)
{
var splitOpts = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries;
var lines = inputString.Split('\n', splitOpts);
return string.Join(Environment.NewLine, lines);
}

public static bool TryGetChildNode(XmlNode node, string childName, [NotNullWhen(true)] out XmlNode? element)
{
element = node[childName];
return element != null;
}

private static bool TryGetAttributeText(XmlNode xmlNode, string attributeName, [NotNullWhen(true)] out string? attributeText)
{
attributeText = null;
var attributes = xmlNode.Attributes;
if (attributes != null)
{
var attribute = attributes[attributeName];
if (attribute != null)
{
attributeText = attribute.InnerText;
return true;
}
}
return false;
}

public string GetDocStringForType(string key)
{
if (_docStrings.TryGetValue($"T:{key}", out string? docString))
{
return docString;
}
else
{
#if DEBUG
return $"No DocString defined ({key}).";
#else
return "No DocString defined.";
#endif
}
}

public string GetDocStringForFieldOrProperty(string key)
{
// Will lumping fields and properties into the same search come back to bite us? Yes!
// It's a problem for future someone to take care of. You'll have to make the server
// send over a bool that toggles if we should look for a field or a property.
if (_docStrings.TryGetValue($"F:{key}", out string? fieldDoc))
{
return fieldDoc;
}
if (_docStrings.TryGetValue($"P:{key}", out string? propDoc))
{
return propDoc;
}
else
{
#if DEBUG
return $"No DocString defined ({key}).";
#else
return "No DocString defined.";
#endif
}
}
}
}
3 changes: 3 additions & 0 deletions Robust.Client/ViewVariables/ClientViewVariablesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public override void Initialize()
_netManager.RegisterNetMessage<MsgViewVariablesModifyRemote>();
_netManager.RegisterNetMessage<MsgViewVariablesReqSession>();
_netManager.RegisterNetMessage<MsgViewVariablesReqData>();

LoadDocStrings();
}

public VVPropEditor PropertyFor(Type? type)
Expand Down Expand Up @@ -465,6 +467,7 @@ protected override bool TryGetSession(Guid guid, [NotNullWhen(true)] out ICommon
session = null;
return false;
}

}

[Virtual]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,27 @@ internal interface IClientViewVariablesManagerInternal : IClientViewVariablesMan
/// <seealso cref="ViewVariablesBlobMetadata.Traits" />
/// <seealso cref="Shared.ViewVariables.ViewVariablesManager.TraitIdsFor"/>
ICollection<object> TraitIdsFor(Type type);

/// <summary>
/// Load the DocStrings for use by VV
/// </summary>
/// <remarks>
/// Needs to be called before any call to the GetDocString* functions
/// </remarks>
void LoadDocStrings();

/// <summary>
/// Retrieve a DocString for a type definition matching the given key
/// </summary>
/// <param name="key"></param>
/// <returns>DocString for the type or an error string</returns>
string GetDocStringForType(string key);

/// <summary>
/// Retrieve a DocString for a field or property definition matching the given key
/// </summary>
/// <param name="key"></param>
/// <returns>DocString for the field or property or an error string</returns>
string GetDocStringForFieldOrProperty(string key);
}
}
Loading
Loading