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

Intellisense support for BCL types #413

Merged
merged 3 commits into from
Nov 8, 2023
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
56 changes: 42 additions & 14 deletions AvaloniaVS.Shared/Views/AvaloniaDesigner.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using AvaloniaVS.Services;
using EnvDTE;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
Expand Down Expand Up @@ -465,11 +466,11 @@ private async Task StartProcessAsync()
var assemblyPath = SelectedTarget?.XamlAssembly;
var executablePath = SelectedTarget?.ExecutableAssembly;
var hostAppPath = SelectedTarget?.HostApp;
var isNetFx = SelectedTarget?.IsNetFramework;
var isNetFx = SelectedTarget?.IsNetFramework;

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

try
{
Expand Down Expand Up @@ -518,30 +519,31 @@ private async Task StartProcessAsync()
Log.Logger.Verbose("Finished AvaloniaDesigner.StartProcessAsync()");
}

private void RebuildMetadata(string assemblyPath, string executablePath)
private void RebuildMetadata(string assemblyPath)
{
assemblyPath = assemblyPath ?? SelectedTarget?.XamlAssembly;
executablePath = executablePath ?? SelectedTarget?.ExecutableAssembly;
assemblyPath ??= SelectedTarget?.XamlAssembly;

if (assemblyPath != null && executablePath != null)
if (assemblyPath != null && SelectedTarget?.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 = GetMSBuildProperty("MSBuildProjectDirectory", storage) + "\\"
+ GetMSBuildProperty("IntermediateOutputPath", storage) + "Avalonia\\references";
if (metadata.CompletionMetadata == null || metadata.NeedInvalidation)
{
CreateCompletionMetadataAsync(executablePath, metadata).FireAndForget();
CreateCompletionMetadataAsync(intermediateOutputPath, metadata).FireAndForget();
}
}
}

private static Dictionary<string, Task<Metadata>> _metadataCache;

private static async Task CreateCompletionMetadataAsync(
string executablePath,
string intermediateOutputPath,
XamlBufferMetadata target)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Expand All @@ -554,22 +556,22 @@ private static async Task CreateCompletionMetadataAsync(
dte.Events.BuildEvents.OnBuildBegin += (s, e) => _metadataCache.Clear();
}

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

try
{
var sw = Stopwatch.StartNew();

Task<Metadata> metadataLoad;

if (!_metadataCache.TryGetValue(executablePath, out metadataLoad))
if (!_metadataCache.TryGetValue(intermediateOutputPath, out metadataLoad))
{
metadataLoad = Task.Run(() =>
{
var metadataReader = new MetadataReader(new DnlibMetadataProvider());
return metadataReader.GetForTargetAssembly(executablePath);
return metadataReader.GetForTargetAssembly(new AvaloniaCompilationAssemblyProvider(intermediateOutputPath));
});
_metadataCache[executablePath] = metadataLoad;
_metadataCache[intermediateOutputPath] = metadataLoad;
}

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

sw.Stop();

Log.Logger.Verbose("Finished AvaloniaDesigner.CreateCompletionMetadataAsync() took {Time} for {ExecutablePath}", sw.Elapsed, executablePath);
Log.Logger.Verbose("Finished AvaloniaDesigner.CreateCompletionMetadataAsync() took {Time} for {ExecutablePath}", sw.Elapsed, intermediateOutputPath);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -840,5 +842,31 @@ private static async Task<string> ReadAllTextAsync(string fileName)
return await reader.ReadToEndAsync();
}
}

private IVsBuildPropertyStorage GetMSBuildPropertyStorage(Project project)
{
ThreadHelper.ThrowIfNotOnUIThread();
IVsSolution solution = (IVsSolution)ServiceProvider.GlobalProvider.GetService(typeof(SVsSolution));

int hr = solution.GetProjectOfUniqueName(project.FullName, out var hierarchy);
System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(hr);

return hierarchy as IVsBuildPropertyStorage;
}

private string GetMSBuildProperty(string key, IVsBuildPropertyStorage storage)
{
ThreadHelper.ThrowIfNotOnUIThread();
int hr = storage.GetPropertyValue(key, null, (uint)_PersistStorageType.PST_USER_FILE, out var value);
int E_XML_ATTRIBUTE_NOT_FOUND = unchecked((int)0x8004C738);

// ignore this HR, it means that there's no value for this key
if (hr != E_XML_ATTRIBUTE_NOT_FOUND)
{
System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(hr);
}

return value;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.IO;

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

public AvaloniaCompilationAssemblyProvider(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
_path = path;
}

public IEnumerable<string> GetAssemblies()
{
return File.ReadAllText(_path).Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace Avalonia.Ide.CompletionEngine.AssemblyMetadata
{
public interface IAssemblyProvider
{
IEnumerable<string> GetAssemblies();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Avalonia.Ide.CompletionEngine.AssemblyMetadata;
namespace Avalonia.Ide.CompletionEngine.AssemblyMetadata;

public class MetadataReader
{
Expand All @@ -14,26 +9,9 @@ public MetadataReader(IMetadataProvider provider)
_provider = provider;
}

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 Metadata? GetForTargetAssembly(string path)
public Metadata? GetForTargetAssembly(IAssemblyProvider assemblyProvider)
{
if (!File.Exists(path))
return null;

using var session = _provider.GetMetadata(MetadataReader.GetAssemblies(path));
using var session = _provider.GetMetadata(assemblyProvider.GetAssemblies());
return MetadataConverter.ConvertMetadata(session);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,6 @@ public override string ToString()
};

private static Metadata Metadata = new MetadataReader(new DnlibMetadataProvider())
.GetForTargetAssembly(typeof(XamlCompletionTestBase).Assembly.GetModules()[0].FullyQualifiedName);
.GetForTargetAssembly(new FolderAssemblyProvider(typeof(XamlCompletionTestBase).Assembly.GetModules()[0].FullyQualifiedName));
}
}
30 changes: 28 additions & 2 deletions tests/CompletionEngineTests/XamlCompletionTestBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Avalonia.Ide.CompletionEngine;
Expand All @@ -8,6 +10,30 @@

namespace CompletionEngineTests
{
public class FolderAssemblyProvider : IAssemblyProvider
{
private readonly string _path;

public FolderAssemblyProvider(string path)
{
_path = path;
}

public IEnumerable<string> GetAssemblies()
{
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 class XamlCompletionTestBase
{
private static readonly string Prologue = @"<UserControl xmlns='https://github.com/avaloniaui'
Expand All @@ -17,7 +43,7 @@ public class XamlCompletionTestBase


private static Metadata Metadata = new MetadataReader(new DnlibMetadataProvider())
.GetForTargetAssembly(typeof(XamlCompletionTestBase).Assembly.GetModules()[0].FullyQualifiedName);
.GetForTargetAssembly(new FolderAssemblyProvider(typeof(XamlCompletionTestBase).Assembly.GetModules()[0].FullyQualifiedName));

CompletionSet TransformCompletionSet(CompletionSet set)
{
Expand All @@ -31,7 +57,7 @@ CompletionSet TransformCompletionSet(CompletionSet set)
};
}

protected CompletionSet GetCompletionsFor(string xaml, string xamlAfterCursor ="")
protected CompletionSet GetCompletionsFor(string xaml, string xamlAfterCursor = "")
{
xaml = Prologue + xaml;
var engine = new CompletionEngine();
Expand Down