Auto detect all Visual Studio and Rider installs

This commit is contained in:
Simon Lübeß
2026-08-14 23:34:04 +02:00
parent f8e533f28d
commit 62ad8bee35
13 changed files with 703 additions and 563 deletions
+2 -4
View File
@@ -3,9 +3,7 @@ namespace GlitchyEditor.CodeEditors;
interface IIdeAdapter interface IIdeAdapter
{ {
static void OpenScript(StringView fileName); void OpenScript(StringView fileName, int lineNumber = 0);
static void OpenScript(StringView fileName, int lineNumber); void OpenScriptProject();
static void OpenScriptProject();
} }
@@ -6,29 +6,34 @@ using System.IO;
using System.IO; using System.IO;
using GlitchyEditor.EditWindows; using GlitchyEditor.EditWindows;
using ImGui; using ImGui;
using GlitchyEditor.Settings;
namespace GlitchyEditor.CodeEditors; namespace GlitchyEditor.CodeEditors;
class RiderIdeAdapter : IIdeAdapter class RiderIdeAdapter : IIdeAdapter
{ {
public static void OpenScript(StringView fileName) private IdeInstallation _ideInstallation;
public this(IdeInstallation ideInstallation)
{ {
OpenScript(fileName, 0); Debug.Assert(ideInstallation.Ide == .Rider);
_ideInstallation = ideInstallation;
} }
public static void OpenScript(StringView fileName, int lineNumber) public void OpenScript(StringView fileName, int lineNumber)
{ {
String solutionPath = scope .(); String solutionPath = scope .();
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
ProcessStartInfo startInfo = scope .(); ProcessStartInfo startInfo = scope .();
startInfo.SetFileName(EditorApp.Instance.Settings.ScriptSettings.RiderPath); startInfo.SetFileName(_ideInstallation.Path);
startInfo.SetArguments(scope $"{solutionPath} --line {lineNumber} {fileName}"); startInfo.SetArguments(scope $"{solutionPath} --line {lineNumber} {fileName}");
scope SpawnedProcess().Start(startInfo); scope SpawnedProcess().Start(startInfo);
} }
public static void OpenScriptProject() public void OpenScriptProject()
{ {
String solutionPath = scope .(); String solutionPath = scope .();
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
@@ -39,7 +44,7 @@ class RiderIdeAdapter : IIdeAdapter
return; return;
} }
if (!File.Exists(EditorApp.Instance.Settings.ScriptSettings.RiderPath)) if (!File.Exists(_ideInstallation.Path))
{ {
Editor.Instance.ShowSettings(); Editor.Instance.ShowSettings();
Editor.Instance.SettingsWindow.HighlightSetting("Tools", "Rider path"); Editor.Instance.SettingsWindow.HighlightSetting("Tools", "Rider path");
@@ -51,7 +56,7 @@ class RiderIdeAdapter : IIdeAdapter
} }
ProcessStartInfo startInfo = scope .(); ProcessStartInfo startInfo = scope .();
startInfo.SetFileName(EditorApp.Instance.Settings.ScriptSettings.RiderPath); startInfo.SetFileName(_ideInstallation.Path);
startInfo.SetArguments(solutionPath); startInfo.SetArguments(solutionPath);
scope SpawnedProcess().Start(startInfo); scope SpawnedProcess().Start(startInfo);
@@ -2,12 +2,22 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using GlitchyEngine; using GlitchyEngine;
using System.Collections; using System.Collections;
using GlitchyEditor.Settings;
namespace GlitchyEditor.CodeEditors; namespace GlitchyEditor.CodeEditors;
class VisualStudioIdeAdapter : IIdeAdapter class VisualStudioIdeAdapter : IIdeAdapter
{ {
public static bool IsRunning() private IdeInstallation _ideInstallation;
public this(IdeInstallation ideInstallation)
{
Debug.Assert(ideInstallation.Ide == .VisualStudio);
_ideInstallation = ideInstallation;
}
public bool IsRunning()
{ {
List<Process> processes = scope .(); List<Process> processes = scope .();
@@ -19,6 +29,7 @@ class VisualStudioIdeAdapter : IIdeAdapter
for (Process process in processes) for (Process process in processes)
{ {
// TODO?
} }
ClearAndDeleteItems!(processes); ClearAndDeleteItems!(processes);
@@ -26,12 +37,12 @@ class VisualStudioIdeAdapter : IIdeAdapter
return false; return false;
} }
public static void OpenScript(StringView fileName) public void OpenScript(StringView fileName, int lineNumber)
{ {
if (IsRunning()) if (IsRunning())
{ {
ProcessStartInfo startInfo = scope .(); ProcessStartInfo startInfo = scope .();
startInfo.SetFileName(EditorApp.Instance.Settings.ScriptSettings.VisualStudioPath); startInfo.SetFileName(_ideInstallation.Path);
startInfo.SetArguments(scope $"/Edit {fileName}"); startInfo.SetArguments(scope $"/Edit {fileName}");
scope SpawnedProcess().Start(startInfo); scope SpawnedProcess().Start(startInfo);
@@ -39,25 +50,20 @@ class VisualStudioIdeAdapter : IIdeAdapter
else else
{ {
ProcessStartInfo startInfo = scope .(); ProcessStartInfo startInfo = scope .();
startInfo.SetFileName(EditorApp.Instance.Settings.ScriptSettings.VisualStudioPath); startInfo.SetFileName(_ideInstallation.Path);
startInfo.SetArguments(scope $"/Edit {fileName}"); startInfo.SetArguments(scope $"/Edit {fileName}");
scope SpawnedProcess().Start(startInfo); scope SpawnedProcess().Start(startInfo);
} }
} }
public static void OpenScript(StringView fileName, int lineNumber) public void OpenScriptProject()
{
OpenScript(fileName);
}
public static void OpenScriptProject()
{ {
String solutionPath = scope .(); String solutionPath = scope .();
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
ProcessStartInfo psi = scope .(); ProcessStartInfo psi = scope .();
psi.SetFileName("devenv"); psi.SetFileName(_ideInstallation.Path);
psi.SetArguments(solutionPath); psi.SetArguments(solutionPath);
scope SpawnedProcess().Start(psi); scope SpawnedProcess().Start(psi);
@@ -604,13 +604,7 @@ namespace GlitchyEditor.EditWindows
if (ImGui.MenuItem("Open C# Project...")) if (ImGui.MenuItem("Open C# Project..."))
{ {
switch (EditorApp.Instance.Settings.ScriptSettings.SelectedIde) Editor.Instance.IdeAdapter.OpenScriptProject();
{
case .VisualStudio:
VisualStudioIdeAdapter.OpenScriptProject();
case .Rider:
RiderIdeAdapter.OpenScriptProject();
}
} }
ImGui.AttachTooltip("Opens the C# Solution of this project."); ImGui.AttachTooltip("Opens the C# Solution of this project.");
@@ -1440,8 +1434,7 @@ namespace GlitchyEditor.EditWindows
// Special treatment for scripts, open them in Visual Studio. // Special treatment for scripts, open them in Visual Studio.
if (entry->Path.EndsWith(".cs")) if (entry->Path.EndsWith(".cs"))
{ {
// Obviously windows only Editor.Instance.IdeAdapter.OpenScript(entry->Path);
RiderIdeAdapter.OpenScript(entry->Path);
} }
else if (Path.OpenFolder(entry->Path) case .Err) else if (Path.OpenFolder(entry->Path) case .Err)
Log.EngineLogger.Error("Failed to open file."); Log.EngineLogger.Error("Failed to open file.");
+1 -1
View File
@@ -317,7 +317,7 @@ class LogWindow : EditorWindow
{ {
if (message.Source.MessageOrigin != null) if (message.Source.MessageOrigin != null)
{ {
RiderIdeAdapter.OpenScript(message.Source.MessageOrigin.FileName, message.Source.MessageOrigin.LineNumber); Editor.Instance.IdeAdapter.OpenScript(message.Source.MessageOrigin.FileName, message.Source.MessageOrigin.LineNumber);
} }
} }
+31
View File
@@ -6,6 +6,7 @@ using GlitchyEngine.Collections;
using GlitchyEditor.EditWindows; using GlitchyEditor.EditWindows;
using GlitchyEngine; using GlitchyEngine;
using GlitchyEditor.Assets; using GlitchyEditor.Assets;
using GlitchyEditor.CodeEditors;
namespace GlitchyEditor namespace GlitchyEditor
{ {
@@ -79,6 +80,18 @@ namespace GlitchyEditor
private static Editor s_Instance; private static Editor s_Instance;
private IIdeAdapter _ideAdapter = null ~ delete _;
public IIdeAdapter IdeAdapter
{
get => _ideAdapter;
private set
{
delete _ideAdapter;
_ideAdapter = value;
}
}
public static Editor Instance => s_Instance; public static Editor Instance => s_Instance;
/// Creates a new editor for the given world /// Creates a new editor for the given world
@@ -87,6 +100,24 @@ namespace GlitchyEditor
Log.EngineLogger.AssertDebug(s_Instance == null, "Cannot create a second instance of a singleton."); Log.EngineLogger.AssertDebug(s_Instance == null, "Cannot create a second instance of a singleton.");
s_Instance = this; s_Instance = this;
EditorApp.Instance.Settings.OnApplySettings.Add(new (s, e) => {
let activeIde = EditorApp.Instance.Settings.ScriptSettings.ActiveIde;
if (activeIde != null)
{
switch (activeIde.Ide)
{
case .VisualStudio:
IdeAdapter = new VisualStudioIdeAdapter(activeIde);
case .Rider:
IdeAdapter = new RiderIdeAdapter(activeIde);
}
}
else
{
IdeAdapter = null;
}
});
_activeScene = activeScene; _activeScene = activeScene;
_editorScene = editorScene; _editorScene = editorScene;
_contentManager = contentManager; _contentManager = contentManager;
+3 -1
View File
@@ -13,6 +13,7 @@ using GlitchyEditor.Platform;
using System.Diagnostics; using System.Diagnostics;
using GlitchyEditor.ImGui; using GlitchyEditor.ImGui;
using GlitchyEngine.Events; using GlitchyEngine.Events;
using GlitchyEditor.Settings;
namespace GlitchyEditor namespace GlitchyEditor
{ {
@@ -48,6 +49,8 @@ namespace GlitchyEditor
DragDropManager.Init(); DragDropManager.Init();
GlitchyEditor.Settings.Settings.Load();
#if IMGUI #if IMGUI
_imGuiLayer = new ImGuiLayer(); _imGuiLayer = new ImGuiLayer();
PushOverlay(_imGuiLayer); PushOverlay(_imGuiLayer);
@@ -56,7 +59,6 @@ namespace GlitchyEditor
_editorLayer = new EditorLayer(args, _contentManager); _editorLayer = new EditorLayer(args, _contentManager);
PushLayer(_editorLayer); PushLayer(_editorLayer);
GlitchyEditor.Settings.Load();
Settings.OnApplySettings.Add(new (s, e) => { Settings.OnApplySettings.Add(new (s, e) => {
OnEvent(scope SettingsAppliedEvent()); OnEvent(scope SettingsAppliedEvent());
}); });
-292
View File
@@ -1,292 +0,0 @@
using System;
using GlitchyEngine;
using Bon;
using GlitchyEditor;
using GlitchyEditor.CodeEditors;
using System.Collections;
using System.IO;
using System.Diagnostics;
using System.Threading;
namespace GlitchyEditor
{
extension Settings
{
#if DEBUG
[SettingContainer, BonInclude]
public readonly DevSettings DevSettings = new .() ~ delete _;
#endif
[SettingContainer, BonInclude]
public readonly EditorSettings EditorSettings = new .() ~ delete _;
[SettingContainer, BonInclude]
public readonly ScriptSettings ScriptSettings = new .() ~ delete _;
protected override void RegisterEventListeners()
{
#if DEBUG
OnApplySettings.Add(new (s, e) => DevSettings.Apply());
#endif
OnApplySettings.Add(new (s, e) => EditorSettings.Apply());
OnApplySettings.Add(new (s, e) => ScriptSettings.Apply());
}
}
}
namespace GlitchyEditor;
#if DEBUG
[Reflect]
class DevSettings
{
[Setting("Dev", "Use ScriptCore csproj", "If enabled the Editor will include the csproj of the ScriptCore instead of the compiled dll."), BonInclude]
public bool UseScriptCoreDll = true;
public void Apply()
{
}
}
#endif
[Reflect]
enum ScriptIde
{
Rider,
VisualStudio
}
[Reflect]
class ScriptSettings
{
[Setting("Tools", "Visual Studio path", "The path of Visual Studio's \"devenv.exe\""), BonInclude]
public String VisualStudioPath ~ delete _;
[Setting("Tools", "Rider path", "The path to JetBrains Rider IDE (rider64.exe)"), BonInclude]
public String RiderPath ~ delete _;
[Setting("Tools", "IDE", "The IDE that will be used to open scripts for editing."), BonInclude]
public ScriptIde SelectedIde;
private bool _lookedForVs = false;
private bool _lookedForRider = false;
public void Apply()
{
#if BF_PLATFORM_WINDOWS
if (!_lookedForVs && String.IsNullOrWhiteSpace(VisualStudioPath))
{
FindVisualStudio();
}
#endif
if (!_lookedForRider && String.IsNullOrWhiteSpace(RiderPath))
{
FindRider();
}
}
void FindVisualStudio()
{
_lookedForVs = true;
Thread thread = new Thread(new => FindVisualStudioImpl);
thread.AutoDelete = true;
thread.IsBackground = true;
thread.Start();
}
void FindVisualStudioImpl()
{
Log.EngineLogger.Info("Using vswhere.exe to search for Visual Studio...");
String exeFile = scope .();
Environment.GetExecutableFilePath(exeFile);
String exeDirectory = scope .();
Path.GetDirectoryPath(exeFile, exeDirectory);
String vsWherePath = scope .();
Path.Combine(vsWherePath, exeDirectory, "vswhere.exe");
ProcessStartInfo processInfo = scope .();
processInfo.UseShellExecute = false;
processInfo.RedirectStandardOutput = true;
processInfo.CreateNoWindow = true;
processInfo.SetFileName(vsWherePath);
// Find the installation path of the latest visual studio with an installed IDE.
processInfo.SetArguments("-latest -requires Microsoft.VisualStudio.Workload.NativeDesktop -property productPath");
let process = scope SpawnedProcess();
if (process.Start(processInfo) case .Ok)
{
FileStream outputStream = scope FileStream();
process.AttachStandardOutput(outputStream);
process.WaitFor(1000);
if (VisualStudioPath == null)
{
VisualStudioPath = new String();
}
else
{
VisualStudioPath.Clear();
}
if (outputStream.Length > 0)
{
StreamReader streamReaderOut = scope StreamReader(outputStream, null, false, 4096);
streamReaderOut.ReadToEnd(VisualStudioPath).IgnoreError();
VisualStudioPath.Trim();
if (File.Exists(VisualStudioPath))
{
Log.EngineLogger.Info($"Located Visual Studio: {VisualStudioPath}");
Apply();
}
else
{
Log.EngineLogger.Error($"Failed to find Visual Studio. vswhere output: \n{VisualStudioPath}\n\n");
VisualStudioPath.Clear();
}
}
}
else
{
Log.EngineLogger.Error("Couldn't automatically determine location of Visual Studio: Failed to launch vswhere.exe");
}
}
void FindRider()
{
_lookedForRider = true;
Thread thread = new Thread(new => FindRiderImpl);
thread.AutoDelete = true;
thread.IsBackground = true;
thread.Start();
}
void FindRiderImpl()
{
List<RiderInstallInfo> installs = new .();
RiderPathLocator.CollectAllPaths(installs);
// Don't touch the settings or the log from this thread: the editor's log window appends to
// a plain List and isn't synchronized, so logging from here corrupts it.
Application.Instance.InvokeOnMainThread(new () => ReportRiderInstalls(installs));
}
/// Logs the located Rider installations and stores the newest one in RiderPath.
/// @param installs The located installations, newest first. Takes ownership.
bool ReportRiderInstalls(List<RiderInstallInfo> installs)
{
defer { DeleteContainerAndItems!(installs); }
for (RiderInstallInfo install in installs)
{
Log.EngineLogger.Info($"Found Rider {install.Version} at \"{install.Path}\" ({install.InstallType})");
}
if (installs.IsEmpty)
{
Log.EngineLogger.Warning("Couldn't automatically determine location of Rider: No installation found.");
return true;
}
// CollectAllPaths sorts the installations, newest first.
RiderInstallInfo newestInstall = installs.Front;
if (RiderPath == null)
{
RiderPath = new String(newestInstall.Path);
}
else
{
RiderPath..Clear().Append(newestInstall.Path);
}
Log.EngineLogger.Info($"Located Rider: {RiderPath}");
Apply();
return true;
}
}
[Reflect]
class EditorSettings
{
[Setting("Editor", "Switch to Player on play", "If checked the editor will automatically switch to the \"Play\" window after starting the game."), BonInclude]
public bool SwitchToPlayerOnPlay = true;
[Setting("Editor", "Switch to Player on simulate", "If checked the editor will automatically switch to the \"Play\" window after starting the simulation."), BonInclude]
public bool SwitchToPlayerOnSimulate = true;
[Setting("Editor", "Switch to Player on continue", "If checked the editor will automatically switch to the \"Play\" window when the game is continued after pausing."), BonInclude]
public bool SwitchToPlayerOnResume = false;
[Setting("Editor", "Switch to Editor on stop", "If checked the editor will automatically switch to the \"Editor\" window after stopping the game."), BonInclude]
public bool SwitchToEditorOnStop = true;
[Setting("Editor", "Switch to Editor on pause", "If checked the editor will automatically switch to the \"Editor\" window when the game is being paused."), BonInclude]
public bool SwitchToEditorOnPause = false;
[Setting("Editor", "Clear log on play", "If checked the message log will be cleared when the play-mode is entered."), BonInclude]
public bool ClearLogOnPlay = true;
[BonInclude]
private List<String> _recentProjects ~ DeleteContainerAndItems!(_);
/// Gets or sets the path of the Project that was last open.
public StringView LastOpenedProject
{
get
{
if (_recentProjects == null || _recentProjects.Count == 0)
return "";
return _recentProjects[0];
}
set
{
if (_recentProjects == null)
_recentProjects = new List<String>();
// Remove duplicates
for (var entry in _recentProjects)
{
if (entry == value)
{
delete entry;
@entry.Remove();
}
}
_recentProjects.Insert(0, new String(value));
if (_recentProjects.Count > 10)
{
// We only store 10 entries, delete the rest
for (int i = 10; i < _recentProjects.Count; i++)
{
delete _recentProjects[i];
}
_recentProjects.Count = 10;
}
}
}
public List<String> RecentProjects => _recentProjects;
public void Apply()
{
}
}
+1
View File
@@ -4,6 +4,7 @@ using Bon;
using GlitchyEngine; using GlitchyEngine;
using GlitchyEngine.Scripting; using GlitchyEngine.Scripting;
using Xml_Beef; using Xml_Beef;
using GlitchyEditor.Settings;
namespace GlitchyEditor; namespace GlitchyEditor;
-146
View File
@@ -1,146 +0,0 @@
using System;
using ImGui;
using System.Collections;
using System.IO;
using Bon;
using GlitchyEngine;
namespace GlitchyEditor
{
interface ISettings
{
void Apply();
}
/// Fields with this Attribute will be scanned for Settings.
[AttributeUsage(.Field, .ReflectAttribute)]
struct SettingContainerAttribute : Attribute
{
}
enum SettingEditor
{
case Default;
case Path;//(bool MultiSelect, bool OpenFolderDialog, StringView Filter);
}
/// Fields with this Attribute will be exposed as settings.
[AttributeUsage(.Field, .ReflectAttribute)]
struct SettingAttribute : Attribute
{
public String Category;
public String Name;
public String Tooltip;
public SettingEditor EditorMode;
public this(String category, String name, String tooltip = "", SettingEditor editorMode = .Default)
{
Category = category;
Name = name;
Tooltip = tooltip;
EditorMode = editorMode;
}
}
[Reflect, BonTarget]
class Settings
{
#if IMGUI
[SettingContainer, BonInclude]
public readonly ImGuiSettings ImGuiSettings = new .() ~ delete _;
#endif
[BonIgnore]
public Event<EventHandler> OnApplySettings = .() ~ _.Dispose();
/*
[BonInclude]
private List<ISettings> _userSettings ~ ClearAndDeleteItems!(_);
*/
public this()
{
/*List<ISettings> userSettings = append .();
_userSettings = userSettings;*/
RegisterEventListeners();
}
protected virtual void RegisterEventListeners()
{
}
public void Apply()
{
#if IMGUI
ImGuiSettings.Apply();
#endif
OnApplySettings.Invoke(this, .Empty);
/*for (let settings in _userSettings)
{
settings.Apply();
}*/
}
public static void Load()
{
Settings settings = EditorApp.Instance.Settings;
if (!File.Exists("./settings.bon"))
{
settings.Save();
}
var result = Bon.DeserializeFromFile(ref settings, "./settings.bon");
if (result case .Err)
Log.EngineLogger.Error("Failed to deserialze settings.");
settings.Apply();
}
public void Save()
{
gBonEnv.serializeFlags |= .Verbose;
Bon.SerializeIntoFile(this, "./settings.bon");
}
/*
/// Registers a instance of a settings interface. Note: Takes ownership of the instance.
public void RegisterUserSettings(ISettings settings)
{
_userSettings.Add(settings);
}
public T GetUserSettings<T>() where T : ISettings, class
{
for (let v in _userSettings)
{
if (v is T)
{
return (T)v;
}
}
return null;
}*/
}
#if IMGUI
[Reflect]
class ImGuiSettings
{
[Setting("UI", "Font Size"), BonInclude]
public int32 FontSize = 14;
[Setting("UI", "Font name"), BonInclude]
public readonly String FontName = new .("Fonts/CascadiaCode.ttf") ~ delete _;
public void Apply()
{
EditorApp.Instance.[Friend]_imGuiLayer.SettingsInvalid = true;
}
}
#endif
}
@@ -0,0 +1,192 @@
using GlitchyEngine;
using System.Collections;
using GlitchyEditor.CodeEditors;
using System.IO;
using System;
using System.Diagnostics;
using ImGui;
using GlitchyEditor.Multithreading;
namespace GlitchyEditor.Settings;
class DetectIDEsBackgroundTask : BackgroundTask
{
enum Stage
{
None,
SearchVisualStudio,
SearchRider
}
private Stage _stage = .None;
private ScriptSettings _settings;
public this(ScriptSettings settings)
{
_settings = settings;
}
public override RunResult Run()
{
// Remove all auto detected IDEs, keep manual ones
using (_settings.EnterAndGetIdeInstallations(let installations))
{
for (let ide in installations)
{
if (ide.IsAutoDetected)
{
@ide.Remove();
delete ide;
}
}
}
#if BF_PLATFORM_WINDOWS
_stage = .SearchVisualStudio;
FindVisualStudio();
#endif
_stage = .SearchRider;
FindRider();
return .Finished;
}
public override void OnRenderPopup()
{
if (ImGui.Begin("Detecting IDEs...", null, .NoDocking | .NoCollapse | .Modal | .NoResize | .AlwaysAutoResize))
{
if (_stage == .SearchVisualStudio)
{
ImGui.Text("Searching Visual Studio installations...");
}
else if (_stage == .SearchRider)
{
ImGui.Text("Searching Rider installations...");
}
ImGui.End();
}
}
#if BF_PLATFORM_WINDOWS
void FindVisualStudio()
{
Log.EngineLogger.Info("Using vswhere.exe to search for Visual Studio...");
String exeFile = scope .();
Environment.GetExecutableFilePath(exeFile);
String exeDirectory = scope .();
Path.GetDirectoryPath(exeFile, exeDirectory);
String vsWherePath = scope .();
Path.Combine(vsWherePath, exeDirectory, "vswhere.exe");
ProcessStartInfo processInfo = scope .();
processInfo.UseShellExecute = false;
processInfo.RedirectStandardOutput = true;
processInfo.CreateNoWindow = true;
processInfo.SetFileName(vsWherePath);
// Find the installation path of the latest visual studio with an installed IDE.
processInfo.SetArguments("-sort -requires Microsoft.VisualStudio.Component.Roslyn.LanguageServices -format text");
let process = scope SpawnedProcess();
if (process.Start(processInfo) case .Ok)
{
FileStream outputStream = scope FileStream();
process.AttachStandardOutput(outputStream);
process.WaitFor(1000);
StreamReader sr = scope .(outputStream);
IdeInstallation installation = null;
String line = scope .(256);
while (sr.ReadLine(line..Clear()) case .Ok)
{
const String InstanceIdPropertyName = "instanceId: ";
const String ProductPathPropertyName = "productPath: ";
const String DisplayNamePropertyName = "displayName: ";
if (line.StartsWith(InstanceIdPropertyName))
{
if (installation != null)
{
using (_settings.EnterAndGetIdeInstallations(let installations))
{
installations.Add(installation);
}
}
installation = new IdeInstallation();
installation.Ide = .VisualStudio;
installation.IsAutoDetected = true;
}
else if (line.StartsWith(ProductPathPropertyName))
{
installation.Path = line.Substring(ProductPathPropertyName.Length);
}
else if (line.StartsWith(DisplayNamePropertyName))
{
installation.Name = line.Substring(DisplayNamePropertyName.Length);
}
}
if (!installation.Name.IsEmpty && !installation.Path.IsEmpty)
{
using (_settings.EnterAndGetIdeInstallations(let installations))
{
installations.Add(installation);
}
}
else
{
delete installation;
}
}
else
{
Log.EngineLogger.Error("Couldn't automatically determine location of Visual Studio: Failed to launch vswhere.exe");
}
}
#endif
private void FindRider()
{
List<RiderInstallInfo> installs = scope .();
RiderPathLocator.CollectAllPaths(installs);
ReportRiderInstalls(installs);
ClearAndDeleteItems!(installs);
}
private void ReportRiderInstalls(List<RiderInstallInfo> installs)
{
if (installs.IsEmpty)
{
Log.EngineLogger.Warning("Couldn't automatically determine location of Rider: No installation found.");
}
using (_settings.EnterAndGetIdeInstallations(let installations))
{
for (RiderInstallInfo install in installs)
{
Log.EngineLogger.Info($"Found Rider {install.Version} at \"{install.Path}\" ({install.InstallType})");
IdeInstallation ide = new IdeInstallation();
ide.IsAutoDetected = true;
ide.Ide = .Rider;
ide.Path = install.Path;
ide.Name = scope $"Rider {install.Version}";
installations.Add(ide);
}
}
}
}
+386
View File
@@ -0,0 +1,386 @@
using System;
using ImGui;
using System.Collections;
using System.IO;
using Bon;
using GlitchyEngine;
using System.Threading;
using System.Linq;
using static GlitchyEditor.SettingsWindow;
namespace GlitchyEditor.Settings;
interface ISettings
{
void Apply();
}
function void RenderMethod(SettingsWindow.Category category);
function void FieldRenderMethod(SettingsWindow.Binding setting);
/// Fields with this Attribute will be scanned for Settings.
[AttributeUsage(.Field, .ReflectAttribute)]
struct SettingContainerAttribute : Attribute
{
public RenderMethod RenderMethod;
}
enum SettingEditor
{
case Default;
case Path;//(bool MultiSelect, bool OpenFolderDialog, StringView Filter);
}
/// Fields with this Attribute will be exposed as settings.
[AttributeUsage(.Field, .ReflectAttribute)]
struct SettingAttribute : Attribute
{
public String Category;
public String Name;
public String Tooltip;
public SettingEditor EditorMode;
public this(String category, String name, String tooltip = "", SettingEditor editorMode = .Default)
{
Category = category;
Name = name;
Tooltip = tooltip;
EditorMode = editorMode;
}
}
/// Fields with this Attribute will be exposed as settings.
[AttributeUsage(.Method, .ReflectAttribute | .AlwaysIncludeTarget)]
struct CustomSettingRendererAttribute : Attribute
{
public String Category;
public this(String category)
{
Category = category;
}
}
[Reflect, BonTarget]
class Settings
{
private bool _areFromDisk = false;
public bool AreFromDisk => _areFromDisk;
#if IMGUI
[SettingContainer, BonInclude]
public readonly ImGuiSettings ImGuiSettings = new .() ~ delete _;
#endif
[BonIgnore]
public Event<EventHandler> OnApplySettings = .() ~ _.Dispose();
#if DEBUG
[SettingContainer, BonInclude]
public readonly DevSettings DevSettings = new .() ~ delete _;
#endif
[SettingContainer, BonInclude]
public readonly EditorSettings EditorSettings = new .() ~ delete _;
[SettingContainer, BonInclude]
public readonly ScriptSettings ScriptSettings = new .() ~ delete _;
/*
[BonInclude]
private List<ISettings> _userSettings ~ ClearAndDeleteItems!(_);
*/
public this()
{
/*List<ISettings> userSettings = append .();
_userSettings = userSettings;*/
RegisterEventListeners();
}
protected virtual void RegisterEventListeners()
{
}
public void Apply()
{
#if IMGUI
ImGuiSettings.Apply();
#endif
#if DEBUG
DevSettings.Apply();
#endif
EditorSettings.Apply();
ScriptSettings.Apply();
OnApplySettings.Invoke(this, .Empty);
/*for (let settings in _userSettings)
{
settings.Apply();
}*/
}
public static void Load()
{
Settings settings = EditorApp.Instance.Settings;
if (!File.Exists("./settings.bon"))
{
settings.Save();
}
var result = Bon.DeserializeFromFile(ref settings, "./settings.bon");
settings._areFromDisk = true;
if (result case .Err)
Log.EngineLogger.Error("Failed to deserialze settings.");
settings.Apply();
}
public void Save()
{
gBonEnv.serializeFlags |= .Verbose;
Bon.SerializeIntoFile(this, "./settings.bon");
}
/*
/// Registers a instance of a settings interface. Note: Takes ownership of the instance.
public void RegisterUserSettings(ISettings settings)
{
_userSettings.Add(settings);
}
public T GetUserSettings<T>() where T : ISettings, class
{
for (let v in _userSettings)
{
if (v is T)
{
return (T)v;
}
}
return null;
}*/
}
#if IMGUI
[Reflect]
class ImGuiSettings
{
[Setting("UI", "Font Size"), BonInclude]
public int32 FontSize = 14;
[Setting("UI", "Font name"), BonInclude]
public readonly String FontName = new .("Fonts/CascadiaCode.ttf") ~ delete _;
public void Apply()
{
EditorApp.Instance.[Friend]_imGuiLayer?.SettingsInvalid = true;
}
}
#endif
#if DEBUG
[Reflect]
class DevSettings
{
[Setting("Dev", "Use ScriptCore csproj", "If enabled the Editor will include the csproj of the ScriptCore instead of the compiled dll."), BonInclude]
public bool UseScriptCoreDll = true;
public void Apply()
{
}
}
#endif
[Reflect]
enum ScriptIde
{
Rider,
VisualStudio
}
[Reflect]
class IdeInstallation
{
[BonInclude]
public ScriptIde Ide;
[BonInclude]
private String _path ~ delete _;
[BonInclude]
private String _name ~ delete _;
public StringView Path
{
get => _path;
set => String.NewOrSet!(_path, value);
}
public StringView Name
{
get => _name;
set => String.NewOrSet!(_name, value);
}
[BonInclude]
public bool IsAutoDetected;
// TODO: allow custom IDEs? We then need something like "'Open Project' command line", "'Open file' command line" and "'Open file at location' command line"
// This would however be quite complicated to implement well, when looking at how different Visual Studio and Rider already are.
}
[Reflect]
class ScriptSettings
{
[BonInclude]
private List<IdeInstallation> _ideInstallations = new .() ~ DeleteContainerAndItems!(_);
// TODO: It's probably overkill to have a lock for the ide list.
// If we cared enough about multi threading, InvokeOnMainThread would probably be enough.
private Monitor _ideInstallationsLock = new .() ~ delete _;
[BonInclude]
private String _activeIdePath = new String() ~ delete _;
public IdeInstallation ActiveIde => _ideInstallations.Where((i) => i.Path == _activeIdePath).FirstOrDefault();
private bool _lookedForVs = false;
private bool _lookedForRider = false;
/// Enters the lock and gets the list of Ide installations.
public Monitor.MonitorLockInstance EnterAndGetIdeInstallations(out List<IdeInstallation> ideInstallations)
{
let lock = _ideInstallationsLock.Enter();
ideInstallations = _ideInstallations;
return lock;
}
public void Apply()
{
if (ActiveIde == null)
{
using (EnterAndGetIdeInstallations(let ideInstallations))
{
// If no IDE is selected, just select the first one
if (ideInstallations?.Count > 0)
{
_activeIdePath.Set(ideInstallations.Front.Path);
}
}
}
}
[CustomSettingRenderer("Tools")]
public bool RenderUI(SettingsWindow.Category category)
{
bool settingsChanged = false;
ImGui.Text("IDE:");
if (ImGui.BeginCombo("##ide", ActiveIde?.Name.ToScopeCStr!()))
{
using (EnterAndGetIdeInstallations(let installations))
{
for (let ide in installations)
{
if (ImGui.Selectable(ide.Name.ToScopeCStr!()) && ActiveIde != ide)
{
_activeIdePath.Set(ide.Path);
settingsChanged = true;
}
ImGui.AttachTooltip(ide.Path);
}
}
ImGui.EndCombo();
}
if (ImGui.Button("Detect IDEs..."))
{
//BackgroundTask task = new BackgroundTask();
EditorApp.Instance.BackgroundTaskManager.StartBackgroundTask(new DetectIDEsBackgroundTask(this));
}
return settingsChanged;
}
}
[Reflect]
class EditorSettings
{
[Setting("Editor", "Switch to Player on play", "If checked the editor will automatically switch to the \"Play\" window after starting the game."), BonInclude]
public bool SwitchToPlayerOnPlay = true;
[Setting("Editor", "Switch to Player on simulate", "If checked the editor will automatically switch to the \"Play\" window after starting the simulation."), BonInclude]
public bool SwitchToPlayerOnSimulate = true;
[Setting("Editor", "Switch to Player on continue", "If checked the editor will automatically switch to the \"Play\" window when the game is continued after pausing."), BonInclude]
public bool SwitchToPlayerOnResume = false;
[Setting("Editor", "Switch to Editor on stop", "If checked the editor will automatically switch to the \"Editor\" window after stopping the game."), BonInclude]
public bool SwitchToEditorOnStop = true;
[Setting("Editor", "Switch to Editor on pause", "If checked the editor will automatically switch to the \"Editor\" window when the game is being paused."), BonInclude]
public bool SwitchToEditorOnPause = false;
[Setting("Editor", "Clear log on play", "If checked the message log will be cleared when the play-mode is entered."), BonInclude]
public bool ClearLogOnPlay = true;
[BonInclude]
private List<String> _recentProjects ~ DeleteContainerAndItems!(_);
/// Gets or sets the path of the Project that was last open.
public StringView LastOpenedProject
{
get
{
if (_recentProjects == null || _recentProjects.Count == 0)
return "";
return _recentProjects[0];
}
set
{
if (_recentProjects == null)
_recentProjects = new List<String>();
// Remove duplicates
for (var entry in _recentProjects)
{
if (entry == value)
{
delete entry;
@entry.Remove();
}
}
_recentProjects.Insert(0, new String(value));
if (_recentProjects.Count > 10)
{
// We only store 10 entries, delete the rest
for (int i = 10; i < _recentProjects.Count; i++)
{
delete _recentProjects[i];
}
_recentProjects.Count = 10;
}
}
}
public List<String> RecentProjects => _recentProjects;
public void Apply()
{
}
}
@@ -6,12 +6,13 @@ using System.Collections;
using System.Reflection; using System.Reflection;
using GlitchyEngine.Math; using GlitchyEngine.Math;
using System.IO; using System.IO;
using GlitchyEditor.Settings;
namespace GlitchyEditor namespace GlitchyEditor
{ {
class SettingsWindow : EditorWindow class SettingsWindow : EditorWindow
{ {
class Binding public class Binding
{ {
public Object SettingsObject; public Object SettingsObject;
public String Name ~ delete _; public String Name ~ delete _;
@@ -29,9 +30,22 @@ namespace GlitchyEditor
} }
} }
class Category public struct MethodBinding
{
public Object SettingsObject;
public MethodInfo Method;
public this(MethodInfo method, Object settingsObject)
{
Method = method;
SettingsObject = settingsObject;
}
}
public class Category
{ {
public List<Binding> _bindings = new .() ~ DeleteContainerAndItems!(_); public List<Binding> _bindings = new .() ~ DeleteContainerAndItems!(_);
public List<MethodBinding> _methodBindings = new .() ~ delete _;
public String Header ~ delete _; public String Header ~ delete _;
@@ -47,6 +61,11 @@ namespace GlitchyEditor
Binding binding = new .(name, fieldName, settingsObject ?? SettingsObject, tooltip, editorType); Binding binding = new .(name, fieldName, settingsObject ?? SettingsObject, tooltip, editorType);
_bindings.Add(binding); _bindings.Add(binding);
} }
public void AddRenderer(MethodInfo method, Object settingsObject = null)
{
_methodBindings.Add(MethodBinding(method, settingsObject));
}
} }
Settings _settings; Settings _settings;
@@ -87,6 +106,8 @@ namespace GlitchyEditor
void ScanForSettings(Object container) void ScanForSettings(Object container)
{ {
Log.EngineLogger.Info($"Visiting type: {container.GetType().GetName(.. scope .())}");
Type type = container.GetType(); Type type = container.GetType();
for (var field in type.GetFields()) for (var field in type.GetFields())
@@ -114,6 +135,18 @@ namespace GlitchyEditor
ScanForSettings(childContainer); ScanForSettings(childContainer);
} }
} }
for (let method in type.GetMethods(.Instance | .Static | .Public | .NonPublic))
{
Log.EngineLogger.Info($"Visiting method: {method.Name}");
Result<CustomSettingRendererAttribute> rendererResult = method.GetCustomAttribute<CustomSettingRendererAttribute>();
if (rendererResult case .Ok(let rendererInfo))
{
AddRenderer(rendererInfo.Category, method, container);
}
}
} }
void AddSetting(String categoryName, String name, StringView fieldName, Object container, StringView tooltip, SettingEditor settingEditor) void AddSetting(String categoryName, String name, StringView fieldName, Object container, StringView tooltip, SettingEditor settingEditor)
@@ -123,6 +156,12 @@ namespace GlitchyEditor
category.AddSetting(name, fieldName, container, tooltip, settingEditor); category.AddSetting(name, fieldName, container, tooltip, settingEditor);
} }
void AddRenderer(String categoryName, MethodInfo method, Object container)
{
Category category = AddCategory(categoryName);
category.AddRenderer(method, container);
}
private String _tabToSelect = new String() ~ delete _; private String _tabToSelect = new String() ~ delete _;
private String _SettingToHighlight = new String() ~ delete _; private String _SettingToHighlight = new String() ~ delete _;
private float _timeToHighlight; private float _timeToHighlight;
@@ -157,6 +196,7 @@ namespace GlitchyEditor
if (ImGui.BeginTabItem(category.Header, null, flags)) if (ImGui.BeginTabItem(category.Header, null, flags))
{ {
ShowCategory(category); ShowCategory(category);
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
} }
@@ -209,6 +249,12 @@ namespace GlitchyEditor
ImGui.TableNextColumn(); ImGui.TableNextColumn();
/*if (setting.EditorType case .Custom(let renderMethod))
{
renderMethod(setting);
continue;
}*/
// Name of setting // Name of setting
ImGui.TextUnformatted(setting.Name); ImGui.TextUnformatted(setting.Name);
@@ -370,100 +416,18 @@ namespace GlitchyEditor
_SettingToHighlight.Clear(); _SettingToHighlight.Clear();
} }
} }
/*if (fieldInfo.FieldType.IsEnum)
{
uint64 enumValue = 0;
fieldInfo.GetValueReference(setting.SettingsObject);
// Assign value
switch (fieldInfo.FieldType.Size)
{
case 1: enumValue = *(uint8*)&enumValue;
case 2: *(uint16*)&enumValue = *(uint16*)&enumValue;
case 4: *(uint32*)&enumValue = *(uint32*)&enumValue;
case 8: *(uint64*)&enumValue = *(uint64*)&enumValue;
} }
bool found = false; for (let methodBinding in category._methodBindings)
for (var field in valType.GetFields())
{ {
if (field.[Friend]mFieldData.mFlags.HasFlag(.EnumCase) && if (methodBinding.Method.Invoke(methodBinding.Method.IsStatic ? null : methodBinding.SettingsObject, category) case .Ok(var returnValue))
*(int64*)&field.[Friend]mFieldData.[Friend]mData == valueData)
{ {
writer.Enum(field.Name); _settingsChanged |= returnValue.Get<bool>();
found = true;
break; returnValue.Dispose();
} }
} }
// Find field on enum
bool found = false;
for (var field in valType.GetFields())
if (field.[Friend]mFieldData.mFlags.HasFlag(.EnumCase)
&& name == field.Name)
{
// Add value of enum case to current enum value
enumValue |= *(int64*)&field.[Friend]mFieldData.[Friend]mData;
found = true;
break;
}
if (!found)
Error!("Enum case not found", reader, valType);
uint64 value = 0;
StringView selectedValue;
for (FieldInfo enumField in fieldInfo.FieldType.GetFields())
{
if (enumField.[Friend]mFieldData.mFlags.HasFlag(.EnumCase))
{
hasCaseData = true;
if (name == enumField.Name)
{
unionPayload = ValueView(enumField.FieldType, val.dataPtr);
foundCase = true;
break;
}
unionDiscrIndex++;
}
else if (enumField.[Friend]mFieldData.mFlags.HasFlag(.EnumDiscriminator))
{
let discrType = enumField.FieldType;
Debug.Assert(discrType.IsInteger);
discrVal = ValueView(discrType, (uint8*)val.dataPtr + enumField.[Friend]mFieldData.mData);
}
}
ImGui.BeginCombo(scope $"##{setting.Name}", null);
for (var v in Enum.GetValues(fieldInfo.FieldType))
{
}
ImGui.EndCombo();
/*// Find field on enum
bool found = false;
for (var field in valType.GetFields())
if (field.[Friend]mFieldData.mFlags.HasFlag(.EnumCase)
&& name == field.Name)
{
// Add value of enum case to current enum value
enumValue |= *(int64*)&field.[Friend]mFieldData.[Friend]mData;
found = true;
break;
}*/
}*/
}
ImGui.EndTable(); ImGui.EndTable();
} }
} }