Find Rider installations

This commit is contained in:
Simon Lübeß
2026-08-09 16:14:40 +02:00
parent 05187e6b9f
commit faa3040471
6 changed files with 1008 additions and 11 deletions
@@ -0,0 +1,214 @@
GlitchyEditor/src/CodeEditors/RiderPathLocator.bf (and the platform specific implementations)
are a Beef port of the Rider path locator from the JetBrains "Rider Source Code Access" Unreal Engine plugin:
https://github.com/JetBrains/RiderSourceCodeAccess
Source/RiderSourceCodeAccess/Private/RiderPathLocator/
The original work and this port are licensed under the Apache License, Version 2.0,
reproduced in full below.
-------------------------------------------------------------------------------
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0.txt
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,440 @@
// Beef port of JetBrains "Rider Source Code Access":
// https://github.com/JetBrains/RiderSourceCodeAccess/tree/main/Source/RiderSourceCodeAccess/Private/RiderPathLocator
//
// Copyright Epic Games, Inc. All Rights Reserved.
// Licensed under the Apache License, Version 2.0 - see LICENSE-RiderPathLocator.txt
//
// Changes from the original: rewritten in Beef against corlib instead of the
// Unreal Engine types (FString/TArray/TOptional/FPaths/FRegexPattern/FJsonSerializer/...).
// The "plugins/rider-cpp" requirement, the ESupportUproject state and the
// RiderLocations.txt resource-file lookup were removed because they only make sense for Unreal projects.
//
// This file holds the platform independent part, the platform specific parts live in the extensions (e.g. RiderPathLocatorWindows.bf).
using System;
using System.Collections;
using System.IO;
using Beefy.utils;
namespace GlitchyEditor.CodeEditors;
/// A dot-separated version number, e.g. "2024.1.2" or the build number "241.15989.155".
struct RiderVersion : IHashable
{
public const int32 InvalidVersion = -1;
private const int32 MaxParts = 4;
private int32[MaxParts] _parts;
private int32 _count;
public bool IsInitialized => _count != 0;
public int32 Major => _count >= 1 ? _parts[0] : InvalidVersion;
public int32 Minor => _count >= 2 ? _parts[1] : InvalidVersion;
public int32 Patch => _count >= 3 ? _parts[2] : InvalidVersion;
public static RiderVersion Parse(StringView versionString)
{
RiderVersion version = .();
for (StringView part in versionString.Split('.'))
{
if (version._count >= MaxParts)
break;
// Note: the original uses FCString::Atoi, which yields 0 for garbage instead of failing.
int32 value = 0;
if (int32.Parse(part) case .Ok(let parsedValue))
value = parsedValue;
version._parts[version._count] = value;
version._count++;
}
return version;
}
/// Compares two versions element by element.
/// @returns A negative value if lhs is older than rhs, a positive value if it is newer; otherwise 0.
public static int Compare(Self lhs, Self rhs)
{
int32 sharedCount = Math.Min(lhs._count, rhs._count);
for (int32 i < sharedCount)
{
if (lhs._parts[i] != rhs._parts[i])
return lhs._parts[i] < rhs._parts[i] ? -1 : 1;
}
// Equal prefix: the shorter version is the older one, so that "5.0" is older than "5.0.1".
return lhs._count <=> rhs._count;
}
public static bool operator<(Self lhs, Self rhs) => Compare(lhs, rhs) < 0;
public static bool operator==(Self lhs, Self rhs) => Compare(lhs, rhs) == 0;
public static bool operator!=(Self lhs, Self rhs) => Compare(lhs, rhs) != 0;
public int GetHashCode()
{
int hash = 0;
for (int32 i < _count)
hash = (hash * 31) + (int)_parts[i];
return hash;
}
public override void ToString(String strBuffer)
{
if (_count == 0)
{
strBuffer.Append("<unknown>");
return;
}
for (int32 i < _count)
{
if (i != 0)
strBuffer.Append('.');
_parts[i].ToString(strBuffer);
}
}
}
/// How a Rider installation was set up.
enum RiderInstallType
{
/// Installed by the regular Rider installer.
Installed,
/// Installed through the JetBrains Toolbox.
Toolbox,
/// Provided by the user.
Custom
}
/// A single Rider installation found on this machine.
class RiderInstallInfo
{
/// The full path of the Rider executable (rider64.exe).
public String Path ~ delete _;
public RiderVersion Version;
public RiderInstallType InstallType;
public this(StringView path, RiderVersion version, RiderInstallType installType)
{
Path = new String(path);
Version = version;
InstallType = installType;
}
}
/// Locates the JetBrains Rider installations on this machine.
static class RiderPathLocator
{
/// Maximum directory depth we descend into while looking for the Rider executable.
/// The original search is unbounded, but it searches locations like "%LOCALAPPDATA%/Programs".
protected const int MaxSearchDepth = 8;
/// Maximum number of directories visited by a single recursive search.
protected const int MaxSearchedDirectories = 4096;
/// Creates the installation info for the given Rider executable.
/// @param riderExePath The path of a rider64.exe.
/// @returns The installation info; or .Err if the given path is not a usable Rider installation.
public static extern Result<RiderInstallInfo> GetInstallInfoFromRiderPath(StringView riderExePath, RiderInstallType installType);
/// Collects every Rider installation that can be found on this machine.
/// The entries are deduplicated by path and sorted, newest version first.
/// @param outInstalls Receives the installations. The caller takes ownership of the entries.
public static extern void CollectAllPaths(List<RiderInstallInfo> outInstalls);
/// Gets the file name of the Rider executable on this platform (e.g. "rider64.exe").
protected static extern StringView GetRiderExecutableName();
/// Gets the default location the Toolbox V2 installs IDEs into.
protected static extern Result<void> GetDefaultIdeInstallLocationForToolboxV2(String outPath);
/// Moves the located installations into outInstalls, dropping the ones that were found more
/// than once, and sorts them so that the newest installation comes first.
/// @param found The located installations. The entries are moved into outInstalls or deleted.
/// @param outInstalls Receives the installations. The caller takes ownership of the entries.
protected static void DeduplicateAndSort(List<RiderInstallInfo> found, List<RiderInstallInfo> outInstalls)
{
// The same installation usually shows up through more than one source.
for (int i < found.Count)
{
RiderInstallInfo install = found[i];
bool isDuplicate = false;
for (RiderInstallInfo existing in outInstalls)
{
if (IsSamePath(existing.Path, install.Path))
{
isDuplicate = true;
break;
}
}
if (isDuplicate)
continue;
outInstalls.Add(install);
// Ownership moved to outInstalls, make sure the caller's cleanup doesn't delete it.
found[i] = null;
}
// Newest first, so that the caller can simply take the first entry.
outInstalls.Sort(scope (lhs, rhs) => RiderVersion.Compare(rhs.Version, lhs.Version));
}
/// Gets the installation directory of Rider from the path of its executable.
/// The executable is expected to live in a "bin" directory, i.e. "<riderDirectory>/bin/rider64.exe".
protected static Result<void> GetRiderDirectory(StringView riderExePath, String outDirectory)
{
// The original matches the regex "(.*)(?:\\|/)bin" against the executable path,
// which is the same as taking the grandparent directory of a file inside "bin".
String binDirectory = scope .();
Try!(Path.GetDirectoryPath(riderExePath, binDirectory));
String binDirectoryName = scope .();
Path.GetFileName(binDirectory, binDirectoryName);
if (StringView.Compare(binDirectoryName, "bin", true) != 0)
return .Err;
return Path.GetDirectoryPath(binDirectory, outDirectory);
}
/// Compares two paths, ignoring case and the difference between the directory separators.
protected static bool IsSamePath(StringView left, StringView right)
{
if (left.Length != right.Length)
return false;
for (int i < left.Length)
{
char8 leftChar = left[i];
char8 rightChar = right[i];
if (Path.IsDirectorySeparatorChar(leftChar) && Path.IsDirectorySeparatorChar(rightChar))
continue;
if (leftChar.ToLower != rightChar.ToLower)
return false;
}
return true;
}
/// Collects the Rider installations managed by the JetBrains Toolbox at the given location.
protected static void CollectPathsFromToolbox(StringView toolboxPath, List<RiderInstallInfo> outInstalls)
{
if (toolboxPath.IsEmpty || !Directory.Exists(toolboxPath))
return;
String customInstallLocation = scope .();
ExtractPathFromSettingsJson(toolboxPath, customInstallLocation);
if (!customInstallLocation.IsEmpty)
{
// Toolbox V1 puts the IDEs into an "apps" subdirectory of the install location.
String appsPath = scope .();
Path.Combine(appsPath, customInstallLocation, "apps");
int countBefore = outInstalls.Count;
CollectPathsFromToolboxDirectory(appsPath, outInstalls);
if (outInstalls.Count != countBefore)
return;
// Toolbox V2 puts them directly into the install location.
CollectPathsFromToolboxDirectory(customInstallLocation, outInstalls);
return;
}
// Toolbox V1 default install location.
String defaultAppsPath = scope .();
Path.Combine(defaultAppsPath, toolboxPath, "apps");
int countBefore = outInstalls.Count;
CollectPathsFromToolboxDirectory(defaultAppsPath, outInstalls);
if (outInstalls.Count != countBefore)
return;
// Toolbox V2 default install location.
String defaultInstallLocation = scope .();
if (GetDefaultIdeInstallLocationForToolboxV2(defaultInstallLocation) case .Ok)
CollectPathsFromToolboxDirectory(defaultInstallLocation, outInstalls);
}
/// Searches the given directory for Rider executables and creates the installation infos for them.
protected static void CollectPathsFromToolboxDirectory(StringView toolboxRiderRootPath, List<RiderInstallInfo> outInstalls)
{
if (toolboxRiderRootPath.IsEmpty || !Directory.Exists(toolboxRiderRootPath))
return;
List<String> riderPaths = scope .();
defer { ClearAndDeleteItems!(riderPaths); }
int searchedDirectories = 0;
FindFilesRecursive(toolboxRiderRootPath, GetRiderExecutableName(), riderPaths, MaxSearchDepth, ref searchedDirectories);
// Reused across the iterations, see the note in FindFilesRecursive.
String historyJsonPath = scope .();
for (String riderPath in riderPaths)
{
if (!(GetInstallInfoFromRiderPath(riderPath, .Toolbox) case .Ok(let installInfo)))
continue;
// The Toolbox leaves the directories of old versions behind after an update, so we
// check which build it actually considers the current one for this installation.
historyJsonPath.Clear();
if (GetHistoryJsonPath(riderPath, historyJsonPath) case .Ok)
{
RiderVersion lastBuildVersion = GetLastBuildVersion(historyJsonPath);
if (lastBuildVersion.IsInitialized && installInfo.Version != lastBuildVersion)
{
delete installInfo;
continue;
}
}
outInstalls.Add(installInfo);
}
}
/// Recursively searches the given directory for files with the given name.
/// @remarks corlib's Directory.EnumerateFiles doesn't search subdirectories.
protected static void FindFilesRecursive(StringView directory, StringView fileName, List<String> outPaths,
int remainingDepth, ref int searchedDirectories)
{
if (remainingDepth <= 0 || searchedDirectories >= MaxSearchedDirectories)
return;
searchedDirectories++;
for (FileFindEntry entry in Directory.EnumerateFiles(directory, fileName))
{
String path = new String();
entry.GetFilePath(path);
outPaths.Add(path);
}
// Reused across the iterations: a "scope" allocation inside a loop body lives until the
// enclosing method returns, so allocating per iteration would grow the stack unboundedly.
String entryName = scope .();
String subDirectory = scope .();
for (FileFindEntry entry in Directory.EnumerateDirectories(directory))
{
entryName.Clear();
entry.GetFileName(entryName);
if (entryName == "." || entryName == "..")
continue;
subDirectory.Clear();
entry.GetFilePath(subDirectory);
FindFilesRecursive(subDirectory, fileName, outPaths, remainingDepth - 1, ref searchedDirectories);
}
}
/// Reads the custom install location out of the Toolbox' ".settings.json".
protected static void ExtractPathFromSettingsJson(StringView toolboxPath, String outInstallLocation)
{
String settingsJsonPath = scope .();
Path.Combine(settingsJsonPath, toolboxPath, ".settings.json");
if (!File.Exists(settingsJsonPath))
return;
StructuredData data = scope .();
if (data.Load(settingsJsonPath) case .Err)
return;
data.GetString("install_location", outInstallLocation);
}
/// Reads the version of a Rider installation out of its "product-info.json".
protected static RiderVersion ParseProductInfoJson(StringView productInfoJsonPath)
{
StructuredData data = scope .();
if (data.Load(productInfoJsonPath) case .Err)
return .();
String buildNumber = scope .();
data.GetString("buildNumber", buildNumber);
return RiderVersion.Parse(buildNumber);
}
/// Walks up from the given Rider executable looking for the Toolbox' ".history.json".
protected static Result<void> GetHistoryJsonPath(StringView riderPath, String outPath)
{
String directory = scope .();
Try!(Path.GetDirectoryPath(riderPath, directory));
String parentDirectory = scope .();
String historyPath = scope .();
// Bounded so that we don't walk all the way up to the drive root.
for (int i < 10)
{
if (!Directory.Exists(directory))
break;
historyPath.Clear();
Path.Combine(historyPath, directory, ".history.json");
if (File.Exists(historyPath))
{
outPath.Append(historyPath);
return .Ok;
}
parentDirectory.Clear();
if (Path.GetDirectoryPath(directory, parentDirectory) case .Err)
break;
if (parentDirectory.IsEmpty || parentDirectory == directory)
break;
directory.Set(parentDirectory);
}
return .Err;
}
/// Reads the build number of the most recent entry of the Toolbox' ".history.json".
protected static RiderVersion GetLastBuildVersion(StringView historyJsonPath)
{
StructuredData data = scope .();
if (data.Load(historyJsonPath) case .Err)
return .();
String lastBuild = scope .();
// The most recent build is the last entry of the "history" array.
for (data.Enumerate("history"))
{
lastBuild.Clear();
using (data.Open("item"))
{
data.GetString("build", lastBuild);
}
}
return RiderVersion.Parse(lastBuild);
}
}
@@ -0,0 +1,235 @@
// Windows implementation of the RiderPathLocator, see RiderPathLocator.bf for the platform independent part and the license of the original code.
#if BF_PLATFORM_WINDOWS
using System;
using System.Collections;
using System.IO;
using System.Text;
namespace GlitchyEditor.CodeEditors;
extension RiderPathLocator
{
private const String RiderExecutableName = "rider64.exe";
/// The maximum length of a registry key name.
private const int MaxRegistryKeyNameLength = 255;
[Import("advapi32.lib"), CLink, CallingConvention(.Stdcall)]
private static extern int32 RegEnumKeyExW(Windows.HKey hKey, uint32 dwIndex, char16* lpName,
uint32* lpcchName, uint32* lpReserved, char16* lpClass, uint32* lpcchClass,
void* lpftLastWriteTime);
protected static override StringView GetRiderExecutableName() => RiderExecutableName;
public static override Result<RiderInstallInfo> GetInstallInfoFromRiderPath(StringView riderExePath, RiderInstallType installType)
{
if (!File.Exists(riderExePath))
return .Err;
String riderDirectory = scope .();
Try!(GetRiderDirectory(riderExePath, riderDirectory));
RiderVersion version = .();
String productInfoPath = scope .();
Path.Combine(productInfoPath, riderDirectory, "product-info.json");
if (File.Exists(productInfoPath))
version = ParseProductInfoJson(productInfoPath);
if (!version.IsInitialized)
{
// Toolbox installations are laid out as "<...>/<build number>/bin/rider64.exe",
// so the name of the installation directory is the version.
String directoryName = scope .();
Path.GetFileName(riderDirectory, directoryName);
version = RiderVersion.Parse(directoryName);
}
return new RiderInstallInfo(riderExePath, version, installType);
}
public static override void CollectAllPaths(List<RiderInstallInfo> outInstalls)
{
const String UninstallKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
const String UninstallKey32 = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
const String RiderKey = @"SOFTWARE\JetBrains\Rider";
const String RiderKey32 = @"SOFTWARE\WOW6432Node\JetBrains\Rider";
List<RiderInstallInfo> found = scope .();
// Everything that isn't moved into outInstalls (i.e. the duplicates) is deleted here.
defer { ClearAndDeleteItems!(found); }
// Regular installations register themselves in the uninstall list.
CollectPathsFromRegistry(Windows.HKEY_CURRENT_USER, UninstallKey, "InstallLocation", "Rider", found);
CollectPathsFromRegistry(Windows.HKEY_LOCAL_MACHINE, UninstallKey, "InstallLocation", "Rider", found);
CollectPathsFromRegistry(Windows.HKEY_CURRENT_USER, UninstallKey32, "InstallLocation", "Rider", found);
CollectPathsFromRegistry(Windows.HKEY_LOCAL_MACHINE, UninstallKey32, "InstallLocation", "Rider", found);
// dotUltimate installations use their own key.
CollectPathsFromRegistry(Windows.HKEY_CURRENT_USER, RiderKey, "InstallDir", "", found);
CollectPathsFromRegistry(Windows.HKEY_LOCAL_MACHINE, RiderKey, "InstallDir", "", found);
CollectPathsFromRegistry(Windows.HKEY_CURRENT_USER, RiderKey32, "InstallDir", "", found);
CollectPathsFromRegistry(Windows.HKEY_LOCAL_MACHINE, RiderKey32, "InstallDir", "", found);
// Toolbox installations aren't registered anywhere, we have to go looking for them.
String defaultToolboxPath = scope .();
if (GetToolboxPathFromEnvironment(defaultToolboxPath) case .Ok)
CollectPathsFromToolbox(defaultToolboxPath, found);
CollectPathsFromToolboxRegistry(Windows.HKEY_CURRENT_USER, @"Software\JetBrains\Toolbox\", found);
CollectPathsFromToolboxRegistry(Windows.HKEY_LOCAL_MACHINE, @"Software\JetBrains\Toolbox\", found);
CollectPathsFromToolboxRegistry(Windows.HKEY_CURRENT_USER, @"Software\JetBrains s.r.o.\JetBrainsToolbox\", found);
CollectPathsFromToolboxRegistry(Windows.HKEY_LOCAL_MACHINE, @"Software\JetBrains s.r.o.\JetBrainsToolbox\", found);
DeduplicateAndSort(found, outInstalls);
}
/// Enumerates the subkeys of the given registry key and creates an installation info for
/// every subkey that points at a Rider installation.
/// @param rootKey The registry hive to look in.
/// @param subKeyPath The key whose subkeys will be enumerated.
/// @param valueName The name of the value that holds the installation directory.
/// @param keyNameFilter If not empty, only subkeys whose name contains this string are looked at.
private static void CollectPathsFromRegistry(Windows.HKey rootKey, StringView subKeyPath,
StringView valueName, StringView keyNameFilter, List<RiderInstallInfo> outInstalls)
{
Windows.HKey key;
if (Windows.RegOpenKeyExW(rootKey, subKeyPath.ToScopedNativeWChar!(), 0, (.)Windows.KEY_READ, out key) != Windows.ERROR_SUCCESS)
return;
defer Windows.RegCloseKey(key);
List<String> subKeyNames = scope .();
defer { ClearAndDeleteItems!(subKeyNames); }
if (EnumerateSubKeys(key, subKeyNames) case .Err)
return;
// All of these are reused across the iterations: a "scope" allocation inside a loop body
// lives until the enclosing method returns, and the uninstall key has hundreds of subkeys.
char16[MaxRegistryKeyNameLength + 1] subKeyNameBuffer = ?;
String installLocation = scope .();
String exePath = scope .();
for (String subKeyName in subKeyNames)
{
if (!keyNameFilter.IsEmpty && !subKeyName.Contains(keyNameFilter))
continue;
int encodedLength = UTF16.GetEncodedLen(subKeyName);
if (encodedLength > MaxRegistryKeyNameLength)
continue;
UTF16.Encode(subKeyName, &subKeyNameBuffer, encodedLength).IgnoreError();
subKeyNameBuffer[encodedLength] = 0;
Windows.HKey subKey;
if (Windows.RegOpenKeyExW(key, &subKeyNameBuffer, 0, (.)Windows.KEY_READ, out subKey) != Windows.ERROR_SUCCESS)
continue;
installLocation.Clear();
Result<void> valueResult = subKey.GetValue(valueName, installLocation);
Windows.RegCloseKey(subKey);
if (valueResult case .Err)
continue;
if (installLocation.IsWhiteSpace)
continue;
exePath.Clear();
Path.Combine(exePath, installLocation, "bin", RiderExecutableName);
if (GetInstallInfoFromRiderPath(exePath, .Installed) case .Ok(let installInfo))
outInstalls.Add(installInfo);
}
}
/// Enumerates the names of all subkeys of the given registry key.
/// @remarks Windows.HKey.EnumValue enumerates values (and is unimplemented), there is no
/// counterpart for subkeys in corlib.
private static Result<void> EnumerateSubKeys(Windows.HKey key, List<String> outNames)
{
// Reused across the iterations, see the note in CollectPathsFromRegistry.
char16[MaxRegistryKeyNameLength + 1] nameBuffer = ?;
for (uint32 index = 0; true; index++)
{
uint32 nameLength = nameBuffer.Count;
int32 result = RegEnumKeyExW(key, index, &nameBuffer, &nameLength, null, null, null, null);
if (result == Windows.ERROR_NO_MORE_ITEMS)
break;
if (result != Windows.ERROR_SUCCESS)
return .Err;
// RegEnumKeyExW null-terminates, but be explicit about it since we reuse the buffer.
nameBuffer[Math.Min((int)nameLength, MaxRegistryKeyNameLength)] = 0;
outNames.Add(new String(&nameBuffer));
}
return .Ok;
}
/// Gets the default location of the JetBrains Toolbox ("%LOCALAPPDATA%/JetBrains/Toolbox").
private static Result<void> GetToolboxPathFromEnvironment(String outPath)
{
String localAppData = scope .();
Try!(Environment.GetEnvironmentVariable("LOCALAPPDATA", localAppData));
Path.Combine(outPath, localAppData, "JetBrains", "Toolbox");
return .Ok;
}
protected static override Result<void> GetDefaultIdeInstallLocationForToolboxV2(String outPath)
{
String localAppData = scope .();
Try!(Environment.GetEnvironmentVariable("LOCALAPPDATA", localAppData));
Path.Combine(outPath, localAppData, "Programs");
return .Ok;
}
/// Looks up the location of the JetBrains Toolbox in the registry and collects the Rider
/// installations it manages.
private static void CollectPathsFromToolboxRegistry(Windows.HKey rootKey, StringView subKeyPath, List<RiderInstallInfo> outInstalls)
{
Windows.HKey key;
if (Windows.RegOpenKeyExW(rootKey, subKeyPath.ToScopedNativeWChar!(), 0, (.)Windows.KEY_READ, out key) != Windows.ERROR_SUCCESS)
return;
defer Windows.RegCloseKey(key);
// The default value of the key contains the path of the Toolbox' "bin" directory.
String toolboxBinPath = scope .();
if (key.GetValue("", toolboxBinPath) case .Err)
return;
if (toolboxBinPath.IsWhiteSpace)
return;
String binDirectoryName = scope .();
Path.GetFileName(toolboxBinPath, binDirectoryName);
if (StringView.Compare(binDirectoryName, "bin", true) != 0)
return;
String toolboxPath = scope .();
if (Path.GetDirectoryPath(toolboxBinPath, toolboxPath) case .Err)
return;
CollectPathsFromToolbox(toolboxPath, outInstalls);
}
}
#endif
@@ -604,7 +604,13 @@ namespace GlitchyEditor.EditWindows
if (ImGui.MenuItem("Open C# Project...")) if (ImGui.MenuItem("Open C# Project..."))
{ {
RiderIdeAdapter.OpenScriptProject(); switch (Application.Instance.Settings.ScriptSettings.SelectedIde)
{
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.");
+64
View File
@@ -2,6 +2,7 @@ using System;
using GlitchyEngine; using GlitchyEngine;
using Bon; using Bon;
using GlitchyEditor; using GlitchyEditor;
using GlitchyEditor.CodeEditors;
using System.Collections; using System.Collections;
using System.IO; using System.IO;
using System.Diagnostics; using System.Diagnostics;
@@ -69,6 +70,7 @@ class ScriptSettings
public ScriptIde SelectedIde; public ScriptIde SelectedIde;
private bool _lookedForVs = false; private bool _lookedForVs = false;
private bool _lookedForRider = false;
public void Apply() public void Apply()
{ {
@@ -78,6 +80,11 @@ class ScriptSettings
FindVisualStudio(); FindVisualStudio();
} }
#endif #endif
if (!_lookedForRider && String.IsNullOrWhiteSpace(RiderPath))
{
FindRider();
}
} }
void FindVisualStudio() void FindVisualStudio()
@@ -154,6 +161,63 @@ class ScriptSettings
Log.EngineLogger.Error("Couldn't automatically determine location of Visual Studio: Failed to launch vswhere.exe"); 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] [Reflect]
+48 -10
View File
@@ -299,24 +299,62 @@ namespace GlitchyEditor
_settingsChanged = true; _settingsChanged = true;
} }
case when fieldType.IsEnum:
int64 underlyingValue = 0;
/*if (setting.EditorType == .FilePath) // Read the enum fields value from the settings-object
var enumValueResult = fieldInfo.GetValueReference(setting.SettingsObject);
if (enumValueResult not case .Ok(var enumValueReference))
break;
// Get underlying integer value from field reference
switch (fieldInfo.FieldType.Size)
{ {
ImGui.SameLine(); case 1: underlyingValue = *(int8*)enumValueReference.DataPtr;
case 2: underlyingValue = *(int16*)enumValueReference.DataPtr;
case 4: underlyingValue = *(int32*)enumValueReference.DataPtr;
case 8: underlyingValue = *(int64*)enumValueReference.DataPtr;
}
if (ImGui.Button("...")) // TODO: handle multiple Flags (as a separate enum type handler though!)
StringView currentName = "";
for (let (name, value) in Enum.GetEnumerator(fieldInfo.FieldType))
{
if (value == underlyingValue)
{ {
OpenFileDialog ofd = scope .(); currentName = name;
ofd.InitialDirectory = value; break;
ofd.SetFilter("All Files (*.*)|*.*"); }
ofd.Multiselect = false; }
if (ofd.ShowDialog() case .Ok(let result) && result == .OK) if (ImGui.BeginCombo(scope $"##{setting.Name}", currentName.ToScopeCStr!()))
{
for (let (name, value) in Enum.GetEnumerator(fieldInfo.FieldType))
{
bool isSelected = (name == currentName);
if (ImGui.Selectable(name.ToScopeCStr!(), isSelected))
{ {
SetSettingValue!(new String(ofd.FileNames[0])); // Set value in field reference
switch (fieldInfo.FieldType.Size)
{
case 1: *(int8*)enumValueReference.DataPtr = (int8)value;
case 2: *(int16*)enumValueReference.DataPtr = (int16)value;
case 4: *(int32*)enumValueReference.DataPtr = (int32)value;
case 8: *(int64*)enumValueReference.DataPtr = (int64)value;
}
if (!isSelected)
_settingsChanged = true;
} }
} }
}*/
ImGui.EndCombo();
}
} }
if (setting.Name == _SettingToHighlight) if (setting.Name == _SettingToHighlight)