diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 516a2bc..30fb3b1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -53,7 +53,8 @@ jobs:
build-vscode:
- runs-on: ubuntu-latest
+ # macOS so the package includes the Eto.macOS preview host
+ runs-on: macos-latest
steps:
- uses: actions/checkout@v7
@@ -67,6 +68,14 @@ jobs:
${{ env.DotNetVersion }}
8.0.x
+ - name: Setup Xcode
+ uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: latest-stable
+
+ - name: Install macOS workload
+ run: dotnet workload install macos
+
- uses: actions/setup-node@v4
with:
node-version: '20'
diff --git a/Directory.Build.props b/Directory.Build.props
index c6967b4..a39a96d 100755
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -20,12 +20,15 @@
$(MSBuildThisFileDirectory)
$(BasePath)artifacts\
+
+ true
+ false
- 2.7.5
+ 2.12.0
- 2.7.5.0
+ 2.12.0.0
$(SetVersion)
diff --git a/src/Eto.Designer/AppDomainDesignHost.cs b/src/Eto.Designer/AppDomainDesignHost.cs
deleted file mode 100755
index ac7928d..0000000
--- a/src/Eto.Designer/AppDomainDesignHost.cs
+++ /dev/null
@@ -1,394 +0,0 @@
-#if NETFRAMEWORK
-using Eto.Drawing;
-using Eto.Forms;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Runtime.Serialization;
-using System.Security.Permissions;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Eto.Designer
-{
-
- class DomainPlatformThemeHandler : IPlatformTheme
- {
- AppDomainProxy proxy;
- Dictionary colors = new Dictionary();
- public DomainPlatformThemeHandler(AppDomainProxy proxy)
- {
- this.proxy = proxy;
- }
- Color GetColor(string name)
- {
- if (colors.TryGetValue(name, out var value))
- return value;
-
- return colors[name] = Color.FromArgb(proxy.GetThemeColor(name));
- }
-
- public Color ProjectBackground => GetColor("ProjectBackground");
- public Color ProjectForeground => GetColor("ProjectForeground");
- public Color ProjectDialogBackground => GetColor("ProjectDialogBackground");
- public Color ErrorForeground => GetColor("ErrorForeground");
- public Color SummaryBackground => GetColor("SummaryBackground");
- public Color SummaryForeground => GetColor("SummaryForeground");
- public Color DesignerBackground => GetColor("DesignerBackground");
- public Color DesignerPanel => GetColor("DesignerPanel");
- public IEnumerable AllColors { get; } = Enumerable.Empty();
- }
-
- class AppDomainProxy : MarshalByRefObject
- {
- string mainAssembly;
- List references;
- IDesignHost designPanel;
- #pragma warning disable 414
- IDisposable resolver;
- #pragma warning restore 414
-
- public Func GetThemeColor { get; set; }
-
- public void Init(string platformType, string initializeAssembly, string mainAssembly, IEnumerable references)
- {
- this.mainAssembly = mainAssembly;
- this.references = references.ToList();
- if (Platform.Instance == null)
- {
- var refs = new[] { Path.GetDirectoryName(typeof(AppDomainProxy).Assembly.Location), mainAssembly };
- resolver = AssemblyResolver.Register(refs.Union(this.references));
-
- var plat = Activator.CreateInstance(Type.GetType(platformType)) as Platform;
- Platform.Initialize(plat);
- if (!string.IsNullOrEmpty(initializeAssembly))
- {
- plat.LoadAssembly(initializeAssembly);
- }
- plat.Add(typeof(IPlatformTheme), () => new DomainPlatformThemeHandler(this));
- var app = new Application();
- app.Attach();
- app.Terminating += App_Terminating;
-
- app.UnhandledException += App_UnhandledException;
-
- AppDomain.CurrentDomain.DomainUnload += CurrentDomain_DomainUnload;
- }
- }
- public void HookupEvents(AppDomainEventSink eventSink)
- {
- GetThemeColor = eventSink.GetThemeColor;
-
- designPanel = new DesignPanel();
- designPanel.MainAssembly = mainAssembly;
- designPanel.References = references;
-
- designPanel.Error = eventSink.Error;
- designPanel.ControlCreating = eventSink.ControlCreating;
- designPanel.ControlCreated = eventSink.ControlCreated;
- }
-
- void App_UnhandledException(object sender, UnhandledExceptionEventArgs e)
- {
- // error!
- }
-
- void App_Terminating(object sender, CancelEventArgs e)
- {
- e.Cancel = true;
- }
-
- void CurrentDomain_DomainUnload(object sender, EventArgs e)
- {
- //EtoAdapter.Unload();
- }
-
- Control container;
- public object GetContainer()
- {
- // keep ref otherwise it gets gc'd in the app domain
- container = designPanel.GetContainer();
- return container.ToContract();
- }
-
- public void Invalidate()
- {
- designPanel.Invalidate();
- }
-
- public bool SetBuilder(string fileName)
- {
- return designPanel.SetBuilder(fileName);
- }
-
- public void Update(string code)
- {
- designPanel.Update(code);
- }
-
- public string GetCodeFile(string fileName)
- {
- return designPanel.GetCodeFile(fileName);
- }
-
- public void Dispose()
- {
- EtoAdapter.Unload();
- designPanel.Dispose();
- }
-
- public override object InitializeLifetimeService()
- {
- return null;
- }
- }
-
- class AppDomainEventSink : MarshalByRefObject
- {
- public AppDomainDesignHost Host { get; set; }
-
- public void ControlCreated() => Host.ControlCreated?.Invoke();
- public void ControlCreating() => Host.ControlCreating?.Invoke();
- public void Error(DesignError ex) => Host.Error?.Invoke(ex);
- public override object InitializeLifetimeService() => null;
-
- public int GetThemeColor(string name)
- {
- switch (name)
- {
- case "DesignerPanel": return Global.Theme.DesignerPanel.ToArgb();
- case "DesignerBackground": return Global.Theme.DesignerBackground.ToArgb();
- case "ErrorForeground": return Global.Theme.ErrorForeground.ToArgb();
- case "ProjectBackground": return Global.Theme.ProjectBackground.ToArgb();
- case "ProjectDialogBackground": return Global.Theme.ProjectDialogBackground.ToArgb();
- case "ProjectForeground": return Global.Theme.ProjectForeground.ToArgb();
- case "SummaryBackground": return Global.Theme.SummaryBackground.ToArgb();
- case "SummaryForeground": return Global.Theme.SummaryForeground.ToArgb();
- default:
- return Colors.White.ToArgb();
- }
- }
- }
-
- public class AppDomainDesignHost : IDesignHost, IDisposable
- {
- List references;
-
- public IEnumerable References
- {
- get { return references; }
- set { references = value.ToList(); }
- }
-
- public string MainAssembly { get; set; }
-
- public AppDomainDesignHost()
- {
- eventSink = new AppDomainEventSink { Host = this };
- timer = new UITimer { Interval = 1 };
- timer.Elapsed += Timer_Elapsed;
- }
-
- ~AppDomainDesignHost()
- {
- Dispose(false);
- }
-
- void Timer_Elapsed(object sender, EventArgs e)
- {
- timer.Stop();
-
- if (domain != null)
- {
- var oldDomain = domain;
- requiresNewDomain = true;
- var newDomain = SetupAppDomain(true);
- ContainerChanged?.Invoke();
- if (newDomain && !ReferenceEquals(oldDomain, domain))
- Task.Run(() => UnloadDomain(oldDomain));
- }
- }
-
- bool requiresNewDomain;
- AppDomain domain;
- FileSystemWatcher watcher;
- AppDomainProxy proxy;
- AppDomainEventSink eventSink;
- static int domainCount = 0;
- UITimer timer;
-
- IEnumerable GetShadowCopyDirs()
- {
- if (!string.IsNullOrEmpty(MainAssembly))
- yield return Path.GetDirectoryName(MainAssembly);
- if (References != null)
- {
- foreach (var r in References)
- {
- if (!string.IsNullOrEmpty(r))
- yield return Path.GetDirectoryName(r);
- }
- }
- }
-
- bool SetupAppDomain(bool setBuilder)
- {
- if (!requiresNewDomain && domain != null)
- {
- EnsureWatcher();
- return false;
- }
-
-
- requiresNewDomain = false;
-#pragma warning disable 618
- // doesn't work without for some reason, and there's no non-obsolete alternative.
- if (!AppDomain.CurrentDomain.ShadowCopyFiles)
- AppDomain.CurrentDomain.SetShadowCopyFiles();
-#pragma warning restore 618
-
- var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
- var initializeAssembly = Builders.BaseCompiledInterfaceBuilder.InitializeAssembly;
-
- var shadowCopyDirs = string.Join(";", GetShadowCopyDirs().Distinct());
- var setup = new AppDomainSetup
- {
- ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
- PrivateBinPath = $"{baseDir};{shadowCopyDirs}",
-
- ShadowCopyFiles = "true",
- ShadowCopyDirectories = shadowCopyDirs,
-
- LoaderOptimization = LoaderOptimization.MultiDomain,
- //LoaderOptimization = LoaderOptimization.NotSpecified
- };
- if (!string.IsNullOrEmpty(MainAssembly))
- setup.CachePath = Path.Combine(Path.GetDirectoryName(MainAssembly), "Eto.Designer");
-
-
- proxy = null;
- domain = AppDomain.CreateDomain("eto.designer." + domainCount++, null, setup);
- try
- {
- using (AssemblyResolver.Register(baseDir))
- {
- var proxyObject = domain.CreateInstanceFromAndUnwrap(typeof(AppDomainProxy).Assembly.Location, typeof(AppDomainProxy).FullName) as AppDomainProxy;
- proxy = proxyObject as AppDomainProxy;
- if (proxy == null)
- throw new InvalidOperationException($"Could not create proxy for domain\nApplicationBase: {AppDomain.CurrentDomain.BaseDirectory}\nBaseDir: {baseDir}\nShadowCopyDirs: {shadowCopyDirs}");
- }
- proxy.Init(Platform.Instance.GetType().AssemblyQualifiedName, initializeAssembly, MainAssembly, references);
-
- proxy.HookupEvents(eventSink);
-
- if (setBuilder)
- proxy.SetBuilder(fileName);
- if (!string.IsNullOrEmpty(code))
- proxy.Update(code);
- }
- catch (Exception ex)
- {
- UnloadDomain(domain);
- domain = null;
- proxy = null;
- throw new InvalidOperationException($"Could not set up proxy for domain: {ex.GetBaseException().Message}", ex);
- }
-
- EnsureWatcher();
-
- return true;
- }
-
- private void EnsureWatcher()
- {
- if (watcher == null && !string.IsNullOrEmpty(MainAssembly))
- {
- var mainAssemblyPath = Path.GetDirectoryName(MainAssembly);
- if (!Directory.Exists(mainAssemblyPath))
- return;
- watcher = new FileSystemWatcher(mainAssemblyPath, "*.dll");
- watcher.Changed += (sender, e) => Application.Instance.AsyncInvoke(() => timer.Start());
- watcher.Created += (sender, e) => Application.Instance.AsyncInvoke(() => timer.Start());
- watcher.EnableRaisingEvents = true;
- }
- }
-
- public Action ContainerChanged { get; set; }
-
- public Action ControlCreated { get; set; }
- public Action ControlCreating { get; set; }
-
- public Action Error { get; set; }
-
- public Control GetContainer()
- {
- SetupAppDomain(true);
- return EtoAdapter.ToControl(proxy?.GetContainer());
- }
-
- public void Invalidate()
- {
- proxy?.Invalidate();
- }
-
- string fileName;
- public bool SetBuilder(string fileName)
- {
- this.fileName = fileName;
- SetupAppDomain(false);
- return proxy?.SetBuilder(fileName) == true;
- }
-
- string code;
- public void Update(string code)
- {
- SetupAppDomain(true);
-
- this.code = code;
- proxy.Update(code);
- }
-
- public string GetCodeFile(string fileName)
- {
- return proxy.GetCodeFile(fileName);
- }
-
- void UnloadDomain(AppDomain domain)
- {
- try
- {
- if (domain != null && !domain.IsFinalizingForUnload())
- {
- AppDomain.Unload(domain);
- }
- }
- catch
- {
- Debug.WriteLine("Could not unload domain");
- // ignore
- }
- }
-
- public void Dispose()
- {
- Dispose(true);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- proxy?.Dispose();
-
- UnloadDomain(domain);
- domain = null;
- proxy = null;
- }
- }
- }
-}
-#endif
\ No newline at end of file
diff --git a/src/Eto.Designer/AssemblyResolver.cs b/src/Eto.Designer/AssemblyResolver.cs
deleted file mode 100644
index f4ad385..0000000
--- a/src/Eto.Designer/AssemblyResolver.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Eto.Designer
-{
- class AssemblyResolver : IDisposable
- {
- public List Files { get; set; }
-
- public AppDomain AppDomain { get; set; }
-
- public static IDisposable Register(string file, AppDomain domain = null)
- {
- return Register(new string[] { file }, domain);
- }
-
- public static IDisposable Register(IEnumerable files, AppDomain domain = null)
- {
- if (files == null)
- return null;
- if (domain == null)
- domain = AppDomain.CurrentDomain;
- var resolver = new AssemblyResolver { AppDomain = domain, Files = files.ToList() };
- domain.AssemblyResolve += resolver.Resolve;
- return resolver;
- }
-
- public void Dispose()
- {
- AppDomain.AssemblyResolve -= Resolve;
- }
-
- Assembly Resolve(object sender, ResolveEventArgs args)
- {
- var assemblyName = new AssemblyName(args.Name).Name;
- foreach (var path in Files)
- {
- var filePath = Path.GetFileNameWithoutExtension(path);
- if (string.Equals(filePath, assemblyName, StringComparison.OrdinalIgnoreCase) && File.Exists(path))
- return Assembly.LoadFrom(path);
- if (!Directory.Exists(path))
- continue;
- filePath = Path.Combine(path, assemblyName + ".dll");
- if (File.Exists(filePath))
- return Assembly.LoadFrom(filePath);
- }
- return null;
- }
- }
-}
diff --git a/src/Eto.Designer/Builders/BaseCompiledInterfaceBuilder.cs b/src/Eto.Designer/Builders/BaseCompiledInterfaceBuilder.cs
index ad4f9f8..90042ba 100755
--- a/src/Eto.Designer/Builders/BaseCompiledInterfaceBuilder.cs
+++ b/src/Eto.Designer/Builders/BaseCompiledInterfaceBuilder.cs
@@ -15,7 +15,8 @@ public abstract class BaseCompiledInterfaceBuilder : IInterfaceBuilder, IDisposa
{
string output;
- public static string InitializeAssembly { get; set; }
+ /// Eto.dll to compile against, for when it was loaded from memory and has no location.
+ public static string EtoAssemblyPath { get; set; }
~BaseCompiledInterfaceBuilder()
{
@@ -72,7 +73,7 @@ public IBuildToken Create(string text, string mainAssembly, IEnumerable
if (!string.IsNullOrEmpty(mainAssembly))
refs.Insert(0, mainAssembly);
refs.RemoveAll(r => Path.GetFileName(r).ToLowerInvariant() == "eto.dll");
- refs.Add(typeof(Control).Assembly.Location);
+ refs.Add(EtoAssemblyPath ?? typeof(Control).Assembly.Location);
ThreadPool.QueueUserWorkItem(state =>
{
diff --git a/src/Eto.Designer/Builders/JsonInterfaceBuilder.cs b/src/Eto.Designer/Builders/JsonInterfaceBuilder.cs
index 5896848..812a97e 100644
--- a/src/Eto.Designer/Builders/JsonInterfaceBuilder.cs
+++ b/src/Eto.Designer/Builders/JsonInterfaceBuilder.cs
@@ -21,6 +21,8 @@ public IBuildToken Create(string text, string mainAssembly, IEnumerable
var control = JsonReader.Load(stream);
if (control != null)
controlCreated(control);
+ else
+ error(new FormatException("The document did not create a control."));
}
}
catch (Exception ex)
diff --git a/src/Eto.Designer/Builders/RoslynInterfaceBuilder.cs b/src/Eto.Designer/Builders/RoslynInterfaceBuilder.cs
index 793f8ee..ce1a5d6 100755
--- a/src/Eto.Designer/Builders/RoslynInterfaceBuilder.cs
+++ b/src/Eto.Designer/Builders/RoslynInterfaceBuilder.cs
@@ -13,6 +13,16 @@ public abstract class RoslynInterfaceBuilder : BaseCompiledInterfaceBuilder
{
protected virtual IEnumerable GetReferences()
{
+#if NETCOREAPP
+ // no reference assemblies on disk to rely on, so compile against the runtime itself
+ var trusted = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string;
+ if (!string.IsNullOrEmpty(trusted))
+ {
+ foreach (var file in trusted.Split(Path.PathSeparator))
+ yield return file;
+ yield break;
+ }
+#endif
string referenceDir = GetReferenceAssembliesFolder();
if (string.IsNullOrEmpty(referenceDir))
diff --git a/src/Eto.Designer/Builders/XamlInterfaceBuilder.cs b/src/Eto.Designer/Builders/XamlInterfaceBuilder.cs
index 7529262..f766754 100755
--- a/src/Eto.Designer/Builders/XamlInterfaceBuilder.cs
+++ b/src/Eto.Designer/Builders/XamlInterfaceBuilder.cs
@@ -21,6 +21,8 @@ public IBuildToken Create(string text, string mainAssembly, IEnumerable
var control = XamlReader.Load(new StringReader(text), null);
if (control != null)
controlCreated(control);
+ else
+ error(new FormatException("The document did not create a control."));
}
catch (Exception ex)
{
diff --git a/src/Eto.Designer/DesignSurface.cs b/src/Eto.Designer/DesignSurface.cs
index 8d5d2c7..03c8a76 100644
--- a/src/Eto.Designer/DesignSurface.cs
+++ b/src/Eto.Designer/DesignSurface.cs
@@ -24,6 +24,12 @@ public class DesignSurface : Drawable
public event EventHandler InvalidateContent;
+ /// Raised when the user drags the content to a size, or resets it to auto size.
+ public event EventHandler RequestedSizeChanged;
+
+ /// Size the user dragged the content to, or null for its own size.
+ public Size? RequestedSize => _sizeBounds != null ? Size.Round(_sizeBounds.Value) : (Size?)null;
+
public DesignSurface()
{
if (!Platform.Instance.IsGtk) // doesn't work correctly on Gtk2 due to lack of control transparency
@@ -82,6 +88,7 @@ RectangleF SizeBounds
_sizeBounds = value.Size;
_content.Size = Size.Round(value.Size);
Invalidate();
+ RequestedSizeChanged?.Invoke(this, EventArgs.Empty);
}
}
@@ -191,6 +198,7 @@ IEnumerable CreateGrips()
_sizeBounds = null;
_content.Size = _originalContentSize ?? new Size(-1, -1);
InvalidateContent?.Invoke(this, EventArgs.Empty);
+ RequestedSizeChanged?.Invoke(this, EventArgs.Empty);
},
ShouldDraw = location => _sizeBounds != null,
ToolTip = "Click to reset to auto size",
diff --git a/src/Eto.Designer/Eto.Designer.csproj b/src/Eto.Designer/Eto.Designer.csproj
index 25c923b..f7a32ff 100755
--- a/src/Eto.Designer/Eto.Designer.csproj
+++ b/src/Eto.Designer/Eto.Designer.csproj
@@ -4,9 +4,9 @@
Roslyn;RoslynCS;RoslynVB
-
-
-
+
+
+
-
+
diff --git a/src/Eto.DevExtension.PreviewHost.macOS/Eto.DevExtension.PreviewHost.macOS.csproj b/src/Eto.DevExtension.PreviewHost.macOS/Eto.DevExtension.PreviewHost.macOS.csproj
new file mode 100644
index 0000000..2021264
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost.macOS/Eto.DevExtension.PreviewHost.macOS.csproj
@@ -0,0 +1,57 @@
+
+
+
+ Exe
+ net10.0-macos
+ 12.0
+ osx-x64;osx-arm64
+ disable
+ disable
+ Eto.DevExtension.PreviewHost
+ Eto.DevExtension.PreviewHost
+ ca.picoe.eto.previewhost
+ Eto Preview Host
+ en
+
+ dynamic
+ None
+ false
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Eto.DevExtension.PreviewHost.macOS/Info.plist b/src/Eto.DevExtension.PreviewHost.macOS/Info.plist
new file mode 100644
index 0000000..fc7c47c
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost.macOS/Info.plist
@@ -0,0 +1,9 @@
+
+
+
+
+
+ LSUIElement
+
+
+
diff --git a/src/Eto.DevExtension.PreviewHost/Eto.DevExtension.PreviewHost.csproj b/src/Eto.DevExtension.PreviewHost/Eto.DevExtension.PreviewHost.csproj
new file mode 100644
index 0000000..f485ccf
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/Eto.DevExtension.PreviewHost.csproj
@@ -0,0 +1,102 @@
+
+
+ Exe
+
+ net8.0-windows;net8.0
+ disable
+ disable
+ LatestMajor
+
+ false
+
+ Eto.DevExtension.PreviewHost
+ Eto.DevExtension.PreviewHost
+ en
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Eto.DevExtension.PreviewHost/HostTheme.cs b/src/Eto.DevExtension.PreviewHost/HostTheme.cs
new file mode 100644
index 0000000..27fa74f
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/HostTheme.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Linq;
+using Eto.Designer;
+using Eto.Drawing;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ /// Theme colors sent by the editor, so previews match it.
+ class HostTheme : IPlatformTheme
+ {
+ readonly IReadOnlyDictionary colors;
+
+ public HostTheme(IReadOnlyDictionary colors) => this.colors = colors;
+
+ Color Get(string name, Color fallback) =>
+ colors.TryGetValue(name, out var value) && Color.TryParse(value, out var color) ? color : fallback;
+
+ public Color ProjectBackground => Get(nameof(ProjectBackground), SystemColors.Control);
+ public Color ProjectForeground => Get(nameof(ProjectForeground), SystemColors.ControlText);
+ public Color ProjectDialogBackground => Get(nameof(ProjectDialogBackground), SystemColors.Control);
+ public Color ErrorForeground => Get(nameof(ErrorForeground), Colors.Red);
+ public Color SummaryBackground => Get(nameof(SummaryBackground), SystemColors.Control);
+ public Color SummaryForeground => Get(nameof(SummaryForeground), SystemColors.ControlText);
+ public Color DesignerBackground => Get(nameof(DesignerBackground), SystemColors.Control);
+ public Color DesignerPanel => Get(nameof(DesignerPanel), Color.FromRgb(0xF0F0F0));
+ public IEnumerable AllColors => Enumerable.Empty();
+ }
+}
diff --git a/src/Eto.DevExtension.PreviewHost/Platforms/GtkPreviewPlatform.cs b/src/Eto.DevExtension.PreviewHost/Platforms/GtkPreviewPlatform.cs
new file mode 100644
index 0000000..6114e86
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/Platforms/GtkPreviewPlatform.cs
@@ -0,0 +1,86 @@
+#if !WINDOWS && !MACOS
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Eto.Forms;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ class GtkPreviewPlatform : PreviewPlatform
+ {
+ public override string Name => "Gtk";
+ public override string AssemblyName => "Eto.Gtk";
+ public override string PackageId => "eto.platform.gtk";
+
+ public override IEnumerable SharedNames => new[] { "GtkSharp", "GdkSharp", "GLibSharp", "GioSharp", "AtkSharp", "PangoSharp", "CairoSharp" };
+
+ public override IEnumerable GetFrameworks(IEnumerable available) =>
+ ProjectAssemblies.RuntimeFrameworks();
+
+ public override object CreatePlatform() => new Eto.GtkSharp.Platform();
+
+ // otherwise the first text box shows with its text selected, as the offscreen window focuses it
+ public override void Initialized() => Gtk.Settings.Default.SetProperty("gtk-entry-select-on-focus", new GLib.Value(false));
+
+ public override Task CaptureAsync(object control, RenderRequest request)
+ {
+ var etoControl = (Control)control;
+ var content = Eto.Designer.DesignPanel.GetContent(etoControl);
+ if (request.Width != null || request.Height != null)
+ content.Size = new Eto.Drawing.Size(request.Width ?? -1, request.Height ?? -1);
+
+ // an offscreen window, since Wayland won't let a real one be moved out of sight
+ var window = new Gtk.OffscreenWindow();
+ Gtk.Widget widget = null;
+ try
+ {
+ widget = Gtk3Helpers.ToNative(content, true);
+ window.Add(widget);
+ // GTK would give wrapping labels their narrowest width, unlike an Eto form
+ if (request.Width == null)
+ content.Width = (int)Math.Ceiling(content.GetPreferredSize().Width);
+ window.ShowAll();
+ window.Focus = null;
+ for (var i = 0; i < 10 && Gtk.Application.EventsPending(); i++)
+ Gtk.Application.RunIteration(false);
+
+ var width = widget.AllocatedWidth;
+ var height = widget.AllocatedHeight;
+ if (width <= 0 || height <= 0)
+ return Task.FromResult(RenderResult.Empty(width, height));
+
+ var scale = request.Scale > 0 ? request.Scale : 1;
+ using (var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, (int)Math.Ceiling(width * scale), (int)Math.Ceiling(height * scale)))
+ {
+ using (var context = new Cairo.Context(surface))
+ {
+ context.Scale(scale, scale);
+ widget.Draw(context);
+ }
+ surface.Flush();
+ // CairoSharp can only write PNGs to a file
+ var file = Path.GetTempFileName();
+ try
+ {
+ surface.WriteToPng(file);
+ return Task.FromResult(RenderResult.Png(File.ReadAllBytes(file), width, height));
+ }
+ finally
+ {
+ File.Delete(file);
+ }
+ }
+ }
+ finally
+ {
+ if (widget?.Parent == window)
+ window.Remove(widget);
+ window.Destroy();
+ etoControl.Dispose();
+ }
+ }
+ }
+}
+#endif
diff --git a/src/Eto.DevExtension.PreviewHost/Platforms/MacPreviewPlatform.cs b/src/Eto.DevExtension.PreviewHost/Platforms/MacPreviewPlatform.cs
new file mode 100644
index 0000000..fbe554e
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/Platforms/MacPreviewPlatform.cs
@@ -0,0 +1,90 @@
+#if !WINDOWS
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Eto.Forms;
+#if MACOS
+using AppKit;
+using Foundation;
+using CoreGraphics;
+#else
+using MonoMac.AppKit;
+using MonoMac.Foundation;
+using MonoMac.CoreGraphics;
+#endif
+
+namespace Eto.DevExtension.PreviewHost
+{
+ /// Eto.macOS in the app bundle host, or Eto.Mac64 when run with dotnet.
+ class MacPreviewPlatform : PreviewPlatform
+ {
+#if MACOS
+ public override string Name => "macOS";
+ public override string AssemblyName => "Eto.macOS";
+ public override string PackageId => "eto.platform.macos";
+
+ // folders are versioned, e.g. net10.0-macos26.0
+ public override IEnumerable GetFrameworks(IEnumerable available) =>
+ Majors().SelectMany(major => available
+ .Where(r => r.StartsWith($"net{major}.0-macos", StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(r => r, StringComparer.OrdinalIgnoreCase));
+#else
+ public override string Name => "Mac64";
+ public override string AssemblyName => "Eto.Mac64";
+ public override string PackageId => "eto.platform.mac64";
+ public override IEnumerable CompanionNames => new[] { "MonoMac" };
+
+ public override IEnumerable GetFrameworks(IEnumerable available) =>
+ ProjectAssemblies.RuntimeFrameworks();
+#endif
+
+ public override object CreatePlatform() => new Eto.Mac.Platform();
+
+ // no dock icon or menu bar
+ public override void Initialized() => NSApplication.SharedApplication.ActivationPolicy = NSApplicationActivationPolicy.Accessory;
+
+ public override Task CaptureAsync(object control, RenderRequest request)
+ {
+ var form = OffscreenForm.Create((Control)control, request, out var content);
+ try
+ {
+ form.Show();
+ Application.Instance.RunIteration();
+
+ #if MACOS
+ var view = MacOSHelpers.ToNative(content);
+#else
+ var view = MonoMac64Helpers.ToNative(content);
+#endif
+ var width = (int)Math.Ceiling(view.Frame.Width);
+ var height = (int)Math.Ceiling(view.Frame.Height);
+ if (width <= 0 || height <= 0)
+ return Task.FromResult(RenderResult.Empty(width, height));
+
+ // sized by hand, since the offscreen window has no screen to take a backing scale from
+ var scale = request.Scale > 0 ? request.Scale : 1;
+ var bounds = view.Bounds;
+ var rep = view.BitmapImageRepForCachingDisplayInRect(bounds);
+ if (Math.Abs(rep.PixelsWide - width * scale) > 1)
+ rep = new NSBitmapImageRep(IntPtr.Zero, (int)Math.Ceiling(width * scale), (int)Math.Ceiling(height * scale), 8, 4, true, false, NSColorSpace.DeviceRGB, 0, 32) { Size = bounds.Size };
+ view.CacheDisplay(bounds, rep);
+
+ var data = rep.RepresentationUsingTypeProperties(NSBitmapImageFileType.Png, new NSDictionary());
+ using (var input = data.AsStream())
+ using (var stream = new MemoryStream())
+ {
+ input.CopyTo(stream);
+ return Task.FromResult(RenderResult.Png(stream.ToArray(), width, height));
+ }
+ }
+ finally
+ {
+ form.Close();
+ ((Control)control).Dispose();
+ }
+ }
+ }
+}
+#endif
diff --git a/src/Eto.DevExtension.PreviewHost/Platforms/PreviewPlatform.cs b/src/Eto.DevExtension.PreviewHost/Platforms/PreviewPlatform.cs
new file mode 100644
index 0000000..e1a29f5
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/Platforms/PreviewPlatform.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ ///
+ /// The Eto platform a preview is drawn with. Pick one with .
+ ///
+ ///
+ /// The name members are read before Eto is loaded, so only the methods taking or returning objects may touch Eto types.
+ ///
+ abstract class PreviewPlatform
+ {
+ /// Name passed with --platform.
+ public abstract string Name { get; }
+
+ /// Eto platform assembly, e.g. Eto.Wpf.
+ public abstract string AssemblyName { get; }
+
+ /// NuGet package id holding .
+ public abstract string PackageId { get; }
+
+ /// Assemblies that ship beside in its package and must match it.
+ public virtual IEnumerable CompanionNames => Enumerable.Empty();
+
+ /// Other libraries the platform needs, taken from the project when it has them.
+ public virtual IEnumerable SharedNames => Enumerable.Empty();
+
+ /// Package lib folders to look in, best first.
+ public abstract IEnumerable GetFrameworks(IEnumerable available);
+
+ /// Creates the Eto.Platform.
+ public abstract object CreatePlatform();
+
+ /// Called once the application is running.
+ public virtual void Initialized() { }
+
+ /// Draws the control to a PNG. is an Eto.Forms.Control, and is disposed after.
+ public abstract Task CaptureAsync(object control, RenderRequest request);
+
+ // newest first, but never newer than the runtime we're on
+ protected static IEnumerable Majors()
+ {
+ for (var major = Environment.Version.Major; major >= 5; major--)
+ yield return major;
+ }
+
+ public static PreviewPlatform Get(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ name = Default;
+ switch (name.ToLowerInvariant())
+ {
+#if WINDOWS
+ case "wpf":
+ return new WpfPreviewPlatform();
+ case "winforms":
+ return new WinFormsPreviewPlatform();
+#elif MACOS
+ case "macos":
+ return new MacPreviewPlatform();
+#else
+ case "gtk":
+ return new GtkPreviewPlatform();
+ case "mac64":
+ return new MacPreviewPlatform();
+#endif
+ default:
+ throw new NotSupportedException($"The {name} platform isn't supported by this preview host");
+ }
+ }
+
+#if WINDOWS
+ static string Default => "Wpf";
+#elif MACOS
+ static string Default => "macOS";
+#else
+ static string Default => OperatingSystem.IsMacOS() ? "Mac64" : "Gtk";
+#endif
+ }
+}
diff --git a/src/Eto.DevExtension.PreviewHost/Platforms/WinFormsPreviewPlatform.cs b/src/Eto.DevExtension.PreviewHost/Platforms/WinFormsPreviewPlatform.cs
new file mode 100644
index 0000000..0754835
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/Platforms/WinFormsPreviewPlatform.cs
@@ -0,0 +1,60 @@
+#if WINDOWS
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Eto.Forms;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ class WinFormsPreviewPlatform : PreviewPlatform
+ {
+ public override string Name => "WinForms";
+ public override string AssemblyName => "Eto.WinForms";
+ public override string PackageId => "eto.platform.windows";
+
+ public override IEnumerable GetFrameworks(IEnumerable available)
+ {
+ foreach (var major in Majors())
+ {
+ yield return $"net{major}.0-windows7.0";
+ yield return $"net{major}.0-windows";
+ }
+ yield return "netcoreapp3.1";
+ }
+
+ public override object CreatePlatform() => new Eto.WinForms.Platform();
+
+ public override Task CaptureAsync(object control, RenderRequest request)
+ {
+ var form = OffscreenForm.Create((Control)control, request, out var content);
+ try
+ {
+ form.Show();
+ System.Windows.Forms.Application.DoEvents();
+
+ var native = WinFormsHelpers.ToNative(content);
+ var width = native.Width;
+ var height = native.Height;
+ if (width <= 0 || height <= 0)
+ return Task.FromResult(RenderResult.Empty(width, height));
+
+ // WinForms can only draw at the screen's own scale, so report the size in logical pixels
+ var dpiScale = native.DeviceDpi / 96.0;
+ using (var bitmap = new System.Drawing.Bitmap(width, height))
+ using (var stream = new MemoryStream())
+ {
+ native.DrawToBitmap(bitmap, new System.Drawing.Rectangle(0, 0, width, height));
+ bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
+ return Task.FromResult(RenderResult.Png(stream.ToArray(), (int)Math.Round(width / dpiScale), (int)Math.Round(height / dpiScale)));
+ }
+ }
+ finally
+ {
+ form.Close();
+ ((Control)control).Dispose();
+ }
+ }
+ }
+}
+#endif
diff --git a/src/Eto.DevExtension.PreviewHost/Platforms/WpfPreviewPlatform.cs b/src/Eto.DevExtension.PreviewHost/Platforms/WpfPreviewPlatform.cs
new file mode 100644
index 0000000..ecb6be8
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/Platforms/WpfPreviewPlatform.cs
@@ -0,0 +1,74 @@
+#if WINDOWS
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Threading;
+using Eto.Forms;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ class WpfPreviewPlatform : PreviewPlatform
+ {
+ public override string Name => "Wpf";
+ public override string AssemblyName => "Eto.Wpf";
+ public override string PackageId => "eto.platform.wpf";
+
+ public override IEnumerable GetFrameworks(IEnumerable available)
+ {
+ foreach (var major in Majors())
+ {
+ yield return $"net{major}.0-windows7.0";
+ yield return $"net{major}.0-windows";
+ }
+ yield return "netcoreapp3.1";
+ }
+
+ public override object CreatePlatform() => new Eto.Wpf.Platform();
+
+ // the offscreen forms come and go, and must not end the process
+ public override void Initialized() => System.Windows.Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+ public override async Task CaptureAsync(object control, RenderRequest request)
+ {
+ var form = OffscreenForm.Create((Control)control, request, out var content);
+ try
+ {
+ form.Show();
+ await Dispatcher.Yield(DispatcherPriority.ContextIdle);
+
+ var element = WpfHelpers.ToNative(content);
+ element.UpdateLayout();
+ var width = (int)Math.Ceiling(element.ActualWidth);
+ var height = (int)Math.Ceiling(element.ActualHeight);
+ if (width <= 0 || height <= 0)
+ return RenderResult.Empty(width, height);
+
+ var scale = request.Scale > 0 ? request.Scale : 1;
+ var bitmap = new RenderTargetBitmap((int)Math.Ceiling(width * scale), (int)Math.Ceiling(height * scale), 96 * scale, 96 * scale, PixelFormats.Pbgra32);
+ // through a brush, since rendering the element directly includes its offset in the window
+ var visual = new DrawingVisual();
+ using (var context = visual.RenderOpen())
+ context.DrawRectangle(new VisualBrush(element), null, new Rect(0, 0, width, height));
+ bitmap.Render(visual);
+
+ var encoder = new PngBitmapEncoder();
+ encoder.Frames.Add(BitmapFrame.Create(bitmap));
+ using (var stream = new MemoryStream())
+ {
+ encoder.Save(stream);
+ return RenderResult.Png(stream.ToArray(), width, height);
+ }
+ }
+ finally
+ {
+ form.Close();
+ ((Control)control).Dispose();
+ }
+ }
+ }
+}
+#endif
diff --git a/src/Eto.DevExtension.PreviewHost/PreviewRenderer.cs b/src/Eto.DevExtension.PreviewHost/PreviewRenderer.cs
new file mode 100644
index 0000000..526579a
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/PreviewRenderer.cs
@@ -0,0 +1,118 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Eto.Designer;
+using Eto.Forms;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ class RenderRequest
+ {
+ public string FileName;
+ public string Text;
+ public int? Width;
+ public int? Height;
+ public double Scale = 1;
+ }
+
+ class RenderResult
+ {
+ public string Image { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public RenderError Error { get; set; }
+ public bool? RestartRequired { get; set; }
+
+ public static RenderResult Png(byte[] png, int width, int height) =>
+ new RenderResult { Image = Convert.ToBase64String(png), Width = width, Height = height };
+
+ /// For a control with nothing to draw.
+ public static RenderResult Empty(int width, int height) =>
+ new RenderResult { Width = Math.Max(width, 0), Height = Math.Max(height, 0) };
+ }
+
+ class RenderError
+ {
+ public string Message { get; set; }
+ public string Details { get; set; }
+ }
+
+ /// Builds a designer file into a control and draws it to a PNG. Use from the UI thread only.
+ class PreviewRenderer
+ {
+ readonly PreviewPlatform platform;
+ string builderFile;
+ IInterfaceBuilder builder;
+ IBuildToken token;
+
+ public PreviewRenderer(PreviewPlatform platform) => this.platform = platform;
+
+ public Task RenderAsync(RenderRequest request)
+ {
+ var completion = new TaskCompletionSource();
+ try
+ {
+ if (!string.Equals(builderFile, request.FileName, StringComparison.OrdinalIgnoreCase))
+ {
+ builder = BuilderInfo.Find(request.FileName)?.CreateBuilder();
+ builderFile = request.FileName;
+ }
+ if (builder == null)
+ return Task.FromResult(Error(new NotSupportedException($"No designer for {Path.GetFileName(request.FileName)}")));
+
+ token?.Cancel();
+ token = builder.Create(
+ request.Text,
+ ProjectAssemblies.MainAssembly,
+ ProjectAssemblies.Paths,
+ control => Capture(control, request).ContinueWith(r => completion.TrySetResult(r.IsFaulted ? Error(r.Exception) : r.Result)),
+ ex => completion.TrySetResult(Error(ex)));
+ }
+ catch (Exception ex)
+ {
+ completion.TrySetResult(Error(ex));
+ }
+ return completion.Task;
+ }
+
+ Task Capture(Control control, RenderRequest request)
+ {
+ try
+ {
+ return platform.CaptureAsync(control, request);
+ }
+ catch (Exception ex)
+ {
+ return Task.FromException(ex);
+ }
+ }
+
+ static RenderResult Error(Exception ex)
+ {
+ ex = (ex as AggregateException)?.Flatten().InnerException ?? ex;
+ var root = ex.GetBaseException();
+ return new RenderResult { Error = new RenderError { Message = root.Message, Details = ex.ToString() } };
+ }
+ }
+
+ static class OffscreenForm
+ {
+ /// A form out of sight holding the control, shown for real so load events fire and templates apply like in the app.
+ public static Form Create(Control control, RenderRequest request, out Control content)
+ {
+ content = DesignPanel.GetContent(control);
+ if (request.Width != null || request.Height != null)
+ content.Size = new Eto.Drawing.Size(request.Width ?? -1, request.Height ?? -1);
+
+ return new Form
+ {
+ Content = content,
+ WindowStyle = WindowStyle.None,
+ ShowInTaskbar = false,
+ ShowActivated = false,
+ Resizable = false,
+ Location = new Eto.Drawing.Point(-32000, -32000)
+ };
+ }
+ }
+}
diff --git a/src/Eto.DevExtension.PreviewHost/PreviewServer.cs b/src/Eto.DevExtension.PreviewHost/PreviewServer.cs
new file mode 100644
index 0000000..8ea4ed3
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/PreviewServer.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Nodes;
+using System.Threading;
+using System.Threading.Tasks;
+using Eto.DevExtension.LanguageServer;
+using Eto.DevExtension.LanguageServer.Lsp;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ ///
+ /// Handles the preview protocol. Eto only starts on the first render, once it's known which project
+ /// (and so which Eto) this process is for.
+ ///
+ class PreviewServer
+ {
+ readonly LspConnection connection;
+ readonly PreviewPlatform platform;
+ readonly ManualResetEventSlim startUI = new ManualResetEventSlim();
+ readonly TaskCompletionSource uiReady = new TaskCompletionSource();
+ Dictionary theme = new Dictionary();
+ bool configured;
+ object renderer;
+
+ public PreviewServer(LspConnection connection, PreviewPlatform platform)
+ {
+ this.connection = connection;
+ this.platform = platform;
+ connection.OnRequest("initialize", Initialize);
+ connection.OnRequest("shutdown", _ => null);
+ connection.OnNotification("initialized", _ => { });
+ connection.OnNotification("exit", _ => Environment.Exit(0));
+ connection.OnRequest("preview/render", Render);
+ }
+
+ object Initialize(JsonNode parameters)
+ {
+ if (parameters?["theme"] is JsonObject colors)
+ theme = colors.Where(r => r.Value != null).ToDictionary(r => r.Key, r => r.Value.GetValue(), StringComparer.OrdinalIgnoreCase);
+ return new { capabilities = new { }, serverInfo = new { name = "Eto Preview Host" } };
+ }
+
+ object Render(JsonNode parameters)
+ {
+ var request = new RenderRequest
+ {
+ FileName = parameters?["fileName"]?.GetValue(),
+ Text = parameters?["text"]?.GetValue() ?? string.Empty,
+ Width = parameters?["width"]?.GetValue(),
+ Height = parameters?["height"]?.GetValue(),
+ Scale = parameters?["scale"]?.GetValue() ?? 1
+ };
+ var assemblies = (parameters?["assemblies"] as JsonArray)?.Select(r => r?.GetValue()).Where(r => !string.IsNullOrEmpty(r)).ToList()
+ ?? ProjectAssemblyLocator.Find(request.FileName, connection.Log);
+
+ if (!configured)
+ {
+ ProjectAssemblies.Configure(platform, assemblies, request.FileName, connection.Log);
+ configured = true;
+ startUI.Set();
+ uiReady.Task.Wait();
+ }
+ else if (ProjectAssemblies.GetKey(assemblies) != ProjectAssemblies.Key)
+ return new RenderResult { RestartRequired = true };
+
+ return RenderOnUI(request);
+ }
+
+ /// Runs the UI on the calling thread, which must be the process's main thread.
+ public void RunUI()
+ {
+ startUI.Wait();
+ StartEto();
+ }
+
+ // kept apart so no Eto type is touched before the project's Eto is chosen
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ void StartEto()
+ {
+ var etoPlatform = (Eto.Platform)platform.CreatePlatform();
+ etoPlatform.Add(() => new HostTheme(theme));
+ Eto.Designer.Builders.BaseCompiledInterfaceBuilder.EtoAssemblyPath = ProjectAssemblies.EtoFile;
+
+ var app = new Eto.Forms.Application(etoPlatform);
+ app.Initialized += (sender, e) =>
+ {
+ platform.Initialized();
+ ProjectAssemblies.LoadProject();
+ ProjectAssemblies.WatchForChanges(Restart);
+ renderer = new PreviewRenderer(platform);
+ uiReady.TrySetResult(true);
+ };
+ app.UnhandledException += (sender, e) => connection.Log($"Unhandled exception: {e.ExceptionObject}");
+ app.Run();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ RenderResult RenderOnUI(RenderRequest request)
+ {
+ var result = Eto.Forms.Application.Instance.Invoke(() => ((PreviewRenderer)renderer).RenderAsync(request));
+ return result.GetAwaiter().GetResult();
+ }
+
+ void Restart()
+ {
+ connection.Log("Project assemblies changed, restarting the preview host.");
+ connection.Notify("preview/restart", new { });
+ Environment.Exit(0);
+ }
+ }
+}
diff --git a/src/Eto.DevExtension.PreviewHost/Program.cs b/src/Eto.DevExtension.PreviewHost/Program.cs
new file mode 100644
index 0000000..9caae27
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/Program.cs
@@ -0,0 +1,43 @@
+using System;
+using System.IO;
+using System.Threading;
+using Eto.DevExtension.LanguageServer.Lsp;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ static class Program
+ {
+ /// Pass --platform Wpf|Gtk|Mac64|macOS to pick the Eto platform, otherwise the OS's default is used.
+ [STAThread]
+ static int Main(string[] args)
+ {
+ var index = Array.FindIndex(args, r => r == "--platform");
+ var platform = PreviewPlatform.Get(index >= 0 && index + 1 < args.Length ? args[index + 1] : null);
+
+ var connection = new LspConnection(Console.OpenStandardInput(), Console.OpenStandardOutput());
+
+ // stdout is the protocol channel
+ Console.SetOut(TextWriter.Null);
+
+ var server = new PreviewServer(connection, platform);
+ var reader = new Thread(() =>
+ {
+ try
+ {
+ connection.Run();
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine(ex);
+ }
+ // the editor went away
+ Environment.Exit(0);
+ })
+ { IsBackground = true, Name = "Preview protocol" };
+ reader.Start();
+
+ server.RunUI();
+ return 0;
+ }
+ }
+}
diff --git a/src/Eto.DevExtension.PreviewHost/ProjectAssemblies.cs b/src/Eto.DevExtension.PreviewHost/ProjectAssemblies.cs
new file mode 100644
index 0000000..459343a
--- /dev/null
+++ b/src/Eto.DevExtension.PreviewHost/ProjectAssemblies.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.Loader;
+using Eto.DevExtension.LanguageServer;
+
+namespace Eto.DevExtension.PreviewHost
+{
+ ///
+ /// Resolves the project's assemblies and a matching Eto + Eto platform for this process.
+ /// Call once, before anything touches an Eto type.
+ ///
+ ///
+ /// A process only ever serves one project, since assemblies can't be swapped out once loaded.
+ ///
+ static class ProjectAssemblies
+ {
+ // Eto and the libraries it shares with the project, which must all come from the same place
+ static readonly string[] SerializationNames = { "Eto.Serialization.Xaml", "Eto.Serialization.Json" };
+ static readonly string[] SharedNames = { "Portable.Xaml", "Newtonsoft.Json" };
+
+ static readonly Dictionary namedFiles = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ static readonly ConcurrentDictionary loaded = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ static readonly List probeDirectories = new List();
+ static readonly List watchedFiles = new List();
+ static readonly List watchers = new List();
+ static System.Threading.Timer changeTimer;
+ static readonly object loadLock = new object();
+ static AssemblyDependencyResolver dependencyResolver;
+ static Action log;
+ static PreviewPlatform platform;
+
+#if MACOS
+ // Contents/Resources/eto in the app bundle, beside Contents/MonoBundle
+ static readonly string FallbackDirectory = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "Resources", "eto"));
+#else
+ static readonly string FallbackDirectory = Path.Combine(AppContext.BaseDirectory, "eto");
+#endif
+
+ /// Project assembly files, its own first.
+ public static IReadOnlyList Paths { get; private set; } = Array.Empty();
+
+ /// Identifies the assembly set, so a render for a different project can be refused.
+ public static string Key { get; private set; }
+
+ public static string MainAssembly => Paths.Count > 0 ? Paths[0] : null;
+
+ /// File Eto.dll was loaded from, since a copy loaded from memory has no location.
+ public static string EtoFile => namedFiles.TryGetValue("Eto", out var file) ? file : null;
+
+ public static string GetKey(IEnumerable paths) => string.Join("|", paths ?? Enumerable.Empty());
+
+ public static void Configure(PreviewPlatform previewPlatform, IList paths, string documentPath, Action logger)
+ {
+ platform = previewPlatform;
+ log = logger;
+ Paths = (paths ?? new List()).Where(File.Exists).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
+ Key = GetKey(paths);
+
+ foreach (var directory in Paths.Select(Path.GetDirectoryName).Distinct(StringComparer.OrdinalIgnoreCase))
+ probeDirectories.Add(directory);
+
+ if (MainAssembly != null)
+ {
+ try
+ {
+ dependencyResolver = new AssemblyDependencyResolver(MainAssembly);
+ }
+ catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
+ {
+ // no deps.json, as with .NET Framework projects
+ }
+ }
+
+ ChooseEto(documentPath);
+
+ AssemblyLoadContext.Default.Resolving += Resolve;
+ AssemblyLoadContext.Default.ResolvingUnmanagedDll += ResolveUnmanaged;
+ }
+
+ /// Loads the project's own assemblies, so the designer can find their types.
+ public static void LoadProject()
+ {
+ foreach (var path in Paths)
+ {
+ try
+ {
+ Load(path);
+ }
+ catch (Exception ex) when (ex is IOException || ex is BadImageFormatException || ex is UnauthorizedAccessException)
+ {
+ log?.Invoke($"Could not load {path}: {ex.Message}");
+ }
+ }
+ }
+
+ /// Calls once any project file loaded so far is rebuilt.
+ public static void WatchForChanges(Action changed)
+ {
+ List files;
+ lock (loadLock)
+ files = watchedFiles.ToList();
+
+ var timer = changeTimer = new System.Threading.Timer(_ => changed());
+ foreach (var group in files.GroupBy(Path.GetDirectoryName, StringComparer.OrdinalIgnoreCase))
+ {
+ var names = new HashSet(group.Select(Path.GetFileName), StringComparer.OrdinalIgnoreCase);
+ var watcher = new FileSystemWatcher(group.Key) { NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size };
+ FileSystemEventHandler handler = (sender, e) =>
+ {
+ // a build writes several files, so wait for it to settle
+ if (names.Contains(e.Name))
+ timer.Change(1000, System.Threading.Timeout.Infinite);
+ };
+ watcher.Changed += handler;
+ watcher.Created += handler;
+ watcher.EnableRaisingEvents = true;
+ watchers.Add(watcher);
+ }
+ }
+
+ static void ChooseEto(string documentPath)
+ {
+ // the project's own copy, so its custom controls bind to the Eto they were built against
+ var etoDirectory = probeDirectories.FirstOrDefault(r => File.Exists(Path.Combine(r, "Eto.dll")))
+ ?? EtoAssemblyLocator.Find(documentPath, log);
+ var eto = etoDirectory != null ? Path.Combine(etoDirectory, "Eto.dll") : null;
+ var version = eto != null && File.Exists(eto) ? GetPackageVersion(eto) : null;
+ var platformFile = version != null ? FindPlatform(version, etoDirectory) : null;
+
+ if (platformFile == null)
+ {
+ if (eto != null)
+ log?.Invoke($"No {platform.AssemblyName} {version} found for {eto}, so using Eto bundled with the extension.");
+ foreach (var name in new[] { "Eto", platform.AssemblyName }.Concat(platform.CompanionNames).Concat(SerializationNames).Concat(SharedNames).Concat(platform.SharedNames))
+ SetFallback(name);
+ return;
+ }
+
+ // the located copy may be built for a newer runtime than this one
+ namedFiles["Eto"] = FindPackage("Eto", version, RuntimeFrameworks(), "eto.forms") ?? eto;
+ namedFiles[platform.AssemblyName] = platformFile;
+ foreach (var name in platform.CompanionNames)
+ {
+ var file = FindBeside(name, Path.GetDirectoryName(platformFile));
+ if (file != null)
+ namedFiles[name] = file;
+ else
+ SetFallback(name);
+ }
+ foreach (var name in SerializationNames)
+ {
+ var file = FindBeside(name, etoDirectory) ?? FindPackage(name, version, RuntimeFrameworks());
+ if (file != null)
+ namedFiles[name] = file;
+ else
+ SetFallback(name);
+ }
+ foreach (var name in SharedNames.Concat(platform.SharedNames))
+ {
+ var file = probeDirectories.Select(r => FindBeside(name, r)).FirstOrDefault(r => r != null);
+ if (file != null)
+ namedFiles[name] = file;
+ else
+ SetFallback(name);
+ }
+ log?.Invoke($"Using Eto {version} from {namedFiles["Eto"]} with {platformFile}");
+ }
+
+ static void SetFallback(string name)
+ {
+ var file = Path.Combine(FallbackDirectory, name + ".dll");
+ if (File.Exists(file))
+ namedFiles[name] = file;
+ }
+
+ static string FindBeside(string name, string directory)
+ {
+ if (directory == null)
+ return null;
+ var file = Path.Combine(directory, name + ".dll");
+ return File.Exists(file) ? file : null;
+ }
+
+ /// Eto platform of the same version as Eto, preferring a build for this runtime from the package over one next to the project.
+ static string FindPlatform(string version, string etoDirectory)
+ {
+ var name = platform.AssemblyName;
+ var fromPackage = FindPackage(name, version, platform.GetFrameworks, platform.PackageId);
+ if (fromPackage != null)
+ return fromPackage;
+
+ var beside = FindBeside(name, etoDirectory) ?? probeDirectories.Select(r => FindBeside(name, r)).FirstOrDefault(r => r != null);
+ return beside != null && GetPackageVersion(beside) == version ? beside : null;
+ }
+
+ // newest first, but never newer than the runtime we're on
+ public static IEnumerable RuntimeFrameworks()
+ {
+ for (var major = Environment.Version.Major; major >= 5; major--)
+ yield return $"net{major}.0";
+ yield return "netstandard2.0";
+ }
+
+ static string FindPackage(string name, string version, IEnumerable frameworks, string packageId = null) =>
+ FindPackage(name, version, _ => frameworks, packageId);
+
+ /// Given the lib folders in the package, returns the ones to use, best first.
+ static string FindPackage(string name, string version, Func, IEnumerable> getFrameworks, string packageId = null)
+ {
+ packageId = (packageId ?? name).ToLowerInvariant();
+ foreach (var folder in GetPackageFolders())
+ {
+ var lib = Path.Combine(folder, packageId, version.ToLowerInvariant(), "lib");
+ if (!Directory.Exists(lib))
+ continue;
+ var available = Directory.EnumerateDirectories(lib).Select(Path.GetFileName).ToList();
+ foreach (var framework in getFrameworks(available))
+ {
+ var file = Path.Combine(lib, framework, name + ".dll");
+ if (File.Exists(file))
+ return file;
+ }
+ }
+ return null;
+ }
+
+ static IEnumerable GetPackageFolders()
+ {
+ var configured = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
+ if (!string.IsNullOrEmpty(configured))
+ yield return configured;
+ yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
+ }
+
+ static string GetPackageVersion(string file)
+ {
+ var version = FileVersionInfo.GetVersionInfo(file).ProductVersion;
+ if (string.IsNullOrEmpty(version))
+ return null;
+ var plus = version.IndexOf('+');
+ return plus >= 0 ? version.Substring(0, plus) : version;
+ }
+
+ static Assembly Resolve(AssemblyLoadContext context, AssemblyName name)
+ {
+ if (loaded.TryGetValue(name.Name, out var assembly))
+ return assembly;
+
+ var file = FindFile(name);
+ if (file == null)
+ return null;
+ try
+ {
+ return Load(file);
+ }
+ catch (Exception ex) when (ex is IOException || ex is BadImageFormatException || ex is UnauthorizedAccessException)
+ {
+ log?.Invoke($"Could not load {file}: {ex.Message}");
+ return null;
+ }
+ }
+
+ static string FindFile(AssemblyName name)
+ {
+ if (namedFiles.TryGetValue(name.Name, out var named))
+ return named;
+
+ var own = Paths.FirstOrDefault(r => string.Equals(Path.GetFileNameWithoutExtension(r), name.Name, StringComparison.OrdinalIgnoreCase));
+ if (own != null)
+ return own;
+
+ var resolved = dependencyResolver?.ResolveAssemblyToPath(name);
+ if (resolved != null && File.Exists(resolved))
+ return resolved;
+
+ foreach (var directory in probeDirectories)
+ {
+ foreach (var extension in new[] { ".dll", ".exe" })
+ {
+ var candidate = Path.Combine(directory, name.Name + extension);
+ if (File.Exists(candidate))
+ return candidate;
+ }
+ }
+
+ var fallback = Path.Combine(FallbackDirectory, name.Name + ".dll");
+ return File.Exists(fallback) ? fallback : null;
+ }
+
+ static Assembly Load(string file)
+ {
+ lock (loadLock)
+ {
+ var name = AssemblyName.GetAssemblyName(file).Name;
+ if (loaded.TryGetValue(name, out var existing))
+ return existing;
+
+ Assembly assembly;
+ if (IsImmutable(file))
+ assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
+ else
+ {
+ // read into memory so the project can still be rebuilt while the preview is open
+ using (var stream = new MemoryStream(File.ReadAllBytes(file)))
+ assembly = AssemblyLoadContext.Default.LoadFromStream(stream);
+ watchedFiles.Add(file);
+ }
+ loaded[name] = assembly;
+ return assembly;
+ }
+ }
+
+ // package and bundled copies never change, so they can be loaded in place
+ static bool IsImmutable(string file) =>
+ file.StartsWith(FallbackDirectory, StringComparison.OrdinalIgnoreCase)
+ || GetPackageFolders().Any(r => file.StartsWith(r, StringComparison.OrdinalIgnoreCase));
+
+ static IntPtr ResolveUnmanaged(Assembly assembly, string name)
+ {
+ var path = dependencyResolver?.ResolveUnmanagedDllToPath(name);
+ return path != null ? NativeLibrary.Load(path) : IntPtr.Zero;
+ }
+ }
+}
diff --git a/src/Eto.DevExtension.Shared/Eto.DevExtension.Shared.csproj b/src/Eto.DevExtension.Shared/Eto.DevExtension.Shared.csproj
index e566754..4dbfdf6 100755
--- a/src/Eto.DevExtension.Shared/Eto.DevExtension.Shared.csproj
+++ b/src/Eto.DevExtension.Shared/Eto.DevExtension.Shared.csproj
@@ -4,8 +4,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/Eto.DevExtension.VSCode.sln b/src/Eto.DevExtension.VSCode.sln
index fd6b9b5..ae8d9ce 100644
--- a/src/Eto.DevExtension.VSCode.sln
+++ b/src/Eto.DevExtension.VSCode.sln
@@ -5,6 +5,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eto.DevExtension.LanguageSe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eto.Designer", "Eto.Designer\Eto.Designer.csproj", "{7FAA1841-0869-474C-9D89-87E1B4BAB190}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eto.DevExtension.PreviewHost", "Eto.DevExtension.PreviewHost\Eto.DevExtension.PreviewHost.csproj", "{5AF057D2-FE4A-428D-A704-BCC02BFF24C3}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{F132CF00-BA1F-41E0-BFD9-0F691C47C5FE}"
ProjectSection(SolutionItems) = preProject
..\Directory.Build.props = ..\Directory.Build.props
@@ -24,6 +26,10 @@ Global
{7FAA1841-0869-474C-9D89-87E1B4BAB190}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FAA1841-0869-474C-9D89-87E1B4BAB190}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FAA1841-0869-474C-9D89-87E1B4BAB190}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AF057D2-FE4A-428D-A704-BCC02BFF24C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AF057D2-FE4A-428D-A704-BCC02BFF24C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AF057D2-FE4A-428D-A704-BCC02BFF24C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AF057D2-FE4A-428D-A704-BCC02BFF24C3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
diff --git a/src/Eto.DevExtension.VSCode/.gitignore b/src/Eto.DevExtension.VSCode/.gitignore
index 2115027..a101196 100644
--- a/src/Eto.DevExtension.VSCode/.gitignore
+++ b/src/Eto.DevExtension.VSCode/.gitignore
@@ -1,4 +1,5 @@
node_modules/
out/
server/
+preview/
*.vsix
diff --git a/src/Eto.DevExtension.VSCode/.vscodeignore b/src/Eto.DevExtension.VSCode/.vscodeignore
index c668793..1333960 100644
--- a/src/Eto.DevExtension.VSCode/.vscodeignore
+++ b/src/Eto.DevExtension.VSCode/.vscodeignore
@@ -5,3 +5,5 @@ src/**
tsconfig.json
.gitignore
server/**/*.pdb
+preview/**/*.pdb
+scripts/**
diff --git a/src/Eto.DevExtension.VSCode/README.md b/src/Eto.DevExtension.VSCode/README.md
index 5caab6e..9bb7b91 100644
--- a/src/Eto.DevExtension.VSCode/README.md
+++ b/src/Eto.DevExtension.VSCode/README.md
@@ -1,9 +1,14 @@
# Eto.Forms Designer for VS Code
-Autocompletion and hover documentation for [Eto.Forms](https://github.com/picoe/Eto) designer files.
+Autocompletion, hover documentation and a live preview for [Eto.Forms](https://github.com/picoe/Eto) designer files.
## Features
+- A live preview of `.xeto`, `.jeto` and `.eto.cs` files beside the editor. Run
+ **Eto: Open Preview to the Side**, or use the preview button in the editor title bar. Your own
+ controls show up once the project is built, and the preview redraws after each build. Drag the
+ corner handle to try the form at other sizes; click the size label to go back to its own size
+
- Completion of control names, properties, events and property values in `.xeto` (xaml) and `.jeto` (json) files
- Completion of your own controls from the project and the projects it references, once built. In
`.xeto` files picking one adds the `xmlns` it needs; in `.jeto` files they complete as
@@ -19,6 +24,13 @@ the copy bundled with the extension is used.
## Requirements
The [.NET 8 runtime](https://dotnet.microsoft.com/download) or newer, used to run the language server.
+On Windows the preview also needs the .NET Desktop Runtime 8 or newer, and on Linux it needs GTK 3.
+
+The preview runs your project's code in a separate process, drawing with the Eto.Forms version the
+project uses. Pick the platform to draw with from the drop down at the top of the preview: WPF or
+WinForms on Windows, macOS or Mac64 on macOS, and Gtk anywhere GTK 3 is installed (on macOS, from
+Homebrew or MacPorts). **Auto** uses the first of these that a project in the solution references,
+otherwise WPF, macOS or Gtk.
## Settings
@@ -26,6 +38,7 @@ The [.NET 8 runtime](https://dotnet.microsoft.com/download) or newer, used to ru
| --- | --- |
| `eto.dotnetPath` | Path to the `dotnet` executable. Uses `dotnet` from `PATH` when empty. |
| `eto.languageServer.path` | Path to `Eto.DevExtension.LanguageServer.dll`. Uses the bundled copy when empty. |
+| `eto.previewHost.path` | Path to `Eto.DevExtension.PreviewHost.dll`. Uses the bundled copy when empty. Not used for Eto.macOS previews. |
| `eto.etoAssemblyPath` | Folder containing the `Eto.dll` to complete against. Detected from the project when empty. |
| `eto.trace.server` | Logs the traffic between VS Code and the language server. |
@@ -36,9 +49,13 @@ a different project, run **Eto: Restart Language Server**.
```sh
npm install
-npm run build:server # publishes the .NET language server into ./server
+npm run build:server # publishes the .NET language server into ./server and the preview hosts into ./preview
npm run compile
```
+The Eto.macOS preview host is an app bundle, so it's only built when packaging on a Mac with the .NET
+macOS workload. Without it, Mac previews use Eto.Mac64. If the workload rejects your Xcode version, pass
+`npm run build:server -- -p:ValidateXcodeVersion=false`.
+
Press F5 to launch an extension development host. `npm run package` produces a `.vsix` in
`artifacts/vscode`.
diff --git a/src/Eto.DevExtension.VSCode/package-lock.json b/src/Eto.DevExtension.VSCode/package-lock.json
index 6fb6a12..d1b2942 100644
--- a/src/Eto.DevExtension.VSCode/package-lock.json
+++ b/src/Eto.DevExtension.VSCode/package-lock.json
@@ -1,14 +1,15 @@
{
"name": "eto-forms-designer",
- "version": "2.7.5",
+ "version": "2.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "eto-forms-designer",
- "version": "2.7.5",
+ "version": "2.12.0",
"license": "MIT",
"dependencies": {
+ "vscode-jsonrpc": "^8.2.0",
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
diff --git a/src/Eto.DevExtension.VSCode/package.json b/src/Eto.DevExtension.VSCode/package.json
index 80a32be..ba5ce8e 100644
--- a/src/Eto.DevExtension.VSCode/package.json
+++ b/src/Eto.DevExtension.VSCode/package.json
@@ -1,8 +1,8 @@
{
"name": "eto-forms-designer",
"displayName": "Eto.Forms Designer",
- "description": "Autocompletion and hover documentation for Eto.Forms .xeto and .jeto designer files.",
- "version": "2.7.5",
+ "description": "Autocompletion, hover documentation and a live preview for Eto.Forms .xeto and .jeto designer files.",
+ "version": "2.12.0",
"publisher": "picoe",
"license": "MIT",
"repository": {
@@ -76,6 +76,11 @@
"default": "",
"description": "Path to Eto.DevExtension.LanguageServer.dll. Uses the copy bundled with the extension when empty."
},
+ "eto.previewHost.path": {
+ "type": "string",
+ "default": "",
+ "description": "Path to Eto.DevExtension.PreviewHost.dll. Uses the copy bundled with the extension when empty. Not used for Eto.macOS previews."
+ },
"eto.etoAssemblyPath": {
"type": "string",
"default": "",
@@ -97,17 +102,38 @@
{
"command": "eto.restartLanguageServer",
"title": "Eto: Restart Language Server"
+ },
+ {
+ "command": "eto.openPreview",
+ "title": "Eto: Open Preview to the Side",
+ "icon": "$(open-preview)"
}
- ]
+ ],
+ "menus": {
+ "editor/title": [
+ {
+ "command": "eto.openPreview",
+ "group": "navigation",
+ "when": "resourceFilename =~ /\\.(xeto|jeto|eto\\.cs|eto\\.vb)$/i"
+ }
+ ],
+ "commandPalette": [
+ {
+ "command": "eto.openPreview",
+ "when": "resourceFilename =~ /\\.(xeto|jeto|eto\\.cs|eto\\.vb)$/i"
+ }
+ ]
+ }
},
"scripts": {
"vscode:prepublish": "npm run build:server && npm run compile",
- "build:server": "dotnet publish ../Eto.DevExtension.LanguageServer/Eto.DevExtension.LanguageServer.csproj -c Release -o server",
+ "build:server": "node scripts/build-server.js",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"package": "node -e \"require('fs').mkdirSync('../../artifacts/vscode',{recursive:true})\" && vsce package --out ../../artifacts/vscode/"
},
"dependencies": {
+ "vscode-jsonrpc": "^8.2.0",
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
diff --git a/src/Eto.DevExtension.VSCode/scripts/build-server.js b/src/Eto.DevExtension.VSCode/scripts/build-server.js
new file mode 100644
index 0000000..18488f9
--- /dev/null
+++ b/src/Eto.DevExtension.VSCode/scripts/build-server.js
@@ -0,0 +1,39 @@
+// Publishes the language server to server/ and the preview hosts to preview/ for packaging.
+// Extra arguments go to the Eto.macOS host build, e.g. `npm run build:server -- -p:ValidateXcodeVersion=false`.
+const { execFileSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const root = path.resolve(__dirname, '..');
+const src = path.resolve(root, '..');
+const artifacts = path.resolve(src, '..', 'artifacts');
+const host = path.join(src, 'Eto.DevExtension.PreviewHost', 'Eto.DevExtension.PreviewHost.csproj');
+const preview = path.join(root, 'preview');
+
+function dotnet(...args) {
+ execFileSync('dotnet', args, { stdio: 'inherit', cwd: root });
+}
+
+fs.rmSync(preview, { recursive: true, force: true });
+
+dotnet('publish', path.join(src, 'Eto.DevExtension.LanguageServer', 'Eto.DevExtension.LanguageServer.csproj'), '-c', 'Release', '-o', path.join(root, 'server'));
+dotnet('publish', host, '-c', 'Release', '-f', 'net8.0-windows', '-o', path.join(preview, 'win'));
+dotnet('publish', host, '-c', 'Release', '-f', 'net8.0', '-o', path.join(preview, 'net'));
+
+if (process.platform === 'darwin') {
+ try {
+ dotnet('build', path.join(src, 'Eto.DevExtension.PreviewHost.macOS', 'Eto.DevExtension.PreviewHost.macOS.csproj'), '-c', 'Release', ...process.argv.slice(2));
+ const app = 'Eto Preview Host.app';
+ fs.mkdirSync(path.join(preview, 'macos'), { recursive: true });
+ // ditto keeps the bundle's signature intact
+ execFileSync('ditto', [path.join(artifacts, 'Eto.DevExtension.PreviewHost.macOS', 'Release', 'net10.0-macos', app), path.join(preview, 'macos', app)], { stdio: 'inherit' });
+ } catch (e) {
+ // CI packages must always ship it
+ if (process.env.CI)
+ throw e;
+ // usually a missing macOS workload or mismatched Xcode, which shouldn't stop the rest from packaging
+ console.warn('Could not build the Eto.macOS preview host, so Mac previews will use Eto.Mac64.');
+ }
+} else {
+ console.log('Skipping the Eto.macOS preview host, which only builds on macOS. Mac previews will use Eto.Mac64.');
+}
diff --git a/src/Eto.DevExtension.VSCode/src/extension.ts b/src/Eto.DevExtension.VSCode/src/extension.ts
index a0ae5c3..d402929 100644
--- a/src/Eto.DevExtension.VSCode/src/extension.ts
+++ b/src/Eto.DevExtension.VSCode/src/extension.ts
@@ -7,18 +7,26 @@ import {
ServerOptions,
TransportKind
} from 'vscode-languageclient/node';
+import { AUTO, HostLauncher } from './hostLaunch';
+import { isPreviewable, PlatformPicker, PreviewPanel } from './preview';
+import { PreviewHost } from './previewHost';
const SERVER_DLL = 'Eto.DevExtension.LanguageServer.dll';
+const PLATFORM_KEY = 'eto.preview.platform';
let client: LanguageClient | undefined;
let output: vscode.OutputChannel;
+let previewHost: PreviewHost | undefined;
+let platformPicker: PlatformPicker | undefined;
export async function activate(context: vscode.ExtensionContext): Promise {
output = vscode.window.createOutputChannel('Eto.Forms Designer');
context.subscriptions.push(output);
context.subscriptions.push(
- vscode.commands.registerCommand('eto.restartLanguageServer', () => restart(context))
+ vscode.commands.registerCommand('eto.restartLanguageServer', () => restart(context)),
+ vscode.commands.registerCommand('eto.openPreview', (uri?: vscode.Uri) => openPreview(context, uri)),
+ { dispose: () => previewHost?.dispose() }
);
// the server pins one Eto version per session, so these need a fresh process
@@ -93,12 +101,40 @@ async function start(context: vscode.ExtensionContext): Promise {
}
}
+async function openPreview(context: vscode.ExtensionContext, uri?: vscode.Uri): Promise {
+ const document = uri
+ ? await vscode.workspace.openTextDocument(uri)
+ : vscode.window.activeTextEditor?.document;
+ if (!document || !isPreviewable(document)) {
+ vscode.window.showInformationMessage('Open a .xeto, .jeto or .eto.cs file to preview it.');
+ return;
+ }
+
+ if (!previewHost || !platformPicker) {
+ const launcher = new HostLauncher(context.extensionPath, output);
+ const picker: PlatformPicker = platformPicker = {
+ getPlatforms: () => launcher.getPlatforms(),
+ get: () => context.workspaceState.get(PLATFORM_KEY, AUTO),
+ set: id => context.workspaceState.update(PLATFORM_KEY, id)
+ };
+ previewHost = new PreviewHost(fileName => launcher.resolve(fileName, picker.get()), output);
+ }
+ PreviewPanel.show(previewHost, platformPicker, document);
+}
+
function resolveServerPath(context: vscode.ExtensionContext, configured?: string): string | undefined {
+ return resolvePath(context, configured, 'server', SERVER_DLL, path.join('Eto.DevExtension.LanguageServer', 'Debug', 'net8.0'));
+}
+
+/**
+ * @param folder Where the dll is bundled in the extension.
+ * @param buildFolder Where a debug build puts it under artifacts, for running out of the source tree.
+ */
+function resolvePath(context: vscode.ExtensionContext, configured: string | undefined, folder: string, dll: string, buildFolder: string): string | undefined {
const candidates = [
configured?.trim(),
- path.join(context.extensionPath, 'server', SERVER_DLL),
- // running out of the source tree during development
- path.join(context.extensionPath, '..', '..', 'artifacts', 'Eto.DevExtension.LanguageServer', 'Debug', 'net8.0', SERVER_DLL)
+ path.join(context.extensionPath, folder, dll),
+ path.join(context.extensionPath, '..', '..', 'artifacts', buildFolder, dll)
];
for (const candidate of candidates) {
diff --git a/src/Eto.DevExtension.VSCode/src/hostLaunch.ts b/src/Eto.DevExtension.VSCode/src/hostLaunch.ts
new file mode 100644
index 0000000..c20b20d
--- /dev/null
+++ b/src/Eto.DevExtension.VSCode/src/hostLaunch.ts
@@ -0,0 +1,216 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import * as vscode from 'vscode';
+
+const HOST_DLL = 'Eto.DevExtension.PreviewHost.dll';
+const MAC_APP = 'Eto Preview Host.app';
+const MAC_EXECUTABLE = path.join(MAC_APP, 'Contents', 'MacOS', 'Eto.DevExtension.PreviewHost');
+
+// scanning projects on every keystroke is too slow, but an edited project should still be noticed
+const CACHE_MS = 10000;
+
+export const AUTO = 'auto';
+
+/** A platform the preview can draw with. */
+export interface PlatformOption {
+ id: string;
+ label: string;
+}
+
+/** How to start a preview host process. */
+export interface HostLaunch {
+ command: string;
+ args: string[];
+ env?: NodeJS.ProcessEnv;
+ /** Label of the platform it draws with. */
+ platform: string;
+ /** Shown when the process can't be started. */
+ requirement: string;
+}
+
+interface Platform extends PlatformOption {
+ /** Matches a project that uses this platform, from a package or project reference. */
+ reference: RegExp;
+ launch(): HostLaunch | undefined;
+}
+
+/**
+ * Picks the preview host for a document. The platforms offered depend on the OS and what's installed,
+ * and Auto picks the first one the solution references, otherwise the OS's usual one.
+ */
+export class HostLauncher {
+ private readonly autoCache = new Map();
+ private lastMessage: string | undefined;
+
+ constructor(private readonly extensionPath: string, private readonly output: vscode.OutputChannel) { }
+
+ /** Platforms available on this machine, best first. */
+ getPlatforms(): PlatformOption[] {
+ return this.getAvailable().map(r => ({ id: r.id, label: r.label }));
+ }
+
+ /** @param choice A platform id, or {@link AUTO}. */
+ async resolve(fileName: string, choice: string): Promise {
+ const platforms = this.getAvailable();
+ const platform = platforms.find(r => r.id === choice) ?? await this.detect(fileName, platforms);
+ return platform?.launch();
+ }
+
+ private getAvailable(): Platform[] {
+ const config = vscode.workspace.getConfiguration('eto');
+ const dotnet = config.get('dotnetPath')?.trim() || 'dotnet';
+ const configured = config.get('previewHost.path')?.trim();
+ const netHost = () => this.find(configured, path.join('preview', 'net', HOST_DLL), path.join('Eto.DevExtension.PreviewHost', 'Debug', 'net8.0', HOST_DLL));
+ const platforms: Platform[] = [];
+
+ if (process.platform === 'win32') {
+ const windowsHost = () => this.find(configured, path.join('preview', 'win', HOST_DLL), path.join('Eto.DevExtension.PreviewHost', 'Debug', 'net8.0-windows', HOST_DLL));
+ const requirement = 'The preview needs the .NET 8 Desktop Runtime (or newer).';
+ platforms.push(
+ { id: 'Wpf', label: 'WPF', reference: /Eto\.Platform\.Wpf\b|Eto\.Wpf\.csproj/i, launch: () => withDotnet(dotnet, windowsHost(), 'Wpf', 'WPF', requirement) },
+ { id: 'WinForms', label: 'WinForms', reference: /Eto\.Platform\.Windows\b|Eto\.WinForms\.csproj/i, launch: () => withDotnet(dotnet, windowsHost(), 'WinForms', 'WinForms', requirement) }
+ );
+ }
+ if (process.platform === 'darwin') {
+ const app = this.find(undefined, path.join('preview', 'macos', MAC_EXECUTABLE), path.join('Eto.DevExtension.PreviewHost.macOS', 'Debug', 'net10.0-macos', MAC_EXECUTABLE));
+ if (app) {
+ platforms.push({
+ id: 'macOS', label: 'macOS', reference: /Eto\.Platform\.macOS\b|Eto\.macOS\.csproj/i,
+ launch: () => ({ command: app, args: [], platform: 'macOS', requirement: 'The Eto.macOS preview host could not be started.' })
+ });
+ }
+ platforms.push({
+ id: 'Mac64', label: 'Mac64', reference: /Eto\.Platform\.Mac64\b|Eto\.Mac64\.csproj/i,
+ launch: () => withDotnet(dotnet, netHost(), 'Mac64', 'Mac64', 'The preview needs the .NET 8 runtime (or newer).')
+ });
+ }
+
+ const gtk = findGtk();
+ if (gtk) {
+ platforms.push({
+ id: 'Gtk', label: 'Gtk', reference: /Eto\.Platform\.Gtk\b|Eto\.Gtk\.csproj/i,
+ launch: () => {
+ const launch = withDotnet(dotnet, netHost(), 'Gtk', 'Gtk', 'The preview needs the .NET 8 runtime (or newer) and GTK 3.');
+ return launch && { ...launch, env: gtk.env };
+ }
+ });
+ }
+ return platforms;
+ }
+
+ private async detect(fileName: string, platforms: Platform[]): Promise {
+ const key = path.dirname(fileName);
+ const cached = this.autoCache.get(key);
+ if (cached && Date.now() - cached.time < CACHE_MS && platforms.some(r => r.id === cached.platform.id)) {
+ return cached.platform;
+ }
+ if (platforms.length === 0) {
+ return undefined;
+ }
+
+ const texts = (findSolutionProjects(fileName) ?? await findWorkspaceProjects()).map(readText);
+ const referenced = platforms.find(platform => texts.some(text => platform.reference.test(text)));
+ const platform = referenced ?? platforms[0];
+ this.autoCache.set(key, { time: Date.now(), platform });
+ this.log(referenced
+ ? `Previewing with ${platform.label}, as the solution uses it.`
+ : `Previewing with ${platform.label}, as the solution doesn't reference an Eto platform available here.`);
+ return platform;
+ }
+
+ /**
+ * @param bundled Where the host is in the packaged extension.
+ * @param build Where a debug build puts it under artifacts, for running out of the source tree.
+ */
+ private find(configured: string | undefined, bundled: string, build: string): string | undefined {
+ const candidates = [
+ configured,
+ path.join(this.extensionPath, bundled),
+ path.join(this.extensionPath, '..', '..', 'artifacts', build)
+ ];
+ for (const candidate of candidates) {
+ if (!candidate) {
+ continue;
+ }
+ const resolved = path.isAbsolute(candidate) ? candidate : path.join(this.extensionPath, candidate);
+ if (fs.existsSync(resolved)) {
+ return resolved;
+ }
+ }
+ return undefined;
+ }
+
+ private log(message: string): void {
+ if (message !== this.lastMessage) {
+ this.lastMessage = message;
+ this.output.appendLine(message);
+ }
+ }
+}
+
+function withDotnet(dotnet: string, dll: string | undefined, id: string, label: string, requirement: string): HostLaunch | undefined {
+ return dll ? { command: dotnet, args: [dll, '--platform', id], platform: label, requirement } : undefined;
+}
+
+/** GTK 3, and the environment the host needs to find it, or undefined when it isn't installed. */
+function findGtk(): { env?: NodeJS.ProcessEnv } | undefined {
+ if (process.platform === 'darwin') {
+ // Homebrew (Apple silicon, then Intel) or MacPorts, none of which are on the default library path
+ const dir = ['/opt/homebrew/lib', '/usr/local/lib', '/opt/local/lib'].find(r => fs.existsSync(path.join(r, 'libgtk-3.0.dylib')));
+ return dir ? { env: { ...process.env, DYLD_FALLBACK_LIBRARY_PATH: joinPath(dir, process.env.DYLD_FALLBACK_LIBRARY_PATH) } } : undefined;
+ }
+ if (process.platform === 'win32') {
+ // where GtkSharp's build installs it, or anywhere on PATH such as MSYS2
+ const installed = process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Gtk', '3.24.24') : undefined;
+ const dir = [installed, ...(process.env.PATH ?? '').split(path.delimiter)].find(r => r && fs.existsSync(path.join(r, 'libgtk-3-0.dll')));
+ return dir ? { env: { ...process.env, PATH: joinPath(dir, process.env.PATH) } } : undefined;
+ }
+ // the desktop's own toolkit, so assume it's there
+ return {};
+}
+
+function joinPath(first: string, rest: string | undefined): string {
+ return rest ? `${first}${path.delimiter}${rest}` : first;
+}
+
+/** Projects in the nearest solution above the file, or undefined when there's none. */
+function findSolutionProjects(fileName: string): string[] | undefined {
+ let dir = path.dirname(fileName);
+ for (; ;) {
+ const solution = readDir(dir).find(r => /\.slnx?$/i.test(r));
+ if (solution) {
+ const file = path.join(dir, solution);
+ const pattern = solution.toLowerCase().endsWith('.slnx')
+ ? / path.resolve(dir, r[1].replace(/\\/g, path.sep)));
+ }
+ const parent = path.dirname(dir);
+ if (parent === dir) {
+ return undefined;
+ }
+ dir = parent;
+ }
+}
+
+async function findWorkspaceProjects(): Promise {
+ const files = await vscode.workspace.findFiles('**/*.{csproj,vbproj,fsproj}', '**/{bin,obj,node_modules}/**', 500);
+ return files.map(r => r.fsPath);
+}
+
+function readDir(dir: string): string[] {
+ try {
+ return fs.readdirSync(dir);
+ } catch {
+ return [];
+ }
+}
+
+function readText(file: string): string {
+ try {
+ return fs.readFileSync(file, 'utf8');
+ } catch {
+ return '';
+ }
+}
diff --git a/src/Eto.DevExtension.VSCode/src/preview.ts b/src/Eto.DevExtension.VSCode/src/preview.ts
new file mode 100644
index 0000000..f89aa94
--- /dev/null
+++ b/src/Eto.DevExtension.VSCode/src/preview.ts
@@ -0,0 +1,287 @@
+import * as path from 'path';
+import * as vscode from 'vscode';
+import { AUTO, PlatformOption } from './hostLaunch';
+import { PreviewHost, RenderResult } from './previewHost';
+
+const REFRESH_DELAY_MS = 500;
+
+/** Designer files the preview can draw. */
+export function isPreviewable(document: vscode.TextDocument): boolean {
+ return document.uri.scheme === 'file' && /\.(xeto|jeto|eto\.cs|eto\.vb)$/i.test(document.fileName);
+}
+
+/** The platform chosen in the preview's drop down. */
+export interface PlatformPicker {
+ getPlatforms(): PlatformOption[];
+ /** A platform id, or {@link AUTO}. */
+ get(): string;
+ set(id: string): Thenable;
+}
+
+/**
+ * A single preview beside the editor that follows the active designer file and redraws as it is edited.
+ */
+export class PreviewPanel implements vscode.Disposable {
+ private static current: PreviewPanel | undefined;
+
+ private readonly panel: vscode.WebviewPanel;
+ private readonly disposables: vscode.Disposable[] = [];
+ private document: vscode.TextDocument | undefined;
+ private size: { width: number; height: number } | undefined;
+ private scale = 1;
+ private ready = false;
+ private rendering = false;
+ private renderPending = false;
+ private timer: NodeJS.Timeout | undefined;
+
+ static show(host: PreviewHost, picker: PlatformPicker, document: vscode.TextDocument): void {
+ if (PreviewPanel.current) {
+ PreviewPanel.current.panel.reveal(vscode.ViewColumn.Beside, true);
+ } else {
+ PreviewPanel.current = new PreviewPanel(host, picker);
+ }
+ PreviewPanel.current.setDocument(document);
+ }
+
+ private constructor(private readonly host: PreviewHost, private readonly picker: PlatformPicker) {
+ this.panel = vscode.window.createWebviewPanel('eto.preview', 'Eto Preview', { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }, {
+ enableScripts: true,
+ localResourceRoots: []
+ });
+ this.panel.webview.html = getHtml();
+
+ this.disposables.push(
+ this.panel.onDidDispose(() => this.dispose()),
+ this.panel.webview.onDidReceiveMessage(message => this.onMessage(message)),
+ vscode.workspace.onDidChangeTextDocument(e => {
+ if (e.document === this.document) {
+ this.schedule();
+ }
+ }),
+ vscode.window.onDidChangeActiveTextEditor(editor => {
+ if (editor && isPreviewable(editor.document) && editor.document !== this.document) {
+ this.setDocument(editor.document);
+ }
+ }),
+ host.onDidRequestRedraw(() => this.render())
+ );
+ }
+
+ dispose(): void {
+ PreviewPanel.current = undefined;
+ clearTimeout(this.timer);
+ this.panel.dispose();
+ for (const disposable of this.disposables.splice(0)) {
+ disposable.dispose();
+ }
+ }
+
+ private setDocument(document: vscode.TextDocument): void {
+ this.document = document;
+ // a size picked for one file rarely suits the next
+ this.size = undefined;
+ this.panel.title = `Preview ${path.basename(document.fileName)}`;
+ this.render();
+ }
+
+ private onMessage(message: { type: string; scale?: number; width?: number; height?: number; id?: string }): void {
+ switch (message.type) {
+ case 'ready': {
+ this.ready = true;
+ this.scale = message.scale || 1;
+ const options = [{ id: AUTO, label: 'Auto' }, ...this.picker.getPlatforms()];
+ const selected = options.some(r => r.id === this.picker.get()) ? this.picker.get() : AUTO;
+ this.panel.webview.postMessage({ type: 'platforms', options, selected });
+ this.render();
+ break;
+ }
+ case 'platform':
+ this.picker.set(message.id || AUTO).then(() => this.render());
+ break;
+ case 'scale':
+ this.scale = message.scale || 1;
+ this.render();
+ break;
+ case 'resize':
+ this.size = { width: Math.round(message.width!), height: Math.round(message.height!) };
+ this.render();
+ break;
+ case 'reset':
+ this.size = undefined;
+ this.render();
+ break;
+ }
+ }
+
+ private schedule(): void {
+ clearTimeout(this.timer);
+ this.timer = setTimeout(() => this.render(), REFRESH_DELAY_MS);
+ }
+
+ private async render(): Promise {
+ if (!this.ready || !this.document) {
+ return;
+ }
+ if (this.rendering) {
+ this.renderPending = true;
+ return;
+ }
+
+ this.rendering = true;
+ try {
+ do {
+ this.renderPending = false;
+ const document: vscode.TextDocument = this.document;
+ const result: RenderResult = await this.host.render({
+ fileName: document.fileName,
+ text: document.getText(),
+ width: this.size?.width,
+ height: this.size?.height,
+ scale: this.scale
+ });
+ if (PreviewPanel.current !== this) {
+ return;
+ }
+ // results for a file the user already moved away from would only flicker
+ if (document === this.document) {
+ this.panel.webview.postMessage(result.error
+ ? { type: 'error', message: result.error.message, details: result.error.details, platform: result.platform }
+ : { type: 'image', image: result.image, width: result.width, height: result.height, sized: !!this.size, platform: result.platform });
+ }
+ } while (this.renderPending);
+ } finally {
+ this.rendering = false;
+ }
+ }
+}
+
+function getHtml(): string {
+ const nonce = [...Array(32)].map(() => Math.floor(Math.random() * 36).toString(36)).join('');
+ return /* html */ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+}
diff --git a/src/Eto.DevExtension.VSCode/src/previewHost.ts b/src/Eto.DevExtension.VSCode/src/previewHost.ts
new file mode 100644
index 0000000..5b0d24f
--- /dev/null
+++ b/src/Eto.DevExtension.VSCode/src/previewHost.ts
@@ -0,0 +1,164 @@
+import { ChildProcess, spawn } from 'child_process';
+import * as vscode from 'vscode';
+import {
+ createMessageConnection,
+ MessageConnection,
+ StreamMessageReader,
+ StreamMessageWriter
+} from 'vscode-jsonrpc/node';
+import { HostLaunch } from './hostLaunch';
+
+// long enough for a first render that compiles code, short enough to recover from a hung control
+const RENDER_TIMEOUT_MS = 30000;
+
+export interface RenderRequest {
+ fileName: string;
+ text: string;
+ width?: number;
+ height?: number;
+ scale: number;
+}
+
+export interface RenderResult {
+ image?: string;
+ width?: number;
+ height?: number;
+ error?: { message: string; details?: string };
+ /** Label of the platform it was drawn with. */
+ platform?: string;
+}
+
+/**
+ * The .NET process that draws designer files to images, so project code runs outside VS Code.
+ * Renders one request at a time, and restarts the process whenever it exits or stops responding.
+ */
+export class PreviewHost implements vscode.Disposable {
+ private process: ChildProcess | undefined;
+ private connection: MessageConnection | undefined;
+ private starting: Promise | undefined;
+ private queue: Promise = Promise.resolve();
+ private launchKey: string | undefined;
+ // hosts that couldn't start, usually for want of a runtime, so they aren't tried again
+ private readonly unavailable = new Set();
+ private readonly restarted = new vscode.EventEmitter();
+
+ /** Fires after the project is rebuilt, so previews can be redrawn. */
+ readonly onDidRequestRedraw = this.restarted.event;
+
+ constructor(
+ private readonly resolveLaunch: (fileName: string) => Promise,
+ private readonly output: vscode.OutputChannel
+ ) { }
+
+ render(request: RenderRequest): Promise {
+ const next = this.queue.then(() => this.renderNow(request));
+ this.queue = next.catch(() => undefined);
+ return next;
+ }
+
+ dispose(): void {
+ this.stop();
+ this.restarted.dispose();
+ }
+
+ private async renderNow(request: RenderRequest): Promise {
+ // a second try covers a host that exited after a rebuild, or that serves another project
+ for (let attempt = 0; attempt < 2; attempt++) {
+ const launch = await this.resolveLaunch(request.fileName);
+ if (!launch) {
+ return error('Could not find the Eto preview host. Set "eto.previewHost.path" to point at it.');
+ }
+ const connection = await this.getConnection(launch);
+ if (!connection) {
+ return error(`${launch.requirement} See the Eto.Forms Designer output for details.`);
+ }
+
+ let timer: NodeJS.Timeout | undefined;
+ try {
+ const timeout = new Promise<'timeout'>(resolve => timer = setTimeout(() => resolve('timeout'), RENDER_TIMEOUT_MS));
+ const result = await Promise.race([
+ connection.sendRequest('preview/render', { ...request, assemblies: null }),
+ timeout
+ ]);
+ if (result === 'timeout') {
+ this.stop();
+ return error('The preview took too long to draw, so it was stopped.');
+ }
+ if (result?.restartRequired) {
+ this.stop();
+ continue;
+ }
+ return { ...result, platform: launch.platform };
+ } catch (e) {
+ this.stop();
+ if (attempt > 0) {
+ return error('The preview host stopped unexpectedly.', `${e}`);
+ }
+ } finally {
+ clearTimeout(timer);
+ }
+ }
+ return error('The preview host could not load the project.');
+ }
+
+ private getConnection(launch: HostLaunch): Promise {
+ const key = JSON.stringify([launch.command, ...launch.args]);
+ if (this.launchKey === key && this.connection && this.process && isRunning(this.process)) {
+ return Promise.resolve(this.connection);
+ }
+ if (this.unavailable.has(key)) {
+ return Promise.resolve(undefined);
+ }
+ this.stop();
+ this.launchKey = key;
+ this.starting ??= this.start(launch, key).finally(() => this.starting = undefined);
+ return this.starting;
+ }
+
+ private async start(launch: HostLaunch, key: string): Promise {
+ try {
+ const child = spawn(launch.command, launch.args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, env: launch.env });
+ const failed = new Promise((_, reject) => child.once('error', reject));
+ child.stderr?.on('data', data => this.output.append(`${data}`));
+
+ const connection = createMessageConnection(new StreamMessageReader(child.stdout!), new StreamMessageWriter(child.stdin!));
+ connection.onNotification('preview/restart', () => {
+ this.output.appendLine('Project rebuilt, restarting the preview host.');
+ this.restarted.fire();
+ });
+ connection.onNotification('window/logMessage', (params: { message: string }) => this.output.appendLine(params.message));
+ connection.listen();
+
+ this.process = child;
+ this.connection = connection;
+ await Promise.race([connection.sendRequest('initialize', { processId: process.pid }), failed]);
+ connection.sendNotification('initialized', {});
+ return connection;
+ } catch (e) {
+ this.unavailable.add(key);
+ this.output.appendLine(`Could not start the Eto preview host using "${launch.command}": ${e}`);
+ this.stop();
+ return undefined;
+ }
+ }
+
+ private stop(): void {
+ const connection = this.connection;
+ const child = this.process;
+ this.connection = undefined;
+ this.process = undefined;
+ this.launchKey = undefined;
+ connection?.dispose();
+ if (child && isRunning(child)) {
+ child.kill();
+ }
+ }
+}
+
+function isRunning(child: ChildProcess): boolean {
+ return child.exitCode === null && child.signalCode === null;
+}
+
+function error(message: string, details?: string): RenderResult {
+ return { error: { message, details: details ?? message } };
+}
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/2022/source.extension.vsixmanifest b/src/Eto.DevExtension.VisualStudio.Windows/2022/source.extension.vsixmanifest
index c1a35d9..124f30f 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/2022/source.extension.vsixmanifest
+++ b/src/Eto.DevExtension.VisualStudio.Windows/2022/source.extension.vsixmanifest
@@ -3,7 +3,7 @@
xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011"
xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
-
+
Eto.Forms Templates and Previewer
Eto.Forms Support for Visual Studio. Eto.Forms is a cross platform GUI framework for desktop and mobile applications in .NET that can target Wpf, WinForms, Direct2D, MonoMac, Xamarin.Mac, Gtk2, and Gtk3 with a single codebase.
https://github.com/picoe/Eto
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EditorFactory.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EditorFactory.cs
index 3c0f488..a031d54 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EditorFactory.cs
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EditorFactory.cs
@@ -212,12 +212,10 @@ public int CreateEditorInstance(
try
{
- var outputFile = GetAssemblyPath(proj);
- var references = GetReferences(proj).ToList();
- //var outputDir = Path.GetDirectoryName(outputFile);
+ var projectKey = proj?.FullName ?? pszMkDocument;
// Create the Document (editor)
- var editor = new EtoPreviewPane(editorPackage, pszMkDocument, textBuffer, outputFile, references, codeEditor);
+ var editor = new EtoPreviewPane(editorPackage, pszMkDocument, textBuffer, projectKey, codeEditor);
ppunkDocView = Marshal.GetIUnknownForObject(editor);
}
catch (Exception ex)
@@ -259,35 +257,6 @@ public IVsProject ToVsProject(EnvDTE.Project project)
return vsProject;
}
- public static IEnumerable GetReferences(EnvDTE.Project project)
- {
- Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread();
- var vsproject = project.Object as VSLangProj.VSProject;
- if (vsproject == null)
- yield break;
- // note: you could also try casting to VsWebSite.VSWebSite
-
- foreach (VSLangProj.Reference reference in vsproject.References)
- {
- if (reference.SourceProject == null)
- {
- //skip framework assemblies
- //if (((dynamic)reference).AutoReferenced)
- // continue;
-
- // This is an assembly reference
- if (reference.Path != null)
- yield return reference.Path;
- }
- else
- {
- // This is a project reference
- var path = GetAssemblyPath(reference.SourceProject);
- if (path != null)
- yield return path;
- }
- }
- }
internal static string GetAssemblyPath(EnvDTE.Project vsProject)
{
Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread();
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoAdapterHandler.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoAdapterHandler.cs
deleted file mode 100755
index c641e9a..0000000
--- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoAdapterHandler.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using System;
-using Eto.Forms;
-using Eto.Designer;
-using Eto.Wpf.Forms;
-using System.Windows;
-using System.AddIn.Pipeline;
-using System.AddIn.Contract;
-using System.Collections.Generic;
-using Eto.Wpf.Forms.Controls;
-using sw = System.Windows;
-using swc = System.Windows.Controls;
-using System.Windows.Threading;
-using Eto;
-using Eto.DevExtension.VisualStudio.Windows.Editor;
-
-[assembly: ExportHandler(typeof(IEtoAdapterHandler), typeof(EtoAdapterHandler))]
-
-namespace Eto.DevExtension.VisualStudio.Windows.Editor
-{
- class ControlContract : MarshalByRefObject
- {
- public IntPtr Handle { get; set; }
- }
-
- class NativeControl : WpfFrameworkElement
- {
- public NativeControl(FrameworkElement control)
- {
- Control = control;
- }
-
- public override Drawing.Color BackgroundColor
- {
- get { return Drawing.Colors.Transparent; }
- set
- {
- }
- }
- }
-
- public class EtoAdapterHandler : IEtoAdapterHandler
- {
- public object ToContract(Control control)
- {
- var content = DesignPanel.GetContent(control);
- var view = content.ToNative(true);
- return FrameworkElementAdapters.ViewToContractAdapter(view);
- }
-
- public Control FromContract(object contract)
- {
- var view = FrameworkElementAdapters.ContractToViewAdapter((INativeHandleContract)contract);
- view.Focusable = false; // otherwise editor loses focus when switching back to its tab.. may be a better way around this.
- return new Control(new NativeControl(view));
- }
-
- public void Unload()
- {
- Dispatcher.CurrentDispatcher?.InvokeShutdown();
- GC.Collect();
- GC.WaitForPendingFinalizers();
- }
- }
-}
-
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs
index 8fa8cf9..4c2bd39 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs
@@ -31,6 +31,7 @@
using System.Windows.Media;
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
using System.Collections.Generic;
+using Eto.DevExtension.VisualStudio.Intellisense;
namespace Eto.DevExtension.VisualStudio.Windows.Editor
{
@@ -48,6 +49,7 @@ public sealed class EtoPreviewPane : Microsoft.VisualStudio.Shell.WindowPane,
EtoAddinPackage package;
PreviewEditorView preview;
PreviewEditorViewSplitter previewSplitter;
+ PreviewHostClient previewHost;
Panel editorControl;
uint dataEventsCookie;
uint linesEventsCookie;
@@ -100,7 +102,8 @@ void RegisterIndependentView(bool subscribe)
/// our initialization functions.
///
/// Our Package instance.
- public EtoPreviewPane(EtoAddinPackage package, string fileName, IVsTextLines textBuffer, string mainAssembly, IEnumerable references, CodeEditorHost codeEditor)
+ /// Identifies the project, so its designers share one preview host.
+ public EtoPreviewPane(EtoAddinPackage package, string fileName, IVsTextLines textBuffer, string projectKey, CodeEditorHost codeEditor)
: base(package)
{
this.editor = codeEditor;
@@ -111,7 +114,11 @@ public EtoPreviewPane(EtoAddinPackage package, string fileName, IVsTextLines tex
editorControl = new Panel();
editorControl.Content = editor.wpfElement.ToEto();
- previewSplitter = new PreviewEditorViewSplitter(editorControl, mainAssembly, references, () => textBuffer?.GetText());
+ var host = previewHost = PreviewHostClient.Acquire(projectKey);
+ host.ProjectChanged += PreviewHost_ProjectChanged;
+ var designHost = new RemoteDesignPanel(async request => await host.RenderAsync(request, await ProjectAssemblyPaths.GetAsync(fileName)));
+
+ previewSplitter = new PreviewEditorViewSplitter(editorControl, designHost, () => textBuffer?.GetText());
previewSplitter.GotFocus += (sender, e) =>
{
WpfTextView?.VisualElement?.Focus();
@@ -257,7 +264,14 @@ protected override void Dispose(bool disposing)
if (disposing)
{
- RegisterIndependentView(false);
+ RegisterIndependentView(false);
+
+ if (previewHost != null)
+ {
+ previewHost.ProjectChanged -= PreviewHost_ProjectChanged;
+ previewHost.Release();
+ previewHost = null;
+ }
//previewSplitter?.Dispose();
//previewSplitter = null;
@@ -306,6 +320,8 @@ void ViewCode()
#endregion
+ void PreviewHost_ProjectChanged(object sender, EventArgs e) => preview?.Update();
+
void IVsTextBufferDataEvents.OnFileChanged(uint grfChange, uint dwFileAttrs)
{
preview.Update();
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs
new file mode 100644
index 0000000..1c0b93f
--- /dev/null
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs
@@ -0,0 +1,228 @@
+using Eto.Designer;
+using Eto.Drawing;
+using Newtonsoft.Json.Linq;
+using StreamJsonRpc;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Eto.DevExtension.VisualStudio.Windows.Editor
+{
+ ///
+ /// The .NET preview host that draws designer files, shared by the open designers of one project.
+ ///
+ ///
+ /// Previews are drawn in a separate process so the project's own code never loads into Visual Studio.
+ ///
+ sealed class PreviewHostClient
+ {
+ const string HostDll = "Eto.DevExtension.PreviewHost.dll";
+ // long enough for a first render that compiles code, short enough to recover from a hung control
+ static readonly TimeSpan RenderTimeout = TimeSpan.FromSeconds(30);
+
+ static readonly Dictionary hosts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ static bool unavailable;
+
+ readonly string key;
+ readonly SemaphoreSlim requestLock = new SemaphoreSlim(1, 1);
+ int references;
+ Process process;
+ JsonRpc rpc;
+
+ /// Raised on the UI thread after the project is rebuilt, so previews can be redrawn.
+ public event EventHandler ProjectChanged;
+
+ PreviewHostClient(string key) => this.key = key;
+
+ bool IsRunning => rpc != null && process != null && !process.HasExited;
+
+ /// Identifies the project, so its designers share one host.
+ public static PreviewHostClient Acquire(string key)
+ {
+ key = key ?? string.Empty;
+ lock (hosts)
+ {
+ if (!hosts.TryGetValue(key, out var host))
+ hosts[key] = host = new PreviewHostClient(key);
+ host.references++;
+ return host;
+ }
+ }
+
+ public void Release()
+ {
+ lock (hosts)
+ {
+ if (--references > 0)
+ return;
+ hosts.Remove(key);
+ }
+ Stop();
+ }
+
+ /// Project assembly files, the project's own first, or null to let the host find them.
+ public async Task RenderAsync(PreviewRenderRequest request, IList assemblies)
+ {
+ await requestLock.WaitAsync();
+ try
+ {
+ // a second try covers a host that exited after a rebuild, or that serves an older build
+ for (var attempt = 0; attempt < 2; attempt++)
+ {
+ var server = await GetServerAsync();
+ if (server == null)
+ return Error("The preview needs the .NET 8 Desktop Runtime (or newer) to be installed.", null);
+
+ try
+ {
+ using (var timeout = new CancellationTokenSource(RenderTimeout))
+ {
+ var result = await server.InvokeWithParameterObjectAsync(
+ "preview/render",
+ new
+ {
+ fileName = request.FileName,
+ text = request.Text,
+ assemblies,
+ width = request.Size?.Width,
+ height = request.Size?.Height,
+ scale = request.Scale
+ },
+ timeout.Token);
+
+ if ((bool?)result?["restartRequired"] == true)
+ {
+ Stop();
+ continue;
+ }
+ return Read(result);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ Stop();
+ return Error("The preview took too long to draw, so it was stopped.", null);
+ }
+ catch (Exception ex) when (ex is ConnectionLostException || ex is ObjectDisposedException || ex is IOException)
+ {
+ Stop();
+ if (attempt > 0)
+ return Error("The preview host stopped unexpectedly.", ex.ToString());
+ }
+ }
+ return Error("The preview host could not load the project.", null);
+ }
+ finally
+ {
+ requestLock.Release();
+ }
+ }
+
+ static PreviewRenderResult Read(JToken result)
+ {
+ var error = result?["error"];
+ if (error != null && error.Type == JTokenType.Object)
+ return Error((string)error["message"], (string)error["details"]);
+
+ var image = (string)result?["image"];
+ return new PreviewRenderResult
+ {
+ Image = string.IsNullOrEmpty(image) ? null : Convert.FromBase64String(image),
+ Size = new Size((int?)result?["width"] ?? 0, (int?)result?["height"] ?? 0)
+ };
+ }
+
+ static PreviewRenderResult Error(string message, string details) =>
+ new PreviewRenderResult { Error = new DesignError { Message = message, Details = details ?? message } };
+
+ async Task GetServerAsync()
+ {
+ if (IsRunning)
+ return rpc;
+ if (unavailable)
+ return null;
+
+ // clean up after a host that exited, such as after a rebuild
+ Stop();
+
+ var hostPath = Path.Combine(Path.GetDirectoryName(typeof(PreviewHostClient).Assembly.Location), "preview", HostDll);
+ if (!File.Exists(hostPath))
+ {
+ unavailable = true;
+ Debug.WriteLine($"Eto preview host not found at {hostPath}");
+ return null;
+ }
+
+ try
+ {
+ var start = new ProcessStartInfo("dotnet", "\"" + hostPath + "\"")
+ {
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+ process = Process.Start(start);
+ process.ErrorDataReceived += (sender, e) => Debug.WriteLine(e.Data);
+ process.BeginErrorReadLine();
+
+ rpc = new JsonRpc(new HeaderDelimitedMessageHandler(process.StandardInput.BaseStream, process.StandardOutput.BaseStream));
+ rpc.AddLocalRpcMethod("preview/restart", new Action(OnRestart));
+ rpc.AddLocalRpcMethod("window/logMessage", new Action((type, message) => Debug.WriteLine($"Eto preview: {message}")));
+ rpc.StartListening();
+ await rpc.InvokeWithParameterObjectAsync("initialize", new { processId = Process.GetCurrentProcess().Id, theme = GetTheme() });
+ await rpc.NotifyWithParameterObjectAsync("initialized", new { });
+ return rpc;
+ }
+ catch (Exception ex)
+ {
+ // usually no desktop runtime to run it with, so don't keep trying
+ unavailable = true;
+ Debug.WriteLine($"Could not start the Eto preview host: {ex}");
+ Stop();
+ return null;
+ }
+ }
+
+ void OnRestart()
+ {
+ // the host exits right after this, and the next render starts a fresh one
+ Eto.Forms.Application.Instance.AsyncInvoke(() => ProjectChanged?.Invoke(this, EventArgs.Empty));
+ }
+
+ static object GetTheme()
+ {
+ var theme = Global.Theme;
+ return new
+ {
+ designerPanel = theme.DesignerPanel.ToHex(),
+ designerBackground = theme.DesignerBackground.ToHex(),
+ projectBackground = theme.ProjectBackground.ToHex(),
+ projectForeground = theme.ProjectForeground.ToHex()
+ };
+ }
+
+ void Stop()
+ {
+ var oldRpc = rpc;
+ var oldProcess = process;
+ rpc = null;
+ process = null;
+ try
+ {
+ oldRpc?.Dispose();
+ if (oldProcess != null && !oldProcess.HasExited)
+ oldProcess.Kill();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Could not stop the Eto preview host: {ex.Message}");
+ }
+ oldProcess?.Dispose();
+ }
+ }
+}
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Eto.DevExtension.VisualStudio.Windows.csproj b/src/Eto.DevExtension.VisualStudio.Windows/Eto.DevExtension.VisualStudio.Windows.csproj
index ae16b09..d83579a 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Eto.DevExtension.VisualStudio.Windows.csproj
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Eto.DevExtension.VisualStudio.Windows.csproj
@@ -84,8 +84,8 @@
-
+
@@ -184,8 +184,6 @@
-
-
@@ -211,11 +209,11 @@
-->
-
-
-
-
-
+
+
+
+
+
@@ -282,6 +280,18 @@
+
+
+ $(MSBuildProjectDirectory)\..\Eto.DevExtension.PreviewHost\Eto.DevExtension.PreviewHost.csproj
+ $(ArtifactsDir)Eto.DevExtension.PreviewHost\publish\$(Configuration)\
+
+
+
+
+
+
+
+
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Properties/AssemblyInfo.cs b/src/Eto.DevExtension.VisualStudio.Windows/Properties/AssemblyInfo.cs
index e04d3d4..87b78d0 100644
--- a/src/Eto.DevExtension.VisualStudio.Windows/Properties/AssemblyInfo.cs
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Properties/AssemblyInfo.cs
@@ -10,7 +10,7 @@
[assembly: AssemblyTitle("Eto.VisualStudio")]
[assembly: AssemblyDescription("")]
-[assembly: AssemblyVersion("2.7.5.0")]
+[assembly: AssemblyVersion("2.12.0.0")]
// Setting ComVisible to false makes the types in this assembly not visible
@@ -21,6 +21,6 @@
[assembly: ProvideCodeBase(
AssemblyName = "Eto.DevExtension.VisualStudio.Windows",
- Version = "2.7.5.0",
+ Version = "2.12.0.0",
CodeBase = "$PackageFolder$\\Eto.DevExtension.VisualStudio.Windows.dll")]
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-CSharp/App.vstemplate b/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-CSharp/App.vstemplate
index 26fc4bf..3d61f5e 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-CSharp/App.vstemplate
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-CSharp/App.vstemplate
@@ -35,7 +35,7 @@
- Eto.DevExtension.VisualStudio.Windows, Version=2.7.5.0, Culture=neutral, PublicKeyToken=null
+ Eto.DevExtension.VisualStudio.Windows, Version=2.12.0.0, Culture=neutral, PublicKeyToken=null
Eto.DevExtension.VisualStudio.Windows.Wizards.ProjectWizard
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-FSharp/App.vstemplate b/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-FSharp/App.vstemplate
index 6c10728..b6ad1ac 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-FSharp/App.vstemplate
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-FSharp/App.vstemplate
@@ -35,7 +35,7 @@
- Eto.DevExtension.VisualStudio.Windows, Version=2.7.5.0, Culture=neutral, PublicKeyToken=null
+ Eto.DevExtension.VisualStudio.Windows, Version=2.12.0.0, Culture=neutral, PublicKeyToken=null
Eto.DevExtension.VisualStudio.Windows.Wizards.ProjectWizard
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-VisualBasic/App.vstemplate b/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-VisualBasic/App.vstemplate
index 63c1e3c..141ea1a 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-VisualBasic/App.vstemplate
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Templates/App-VisualBasic/App.vstemplate
@@ -35,7 +35,7 @@
- Eto.DevExtension.VisualStudio.Windows, Version=2.7.5.0, Culture=neutral, PublicKeyToken=null
+ Eto.DevExtension.VisualStudio.Windows, Version=2.12.0.0, Culture=neutral, PublicKeyToken=null
Eto.DevExtension.VisualStudio.Windows.Wizards.ProjectWizard
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-CSharp/File.vstemplate b/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-CSharp/File.vstemplate
index 23ee50f..99d3cb7 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-CSharp/File.vstemplate
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-CSharp/File.vstemplate
@@ -24,7 +24,7 @@
- Eto.DevExtension.VisualStudio.Windows, Version=2.7.5.0, Culture=neutral, PublicKeyToken=null
+ Eto.DevExtension.VisualStudio.Windows, Version=2.12.0.0, Culture=neutral, PublicKeyToken=null
Eto.DevExtension.VisualStudio.Windows.Wizards.ProjectWizard
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-FSharp/File.vstemplate b/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-FSharp/File.vstemplate
index 520fe35..ee5f3c2 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-FSharp/File.vstemplate
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-FSharp/File.vstemplate
@@ -24,7 +24,7 @@
- Eto.DevExtension.VisualStudio.Windows, Version=2.7.5.0, Culture=neutral, PublicKeyToken=null
+ Eto.DevExtension.VisualStudio.Windows, Version=2.12.0.0, Culture=neutral, PublicKeyToken=null
Eto.DevExtension.VisualStudio.Windows.Wizards.ProjectWizard
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-VisualBasic/File.vstemplate b/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-VisualBasic/File.vstemplate
index 6415732..bcce098 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-VisualBasic/File.vstemplate
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Templates/File-VisualBasic/File.vstemplate
@@ -24,7 +24,7 @@
- Eto.DevExtension.VisualStudio.Windows, Version=2.7.5.0, Culture=neutral, PublicKeyToken=null
+ Eto.DevExtension.VisualStudio.Windows, Version=2.12.0.0, Culture=neutral, PublicKeyToken=null
Eto.DevExtension.VisualStudio.Windows.Wizards.ProjectWizard
diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Wizards/EtoInitializer.cs b/src/Eto.DevExtension.VisualStudio.Windows/Wizards/EtoInitializer.cs
index 4c7401a..01c7961 100755
--- a/src/Eto.DevExtension.VisualStudio.Windows/Wizards/EtoInitializer.cs
+++ b/src/Eto.DevExtension.VisualStudio.Windows/Wizards/EtoInitializer.cs
@@ -34,8 +34,6 @@ public static void Initialize()
if (Application.Instance == null)
new Eto.Forms.Application().Attach();
-
- Eto.Designer.Builders.BaseCompiledInterfaceBuilder.InitializeAssembly = typeof(EtoInitializer).Assembly.FullName;
}
private static void ThemeWindow(System.Windows.Window w)