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

VsProjectAssembliesProvider #487

Merged
merged 2 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions AvaloniaVS.Shared/AvaloniaVS.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Services\PreviewerProcess.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\SolutionService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\Throttle.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\VsProjectAssembliesProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\Actions\Base\BaseSuggestedAction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\Actions\MissingAliasSuggestedAction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SuggestedActions\Actions\MissingNamespaceAndAliasSuggestedAction.cs" />
Expand Down
57 changes: 57 additions & 0 deletions AvaloniaVS.Shared/Services/VsProjectAssembliesProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Ide.CompletionEngine.AssemblyMetadata;
using Microsoft.VisualStudio.Shell;
using Serilog;
using VSLangProj;

namespace AvaloniaVS.Shared.Services
{
// VS API requires this code to run on Main Thread, so we have to fetch that ahead.
internal class VsProjectAssembliesProvider : IAssemblyProvider
{
private readonly List<string> _references;

private VsProjectAssembliesProvider(List<string> references)
{
_references = references;
}

public static VsProjectAssembliesProvider TryCreate(EnvDTE.Project project, string xamlPrimaryAssemblyPath)
{
ThreadHelper.ThrowIfNotOnUIThread();

try
{
if (project.Object is VSProject vsProject)
{
var references = new List<string>(200);
references.Add(xamlPrimaryAssemblyPath);

foreach (Reference reference in vsProject.References)
{
if (reference.Type == prjReferenceType.prjReferenceTypeAssembly
&& reference.Path is not null)
{
references.Add(reference.Path);
}
}

// Not sure if it's possible, but never know what surprise VS has.
if (references.Count == 1)
return null;

return new VsProjectAssembliesProvider(references);
}
}
catch (Exception ex)
{
Log.Logger.Error(ex, "VsProjectAssembliesProvider.TryCreate failed with an exception.");
}
return null;
}

public IEnumerable<string> GetAssemblies() => _references;
}
}
50 changes: 33 additions & 17 deletions AvaloniaVS.Shared/Views/AvaloniaDesigner.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
using Avalonia.Ide.CompletionEngine.DnlibMetadataProvider;
using AvaloniaVS.Models;
using AvaloniaVS.Services;
using AvaloniaVS.Shared.Services;
using EnvDTE;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using Serilog;
using VSLangProj;
using Task = System.Threading.Tasks.Task;

namespace AvaloniaVS.Views
Expand Down Expand Up @@ -482,7 +485,7 @@ private async Task StartProcessAsync()

if (assemblyPath != null && executablePath != null && hostAppPath != null && isNetFx != null)
{
RebuildMetadata(assemblyPath);
RebuildMetadata(assemblyPath, executablePath);

try
{
Expand Down Expand Up @@ -531,7 +534,7 @@ private async Task StartProcessAsync()
Log.Logger.Verbose("Finished AvaloniaDesigner.StartProcessAsync()");
}

private string GetIntermediateOutputPath(IVsBuildPropertyStorage storage)
private string GetReferencesFilePath(IVsBuildPropertyStorage storage)
{
// .NET 8 SDK Artifacts output layout
// https://learn.microsoft.com/en-us/dotnet/core/sdk/artifacts-output
Expand All @@ -551,22 +554,37 @@ private string GetIntermediateOutputPath(IVsBuildPropertyStorage storage)
}
}

private void RebuildMetadata(string assemblyPath)
private void RebuildMetadata(string assemblyPath, string executablePath)
{

assemblyPath ??= SelectedTarget?.XamlAssembly;
var project = SelectedTarget?.Project;

if (assemblyPath != null && SelectedTarget?.Project != null)
if (assemblyPath != null && project != null)
{
var buffer = _editor.TextView.TextBuffer;
var metadata = buffer.Properties.GetOrCreateSingletonProperty(
typeof(XamlBufferMetadata),
() => new XamlBufferMetadata());
buffer.Properties["AssemblyName"] = Path.GetFileNameWithoutExtension(assemblyPath);
var storage = GetMSBuildPropertyStorage(SelectedTarget.Project);
string intermediateOutputPath = GetIntermediateOutputPath(storage);

if (metadata.CompletionMetadata == null || metadata.NeedInvalidation)
{
CreateCompletionMetadataAsync(intermediateOutputPath, assemblyPath, metadata).FireAndForget();
Func<IAssemblyProvider> assemblyProviderFunc = () =>
{
if (VsProjectAssembliesProvider.TryCreate(project, assemblyPath) is { } vsProjectAsmProvider)
{
return vsProjectAsmProvider;
}
else if (GetReferencesFilePath(GetMSBuildPropertyStorage(project)) is { } referencesPath
&& File.Exists(referencesPath))
{
return new ReferenceFileAssemblyProvider(referencesPath, assemblyPath);
}
return new DepsJsonFileAssemblyProvider(executablePath, assemblyPath);
};

CreateCompletionMetadataAsync(executablePath, assemblyProviderFunc, metadata).FireAndForget();
}
}
}
Expand All @@ -575,8 +593,8 @@ private void RebuildMetadata(string assemblyPath)
private static readonly MetadataReader _metadataReader = new(new DnlibMetadataProvider());

