Editor/AnyEditorMenu.cs
using Sandbox;
using Editor;
using System.IO;
using System.Linq;
using System;
using System.Collections.Generic;
using Microsoft.Win32;
namespace AnyEditor;
public static class AnyEditorMenu
{
[Menu( "Editor", "Any Editor/Configure Path..." )]
public static void ConfigurePath()
{
var fd = new FileDialog( null );
fd.Title = "Select Editor Executable";
fd.DefaultSuffix = ".exe";
fd.SetNameFilter( "Executable Files (*.exe)|*.exe|All Files (*.*)|*.*" );
if ( fd.Execute() )
{
AnyEditorConfig.ExePath = fd.SelectedFile;
// Default to just path when manually configuring
AnyEditorConfig.Arguments = "\"{path}\"";
AnyEditorConfig.AddRecent( fd.SelectedFile );
Log.Info( $"AnyEditor path set to: {fd.SelectedFile}" );
}
}
// Because i'm lazy
[Menu( "Editor", "Any Editor/Recent Editors..." )]
public static void ShowRecents()
{
new RecentEditorsDialog();
}
// You can add more here if you want, up to you - or just use recents.
// If path attribution is available, add it in to the -g args.
[Menu( "Editor", "Any Editor/Common/Set to Cursor" )]
public static void SetToCursor() => SetCommonEditor( "Cursor",
new[] { "Cursor/Cursor.exe", "cursor/cursor.exe" },
new[] { "Cursor.exe" },
"-g \"{path}:{line}:{column}\"" );
[Menu( "Editor", "Any Editor/Common/Set to VS Code" )]
public static void SetToVSCode() => SetCommonEditor( "VS Code",
new[] { "Microsoft VS Code/Code.exe" },
new[] { "Code.exe", "VSCode.exe" },
"-g \"{path}:{line}:{column}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Visual Studio 2026" )]
public static void SetToVS2026() => SetCommonEditorWildcard( "Visual Studio 2026", "Microsoft Visual Studio", "2026", "*/Common7/IDE/devenv.exe", null, "\"{path}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Visual Studio 2025" )]
public static void SetToVS2025() => SetCommonEditorWildcard( "Visual Studio 2025", "Microsoft Visual Studio", "2025", "*/Common7/IDE/devenv.exe", null, "\"{path}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Visual Studio 2022" )]
public static void SetToVS2022() => SetCommonEditorWildcard( "Visual Studio 2022", "Microsoft Visual Studio", "2022", "*/Common7/IDE/devenv.exe", null, "\"{path}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Visual Studio 2019" )]
public static void SetToVS2019() => SetCommonEditorWildcard( "Visual Studio 2019", "Microsoft Visual Studio", "2019", "*/Common7/IDE/devenv.exe", null, "\"{path}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Visual Studio (Latest)" )]
public static void SetToVSLatest() => SetCommonEditorWildcard( "Visual Studio (Latest)", "Microsoft Visual Studio", "*", "*/Common7/IDE/devenv.exe", "devenv.exe", "\"{path}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Rider" )]
public static void SetToRider() => SetCommonEditorWildcard( "Rider", "JetBrains", "JetBrains Rider*", "bin/rider64.exe", "rider64.exe", "\"{path}\" --line {line} --column {column}" );
[Menu( "Editor", "Any Editor/Common/Set to Zed" )]
public static void SetToZed() => SetCommonEditor( "Zed",
new[] { "Zed/zed.exe" },
new[] { "zed.exe" },
"\"{path}:{line}:{column}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Sublime Text" )]
public static void SetToSublime() => SetCommonEditor( "Sublime Text",
new[] { "Sublime Text/sublime_text.exe", "Sublime Text 3/sublime_text.exe" },
new[] { "sublime_text.exe" },
"\"{path}:{line}:{column}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Notepad++" )]
public static void SetToNotepadPlusPlus() => SetCommonEditor( "Notepad++",
new[] { "Notepad++/notepad++.exe" },
new[] { "notepad++.exe" },
"\"{path}\" -n{line} -c{column}" );
[Menu( "Editor", "Any Editor/Common/Set to Notepad" )]
public static void SetToNotepad() => SetCommonEditor( "Notepad",
new[] { "Windows/System32/notepad.exe", "Windows/notepad.exe" },
new[] { "notepad.exe" },
"\"{path}\"" );
[Menu( "Editor", "Any Editor/Common/Set to Google Antigravity" )]
public static void SetToAntigravity() => SetCommonEditor( "Google Antigravity", new string[] { }, new[] { "Antigravity.exe" }, "-g \"{path}:{line}:{column}\"" );
[Menu( "Editor", "Any Editor/Information" )]
public static void ShowInfo()
{
var msg = "Any Editor Library\n\n" +
"Allows you to use any executable as your code editor in S&Box.\n\n" +
"USAGE:\n" +
"1. Configure the path via 'Editor > Any Editor > Configure Path...' (or select a Common editor)\n" +
"2. Go to 'Edit > Preferences' and select 'Any Editor (Custom)' as your Code Editor.\n" +
" Note: You MUST select 'Any Editor (Custom)' in preferences for this to work.\n\n" +
"UNINSTALLATION:\n" +
"If you remove this library, you should manually:\n" +
"1. Delete 'anyeditor.config.json' from your project root.\n" +
"2. Remove the 'anyeditor.config.json' entry from your .gitignore file.\n\n" +
$"Current Configured Path: {AnyEditorConfig.ExePath}\n" +
$"Current Arguments: {AnyEditorConfig.Arguments}\n" +
$"Configuration File: {AnyEditorConfig.GetConfigFileLocation()}\n\n" +
"TROUBLESHOOTING:\n" +
"If this library spontaneously combusts after a S&Box update,\n" +
"simply uninstall it and pretend we never met. No hard feelings.\n\n" +
"CREDITS:\n" +
"Laoh\n"+
"Pixel VG for the inspiration from Cursor Editor (https://sbox.game/kolpak/cursoreditor) - check them out if you just use cursor and dont want the bloat";
EditorUtility.DisplayDialog( "Any Editor Information", msg );
}
// Set common editor with path attribution
private static void SetCommonEditor( string name, string[] relativePaths, string[] registryNames = null, string args = "\"{path}\"" )
{
string path = null;
if ( registryNames != null )
{
foreach ( var exeName in registryNames )
{
path = FindPathInRegistry( exeName );
if ( !string.IsNullOrEmpty( path ) && File.Exists( path ) ) break;
}
}
if ( string.IsNullOrEmpty( path ) )
{
foreach( var p in relativePaths )
{
path = FindEditorPath( p );
if ( !string.IsNullOrEmpty( path ) ) break;
}
}
if ( !string.IsNullOrEmpty( path ) )
{
AnyEditorConfig.ExePath = path;
AnyEditorConfig.Arguments = args;
AnyEditorConfig.AddRecent( path );
Log.Info( $"AnyEditor path set to {name}: {path}" );
EditorUtility.DisplayDialog( "Success", $"AnyEditor path set to {name}!\n\nRemember to select 'Any Editor (Custom)' in Edit > Preferences." );
}
else
{
Log.Warning( $"Could not find {name} - either default path is cooked or registry is incorrect, you can fix this by configuring the path manually via 'Editor > Any Editor > Configure Path...' or updating the path in AnyEditorMenu.cs" );
EditorUtility.DisplayDialog( "Not Found", $"Could not find {name} installation automatically.\nPlease configure the path manually. See Console." );
}
}
private static void SetCommonEditorWildcard( string name, string baseFolder, string dirPattern, string relativeFilePattern, string registryName = null, string args = "\"{path}\"" )
{
string path = null;
if ( !string.IsNullOrEmpty( registryName ) )
{
path = FindPathInRegistry( registryName );
}
if ( string.IsNullOrEmpty( path ) || !File.Exists( path ) )
{
path = FindEditorPathWildcard( baseFolder, dirPattern, relativeFilePattern );
}
if ( !string.IsNullOrEmpty( path ) )
{
AnyEditorConfig.ExePath = path;
AnyEditorConfig.Arguments = args;
AnyEditorConfig.AddRecent( path );
Log.Info( $"AnyEditor path set to {name}: {path}" );
EditorUtility.DisplayDialog( "Success", $"AnyEditor path set to {name}!\n\nRemember to select 'Any Editor (Custom)' in Edit > Preferences." );
}
else
{
Log.Warning( $"Could not find {name} - either default path is cooked or registry is incorrect, you can fix this by configuring the path manually via 'Editor > Any Editor > Configure Path...' or updating the path in AnyEditorMenu.cs" );
EditorUtility.DisplayDialog( "Not Found", $"Could not find {name} installation automatically.\nPlease configure the path manually. See Console." );
}
}
private static string FindPathInRegistry( string exeName )
{
try
{
// Some apps register here, so hardcoding it is
string keyPath = $@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{exeName}";
using ( var key = Registry.LocalMachine.OpenSubKey( keyPath ) )
{
var val = key?.GetValue( "" ) as string;
if ( !string.IsNullOrEmpty( val ) ) return val;
}
using ( var key = Registry.CurrentUser.OpenSubKey( keyPath ) )
{
var val = key?.GetValue( "" ) as string;
if ( !string.IsNullOrEmpty( val ) ) return val;
}
// Fallback: Also in here, infact most likely in here
string keyPathClasses = $@"Applications\{exeName}\shell\open\command";
using ( var key = Registry.ClassesRoot.OpenSubKey( keyPathClasses ) )
{
var val = key?.GetValue( "" ) as string;
if ( !string.IsNullOrEmpty( val ) )
{
var split = val.Split( '"' );
foreach( var s in split )
{
if ( s.EndsWith( ".exe", StringComparison.OrdinalIgnoreCase ) && File.Exists( s ) )
{
return s;
}
}
if ( File.Exists( val ) ) return val;
}
}
}
catch { }
return null;
}
private static string FindEditorPath( string relativePath )
{
// Also check System directory for things like notepad
var systemPath = Environment.GetFolderPath( Environment.SpecialFolder.System );
if ( File.Exists( Path.Combine( systemPath, Path.GetFileName( relativePath ) ) ) )
return Path.Combine( systemPath, Path.GetFileName( relativePath ) );
var paths = new[] {
System.Environment.GetFolderPath( System.Environment.SpecialFolder.LocalApplicationData ),
System.Environment.GetFolderPath( System.Environment.SpecialFolder.ProgramFiles ),
System.Environment.GetFolderPath( System.Environment.SpecialFolder.ProgramFilesX86 ),
System.Environment.GetFolderPath( System.Environment.SpecialFolder.Windows )
};
foreach ( var root in paths )
{
var path = Path.Combine( root, relativePath );
if ( File.Exists( path ) ) return path;
}
return null;
}
private static string FindEditorPathWildcard( string baseFolder, string dirPattern, string relativeFilePattern )
{
// Basically if registry check fucks up, we'll just try the common places
var roots = new[] {
System.Environment.GetFolderPath( System.Environment.SpecialFolder.LocalApplicationData ),
System.Environment.GetFolderPath( System.Environment.SpecialFolder.ProgramFiles ),
System.Environment.GetFolderPath( System.Environment.SpecialFolder.ProgramFilesX86 )
};
foreach ( var root in roots )
{
var basePath = Path.Combine( root, baseFolder );
if ( !Directory.Exists( basePath ) ) continue;
try
{
var dirs = Directory.GetDirectories( basePath, dirPattern );
Array.Sort( dirs );
Array.Reverse( dirs );
foreach ( var dir in dirs )
{
if ( !relativeFilePattern.Contains( "*" ) )
{
var fullPath = Path.Combine( dir, relativeFilePattern );
if ( File.Exists( fullPath ) ) return fullPath;
}
else
{
var subParts = relativeFilePattern.Split( '/' );
if ( subParts[0] == "*" )
{
var subPattern = string.Join( Path.DirectorySeparatorChar, subParts.Skip( 1 ) );
var subDirs = Directory.GetDirectories( dir );
foreach ( var subDir in subDirs )
{
var finalPath = Path.Combine( subDir, subPattern );
if ( File.Exists( finalPath ) ) return finalPath;
}
}
}
}
}
catch { }
}
return null;
}
}