Editor/HotCodeEditor/Navigation/XmlDocs.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;

/// <summary>
/// Hands Roslyn the doc comments from a compiler-generated XML doc file (Foo.xml next to Foo.dll).
/// Roslyn's own XmlDocumentationProvider is in an assembly HotCode doesn't reference, and this is all it does.
/// The file is read on first use, not when the reference is created.
/// </summary>
public sealed class XmlDocs : DocumentationProvider
{
	private readonly string _path;
	private readonly string _text;
	private Dictionary<string, string> _members;

	private XmlDocs( string path, string text )
	{
		_path = path;
		_text = text;
	}

	public static XmlDocs FromFile( string path ) => new( path, null );
	public static XmlDocs FromText( string xml ) => new( null, xml );

	protected override string GetDocumentationForSymbol( string documentationMemberID, CultureInfo preferredCulture, CancellationToken cancellationToken = default )
	{
		var members = LazyInitializer.EnsureInitialized( ref _members, Load );
		return members.TryGetValue( documentationMemberID, out var xml ) ? xml : "";
	}

	/// <summary>
	/// Member id ("M:Sandbox.GameObject.Destroy") -> its &lt;member&gt; element as XML.
	/// </summary>
	private Dictionary<string, string> Load()
	{
		var members = new Dictionary<string, string>( StringComparer.Ordinal );

		try
		{
			var doc = _text is not null ? XDocument.Parse( _text ) : XDocument.Load( _path );
			foreach ( var member in doc.Descendants( "member" ) )
			{
				var name = (string)member.Attribute( "name" );
				if ( !string.IsNullOrEmpty( name ) ) members[name] = member.ToString( SaveOptions.DisableFormatting );
			}
		}
		catch ( Exception e ) when ( e is XmlException or IOException or UnauthorizedAccessException )
		{
			// No docs is fine; hints just show without them
		}

		return members;
	}

	public override bool Equals( object obj ) => obj is XmlDocs other && other._path == _path && ReferenceEquals( other._text, _text );
	public override int GetHashCode() => HashCode.Combine( _path, _text );
}