private static async Task CreateCompletionMetadataAsync(
string intermediateOutputPath,
string xamlAssemblyPath,
string executablePath,
Func<IAssemblyProvider> assemblyProviderFunc,
XamlBufferMetadata target)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Expand All @@ -589,21 +607,19 @@ private static async Task CreateCompletionMetadataAsync(
dte.Events.BuildEvents.OnBuildBegin += (s, e) => _metadataCache.Clear();
}

Log.Logger.Information("Started AvaloniaDesigner.CreateCompletionMetadataAsync() for {ExecutablePath}", intermediateOutputPath);
Log.Logger.Information("Started AvaloniaDesigner.CreateCompletionMetadataAsync() for {ExecutablePath}", executablePath);

try
{
var sw = Stopwatch.StartNew();

Task<Metadata> metadataLoad;

if (!_metadataCache.TryGetValue(intermediateOutputPath, out metadataLoad))
if (!_metadataCache.TryGetValue(executablePath, out metadataLoad))
{
metadataLoad = Task.Run(() =>
{
return _metadataReader.GetForTargetAssembly(new AvaloniaCompilationAssemblyProvider(intermediateOutputPath, xamlAssemblyPath));
});
_metadataCache[intermediateOutputPath] = metadataLoad;
var assemblyProvider = assemblyProviderFunc();
metadataLoad = Task.Run(() => _metadataReader.GetForTargetAssembly(assemblyProvider));
_metadataCache[executablePath] = metadataLoad;
}

target.CompletionMetadata = await metadataLoad;
Expand All @@ -612,7 +628,7 @@ private static async Task CreateCompletionMetadataAsync(

sw.Stop();

Log.Logger.Verbose("Finished AvaloniaDesigner.CreateCompletionMetadataAsync() took {Time} for {ExecutablePath}", sw.Elapsed, intermediateOutputPath);
Log.Logger.Verbose("Finished AvaloniaDesigner.CreateCompletionMetadataAsync() took {Time} for {ExecutablePath}", sw.Elapsed, executablePath);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Avalonia.Ide.CompletionEngine.AssemblyMetadata
{
public class DepsJsonFileAssemblyProvider : IAssemblyProvider
{
private readonly string _path;
private readonly string _xamlPrimaryAssemblyPath;

public DepsJsonFileAssemblyProvider(string executablePath, string xamlPrimaryAssemblyPath)
{
if (string.IsNullOrEmpty(executablePath))
throw new ArgumentNullException(nameof(executablePath));
_path = executablePath;
_xamlPrimaryAssemblyPath = xamlPrimaryAssemblyPath;
}

private static IEnumerable<string> GetAssemblies(string path)
{
if (Path.GetDirectoryName(path) is not { } directory)
{
return Array.Empty<string>();
}

var depsPath = Path.Combine(directory,
Path.GetFileNameWithoutExtension(path) + ".deps.json");
if (File.Exists(depsPath))
return DepsJsonAssemblyListLoader.ParseFile(depsPath);
return Directory.GetFiles(directory).Where(f => f.EndsWith(".dll") || f.EndsWith(".exe"));
}

public IEnumerable<string> GetAssemblies()
{
List<string> result = new List<string>(300);
if (!string.IsNullOrEmpty(_xamlPrimaryAssemblyPath))
{
result.Add(_xamlPrimaryAssemblyPath);
}
try
{
result.AddRange(GetAssemblies(_path));
}
catch (Exception ex) when
(ex is DirectoryNotFoundException || ex is FileNotFoundException)
{
}
catch (Exception ex)
{
throw new IOException($"Failed to read file '{_path}'.", ex);
}
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider)
bool skipRes(string res) => ignoredResExt.Any(r => res.EndsWith(r, StringComparison.OrdinalIgnoreCase));

PreProcessTypes(types, metadata);
var targetAssembly = provider.Assemblies.First();
var targetAssembly = provider.Assemblies.FirstOrDefault() ?? throw new InvalidOperationException("IMetadataReaderSession.Assemblies list is empty.");
foreach (var asm in provider.Assemblies)
{
var aliases = new Dictionary<string, string[]>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

namespace Avalonia.Ide.CompletionEngine.AssemblyMetadata
{
public class AvaloniaCompilationAssemblyProvider : IAssemblyProvider
public class ReferenceFileAssemblyProvider : IAssemblyProvider
{
private readonly string _path;
private readonly string _xamlPrimaryAssemblyPath;

/// <summary>
/// Create a new instance of <see cref="AvaloniaCompilationAssemblyProvider"/>—an implementation of <see cref="IAssemblyProvider"/>.
/// Create a new instance of <see cref="ReferenceFileAssemblyProvider"/>—an implementation of <see cref="IAssemblyProvider"/>.
/// </summary>
/// <param name="path">
/// <para>
Expand All @@ -25,7 +25,7 @@ public class AvaloniaCompilationAssemblyProvider : IAssemblyProvider
/// </param>
/// <param name="xamlPrimaryAssemblyPath">Promary XAML Assembly path</param>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is null or empty</exception>
public AvaloniaCompilationAssemblyProvider(string path, string xamlPrimaryAssemblyPath)
public ReferenceFileAssemblyProvider(string path, string xamlPrimaryAssemblyPath)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
Expand Down