diff --git a/GlitchyEditor/src/CodeEditors/RiderPathLocator/LICENSE-RiderPathLocator.txt b/GlitchyEditor/src/CodeEditors/RiderPathLocator/LICENSE-RiderPathLocator.txt new file mode 100644 index 0000000..f4d93eb --- /dev/null +++ b/GlitchyEditor/src/CodeEditors/RiderPathLocator/LICENSE-RiderPathLocator.txt @@ -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. diff --git a/GlitchyEditor/src/CodeEditors/RiderPathLocator/RiderPathLocator.bf b/GlitchyEditor/src/CodeEditors/RiderPathLocator/RiderPathLocator.bf new file mode 100644 index 0000000..0f2dafe --- /dev/null +++ b/GlitchyEditor/src/CodeEditors/RiderPathLocator/RiderPathLocator.bf @@ -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(""); + 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 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 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 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 found, List 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. "/bin/rider64.exe". + protected static Result 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 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 outInstalls) + { + if (toolboxRiderRootPath.IsEmpty || !Directory.Exists(toolboxRiderRootPath)) + return; + + List 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 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 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); + } +} diff --git a/GlitchyEditor/src/CodeEditors/RiderPathLocator/RiderPathLocatorWindows.bf b/GlitchyEditor/src/CodeEditors/RiderPathLocator/RiderPathLocatorWindows.bf new file mode 100644 index 0000000..6097016 --- /dev/null +++ b/GlitchyEditor/src/CodeEditors/RiderPathLocator/RiderPathLocatorWindows.bf @@ -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 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 "<...>//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 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 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 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 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 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 EnumerateSubKeys(Windows.HKey key, List 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 GetToolboxPathFromEnvironment(String outPath) + { + String localAppData = scope .(); + Try!(Environment.GetEnvironmentVariable("LOCALAPPDATA", localAppData)); + + Path.Combine(outPath, localAppData, "JetBrains", "Toolbox"); + + return .Ok; + } + + protected static override Result 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 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 diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index 94e0f38..76a18e3 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -604,7 +604,13 @@ namespace GlitchyEditor.EditWindows 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."); diff --git a/GlitchyEditor/src/EditorSettings.bf b/GlitchyEditor/src/EditorSettings.bf index f9c2148..94be3e5 100644 --- a/GlitchyEditor/src/EditorSettings.bf +++ b/GlitchyEditor/src/EditorSettings.bf @@ -2,6 +2,7 @@ using System; using GlitchyEngine; using Bon; using GlitchyEditor; +using GlitchyEditor.CodeEditors; using System.Collections; using System.IO; using System.Diagnostics; @@ -69,6 +70,7 @@ class ScriptSettings public ScriptIde SelectedIde; private bool _lookedForVs = false; + private bool _lookedForRider = false; public void Apply() { @@ -78,6 +80,11 @@ class ScriptSettings FindVisualStudio(); } #endif + + if (!_lookedForRider && String.IsNullOrWhiteSpace(RiderPath)) + { + FindRider(); + } } void FindVisualStudio() @@ -154,6 +161,63 @@ class ScriptSettings 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 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 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] diff --git a/GlitchyEditor/src/SettingsWindow.bf b/GlitchyEditor/src/SettingsWindow.bf index a6dbc1d..ca6a1e1 100644 --- a/GlitchyEditor/src/SettingsWindow.bf +++ b/GlitchyEditor/src/SettingsWindow.bf @@ -299,24 +299,62 @@ namespace GlitchyEditor _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 .(); - ofd.InitialDirectory = value; - ofd.SetFilter("All Files (*.*)|*.*"); - ofd.Multiselect = false; + currentName = name; + break; + } + } - 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)