Skip to content

Commit

Permalink
Merge pull request #413 from AvaloniaUI/add-support-for-bcl-types-in-…
Browse files Browse the repository at this point in the history
…intellisense

Intellisense support for BCL types
  • Loading branch information
Takoooooo authored Nov 8, 2023
2 parents 972f10f + 1db1694 commit c240d3d
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 42 deletions.
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

0 comments on commit c240d3d

Please sign in to comment.