diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce0d8c..b67c2f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Code Editor Package for Cursor +## [2.0.27] - 2025-11-02 + +Integration: + +- Add Multiple or Single Cursor Instance Options + ## [2.0.26] - 2025-11-02 Integration: @@ -54,7 +60,7 @@ Project generation: Integration: - Performance improvements with `EditorApplication.update` callbacks. - + Project generation: - Add extra compiler options for analyzers and source generators. @@ -77,7 +83,7 @@ Project generation: Integration: - Prevent ADB Refresh while being in safe-mode with a URP project -- Fixed an issue keeping the progress bar visible even after opening a script with Visual Studio. +- Fixed an issue keeping the progress bar visible even after opening a script with Visual Studio. ## [2.0.15] - 2022-03-21 diff --git a/Editor/ProcessRunner.cs b/Editor/ProcessRunner.cs index e425378..29ace73 100644 --- a/Editor/ProcessRunner.cs +++ b/Editor/ProcessRunner.cs @@ -135,8 +135,6 @@ namespace Microsoft.Unity.VisualStudio.Editor cursorStoragePath = Path.Combine(userProfile, "AppData", "Roaming", "cursor", "User", "workspaceStorage"); #endif - Debug.Log($"[Cursor] Looking for workspaces in: {cursorStoragePath}"); - if (Directory.Exists(cursorStoragePath)) { foreach (var workspaceDir in Directory.GetDirectories(cursorStoragePath)) diff --git a/Editor/VisualStudioCursorInstallation.cs b/Editor/VisualStudioCursorInstallation.cs index 988ef54..e27f87c 100644 --- a/Editor/VisualStudioCursorInstallation.cs +++ b/Editor/VisualStudioCursorInstallation.cs @@ -10,27 +10,36 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using UnityEngine; +using UnityEditor; using SimpleJSON; using IOPath = System.IO.Path; using Debug = UnityEngine.Debug; -namespace Microsoft.Unity.VisualStudio.Editor { - internal class VisualStudioCursorInstallation : VisualStudioInstallation { +namespace Microsoft.Unity.VisualStudio.Editor +{ + internal class VisualStudioCursorInstallation : VisualStudioInstallation + { private static readonly IGenerator _generator = new SdkStyleProjectGeneration(); + internal const string ReuseExistingWindowKey = "cursor_reuse_existing_window"; - public override bool SupportsAnalyzers { - get { + public override bool SupportsAnalyzers + { + get + { return true; } } - public override Version LatestLanguageVersionSupported { - get { + public override Version LatestLanguageVersionSupported + { + get + { return new Version(11, 0); } } - private string GetExtensionPath() { + private string GetExtensionPath() + { var vscode = IsPrerelease ? ".vscode-insiders" : ".vscode"; var extensionsPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), vscode, "extensions"); if (!Directory.Exists(extensionsPath)) @@ -42,7 +51,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { .FirstOrDefault(); } - public override string[] GetAnalyzers() { + public override string[] GetAnalyzers() + { var vstuPath = GetExtensionPath(); if (string.IsNullOrEmpty(vstuPath)) return Array.Empty(); @@ -50,13 +60,16 @@ namespace Microsoft.Unity.VisualStudio.Editor { return GetAnalyzers(vstuPath); } - public override IGenerator ProjectGenerator { - get { + public override IGenerator ProjectGenerator + { + get + { return _generator; } } - private static bool IsCandidateForDiscovery(string path) { + private static bool IsCandidateForDiscovery(string path) + { #if UNITY_EDITOR_OSX return Directory.Exists(path) && Regex.IsMatch(path, ".*Cursor.*.app$", RegexOptions.IgnoreCase); #elif UNITY_EDITOR_WIN @@ -67,12 +80,14 @@ namespace Microsoft.Unity.VisualStudio.Editor { } [Serializable] - internal class VisualStudioCodeManifest { + internal class VisualStudioCodeManifest + { public string name; public string version; } - public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation) { + public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation) + { installation = null; if (string.IsNullOrEmpty(editorPath)) @@ -84,7 +99,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { Version version = null; var isPrerelease = false; - try { + try + { var manifestBase = GetRealPath(editorPath); #if UNITY_EDITOR_WIN @@ -104,17 +120,21 @@ namespace Microsoft.Unity.VisualStudio.Editor { return false; var manifestFullPath = IOPath.Combine(manifestBase, "resources", "app", "package.json"); - if (File.Exists(manifestFullPath)) { + if (File.Exists(manifestFullPath)) + { var manifest = JsonUtility.FromJson(File.ReadAllText(manifestFullPath)); Version.TryParse(manifest.version.Split('-').First(), out version); isPrerelease = manifest.version.ToLower().Contains("insider"); } - } catch (Exception) { + } + catch (Exception) + { // do not fail if we are not able to retrieve the exact version number } isPrerelease = isPrerelease || editorPath.ToLower().Contains("insider"); - installation = new VisualStudioCursorInstallation() { + installation = new VisualStudioCursorInstallation() + { IsPrerelease = isPrerelease, Name = "Cursor" + (isPrerelease ? " - Insider" : string.Empty) + (version != null ? $" [{version.ToString(3)}]" : string.Empty), Path = editorPath, @@ -124,7 +144,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { return true; } - public static IEnumerable GetVisualStudioInstallations() { + public static IEnumerable GetVisualStudioInstallations() + { var candidates = new List(); #if UNITY_EDITOR_WIN @@ -147,7 +168,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { candidates.AddRange(GetXdgCandidates()); #endif - foreach (var candidate in candidates.Distinct()) { + foreach (var candidate in candidates.Distinct()) + { if (TryDiscoverInstallation(candidate, out var installation)) yield return installation; } @@ -172,7 +194,7 @@ namespace Microsoft.Unity.VisualStudio.Editor { var desktopFile = IOPath.Combine(dir, "applications/code.desktop"); if (!File.Exists(desktopFile)) continue; - + var content = File.ReadAllText(desktopFile); match = DesktopFileExecEntry.Match(content); } @@ -202,13 +224,16 @@ namespace Microsoft.Unity.VisualStudio.Editor { return new String(cbuf, 0, chars); } #else - internal static string GetRealPath(string path) { + internal static string GetRealPath(string path) + { return path; } #endif - public override void CreateExtraFiles(string projectDirectory) { - try { + public override void CreateExtraFiles(string projectDirectory) + { + try + { var vscodeDirectory = IOPath.Combine(projectDirectory.NormalizePathSeparators(), ".vscode"); Directory.CreateDirectory(vscodeDirectory); @@ -217,7 +242,9 @@ namespace Microsoft.Unity.VisualStudio.Editor { CreateRecommendedExtensionsFile(vscodeDirectory, enablePatch); CreateSettingsFile(vscodeDirectory, enablePatch); CreateLaunchFile(vscodeDirectory, enablePatch); - } catch (IOException) { + } + catch (IOException) + { } } @@ -232,9 +259,11 @@ namespace Microsoft.Unity.VisualStudio.Editor { ] }"; - private static void CreateLaunchFile(string vscodeDirectory, bool enablePatch) { + private static void CreateLaunchFile(string vscodeDirectory, bool enablePatch) + { var launchFile = IOPath.Combine(vscodeDirectory, "launch.json"); - if (File.Exists(launchFile)) { + if (File.Exists(launchFile)) + { if (enablePatch) PatchLaunchFile(launchFile); @@ -244,8 +273,10 @@ namespace Microsoft.Unity.VisualStudio.Editor { File.WriteAllText(launchFile, DefaultLaunchFileContent); } - private static void PatchLaunchFile(string launchFile) { - try { + private static void PatchLaunchFile(string launchFile) + { + try + { const string configurationsKey = "configurations"; const string typeKey = "type"; @@ -253,7 +284,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { var launch = JSONNode.Parse(content); var configurations = launch[configurationsKey] as JSONArray; - if (configurations == null) { + if (configurations == null) + { configurations = new JSONArray(); launch.Add(configurationsKey, configurations); } @@ -265,14 +297,18 @@ namespace Microsoft.Unity.VisualStudio.Editor { configurations.Add(defaultContent[configurationsKey][0]); WriteAllTextFromJObject(launchFile, launch); - } catch (Exception) { + } + catch (Exception) + { // do not fail if we cannot patch the launch.json file } } - private void CreateSettingsFile(string vscodeDirectory, bool enablePatch) { + private void CreateSettingsFile(string vscodeDirectory, bool enablePatch) + { var settingsFile = IOPath.Combine(vscodeDirectory, "settings.json"); - if (File.Exists(settingsFile)) { + if (File.Exists(settingsFile)) + { if (enablePatch) PatchSettingsFile(settingsFile); @@ -345,8 +381,10 @@ namespace Microsoft.Unity.VisualStudio.Editor { File.WriteAllText(settingsFile, content); } - private void PatchSettingsFile(string settingsFile) { - try { + private void PatchSettingsFile(string settingsFile) + { + try + { const string excludesKey = "files.exclude"; const string solutionKey = "dotnet.defaultSolution"; @@ -361,7 +399,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { var patched = false; // Remove files.exclude for solution+project files in the project root - foreach (var exclude in excludes) { + foreach (var exclude in excludes) + { if (!bool.TryParse(exclude.Value, out var exc) || !exc) continue; @@ -380,7 +419,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { // Check default solution var defaultSolution = settings[solutionKey]; var solutionFile = IOPath.GetFileName(ProjectGenerator.SolutionFile()); - if (defaultSolution == null || defaultSolution.Value != solutionFile) { + if (defaultSolution == null || defaultSolution.Value != solutionFile) + { settings[solutionKey] = solutionFile; patched = true; } @@ -392,7 +432,9 @@ namespace Microsoft.Unity.VisualStudio.Editor { excludes.Remove(patch); WriteAllTextFromJObject(settingsFile, settings); - } catch (Exception) { + } + catch (Exception) + { // do not fail if we cannot patch the settings.json file } } @@ -405,10 +447,12 @@ namespace Microsoft.Unity.VisualStudio.Editor { } "; - private static void CreateRecommendedExtensionsFile(string vscodeDirectory, bool enablePatch) { + private static void CreateRecommendedExtensionsFile(string vscodeDirectory, bool enablePatch) + { // see https://tattoocoder.com/recommending-vscode-extensions-within-your-open-source-projects/ var extensionFile = IOPath.Combine(vscodeDirectory, "extensions.json"); - if (File.Exists(extensionFile)) { + if (File.Exists(extensionFile)) + { if (enablePatch) PatchRecommendedExtensionsFile(extensionFile); @@ -418,15 +462,18 @@ namespace Microsoft.Unity.VisualStudio.Editor { File.WriteAllText(extensionFile, DefaultRecommendedExtensionsContent); } - private static void PatchRecommendedExtensionsFile(string extensionFile) { - try { + private static void PatchRecommendedExtensionsFile(string extensionFile) + { + try + { const string recommendationsKey = "recommendations"; var content = File.ReadAllText(extensionFile); var extensions = JSONNode.Parse(content); var recommendations = extensions[recommendationsKey] as JSONArray; - if (recommendations == null) { + if (recommendations == null) + { recommendations = new JSONArray(); extensions.Add(recommendationsKey, recommendations); } @@ -436,33 +483,39 @@ namespace Microsoft.Unity.VisualStudio.Editor { recommendations.Add(MicrosoftUnityExtensionId); WriteAllTextFromJObject(extensionFile, extensions); - } catch (Exception) { + } + catch (Exception) + { // do not fail if we cannot patch the extensions.json file } } - private static void WriteAllTextFromJObject(string file, JSONNode node) { + private static void WriteAllTextFromJObject(string file, JSONNode node) + { using (var fs = File.Open(file, FileMode.Create)) - using (var sw = new StreamWriter(fs)) { + using (var sw = new StreamWriter(fs)) + { // Keep formatting/indent in sync with default contents sw.Write(node.ToString(aIndent: 4)); } } - private Process FindRunningCursorWithSolution(string solutionPath) { + private Process FindRunningCursorWithSolution(string solutionPath) + { var normalizedTargetPath = solutionPath.Replace('\\', '/').TrimEnd('/').ToLowerInvariant(); - + #if UNITY_EDITOR_WIN // Keep as is for Windows platform since path already includes drive letter #else // Ensure path starts with / for macOS and Linux platforms - if (!normalizedTargetPath.StartsWith("/")) { + if (!normalizedTargetPath.StartsWith("/")) + { normalizedTargetPath = "/" + normalizedTargetPath; } #endif - + var processes = new List(); - + // Get process name list based on different operating systems #if UNITY_EDITOR_OSX processes.AddRange(Process.GetProcessesByName("Cursor")); @@ -473,19 +526,24 @@ namespace Microsoft.Unity.VisualStudio.Editor { #else processes.AddRange(Process.GetProcessesByName("cursor")); #endif - - foreach (var process in processes) { - try { + + foreach (var process in processes) + { + try + { var workspaces = ProcessRunner.GetProcessWorkspaces(process); - if (workspaces != null && workspaces.Length > 0) { - foreach (var workspace in workspaces) { + if (workspaces != null && workspaces.Length > 0) + { + foreach (var workspace in workspaces) + { var normalizedWorkspaceDir = workspace.Replace('\\', '/').TrimEnd('/').ToLowerInvariant(); - + #if UNITY_EDITOR_WIN // Keep as is for Windows platform #else // Ensure path starts with / for macOS and Linux platforms - if (!normalizedWorkspaceDir.StartsWith("/")) { + if (!normalizedWorkspaceDir.StartsWith("/")) + { normalizedWorkspaceDir = "/" + normalizedWorkspaceDir; } #endif @@ -499,7 +557,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { } } } - catch (Exception ex) { + catch (Exception ex) + { Debug.LogError($"[Cursor] Error checking process: {ex}"); continue; } @@ -516,7 +575,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { return files[0]; } - public override bool Open(string path, int line, int column, string solution) { + public override bool Open(string path, int line, int column, string solution) + { line = Math.Max(1, line); column = Math.Max(0, column); @@ -526,31 +586,38 @@ namespace Microsoft.Unity.VisualStudio.Editor { var workspace = TryFindWorkspace(directory); workspace ??= directory; directory = workspace; - - var existingProcess = FindRunningCursorWithSolution(directory); - if (existingProcess != null) { - try { - var args = string.IsNullOrEmpty(path) ? - $"--reuse-window \"{directory}\"" : - $"--reuse-window -g \"{path}\":{line}:{column}"; - - ProcessRunner.Start(ProcessStartInfoFor(application, args)); - return true; - } - catch (Exception ex) { - Debug.LogError($"[Cursor] Error using existing instance: {ex}"); + + if (EditorPrefs.GetBool(ReuseExistingWindowKey, false)) + { + var existingProcess = FindRunningCursorWithSolution(directory); + if (existingProcess != null) + { + try + { + var args = string.IsNullOrEmpty(path) ? + $"--reuse-window \"{directory}\"" : + $"--reuse-window -g \"{path}\":{line}:{column}"; + + ProcessRunner.Start(ProcessStartInfoFor(application, args)); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[Cursor] Error using existing instance: {ex}"); + } } } var newArgs = string.IsNullOrEmpty(path) ? $"--new-window \"{directory}\"" : $"--new-window \"{directory}\" -g \"{path}\":{line}:{column}"; - + ProcessRunner.Start(ProcessStartInfoFor(application, newArgs)); return true; } - private static ProcessStartInfo ProcessStartInfoFor(string application, string arguments) { + private static ProcessStartInfo ProcessStartInfoFor(string application, string arguments) + { #if UNITY_EDITOR_OSX // wrap with built-in OSX open feature arguments = $"-n \"{application}\" --args {arguments}"; @@ -561,7 +628,8 @@ namespace Microsoft.Unity.VisualStudio.Editor { #endif } - public static void Initialize() { + public static void Initialize() + { } } } diff --git a/Editor/VisualStudioEditor.cs b/Editor/VisualStudioEditor.cs index ed687c6..c3129fb 100644 --- a/Editor/VisualStudioEditor.cs +++ b/Editor/VisualStudioEditor.cs @@ -132,6 +132,16 @@ namespace Microsoft.Unity.VisualStudio.Editor GUILayout.Label($"{package.displayName} v{package.version} enabled", style); GUILayout.EndHorizontal(); + if (installation is VisualStudioCursorInstallation) + { + var reuseWindow = EditorPrefs.GetBool(VisualStudioCursorInstallation.ReuseExistingWindowKey, false); + var newReuseWindow = EditorGUILayout.Toggle(new GUIContent("Reuse existing Cursor window", "When enabled, opens files in an existing Cursor window if found. When disabled, always opens a new window."), reuseWindow); + if (newReuseWindow != reuseWindow) + EditorPrefs.SetBool(VisualStudioCursorInstallation.ReuseExistingWindowKey, newReuseWindow); + + EditorGUILayout.Space(); + } + EditorGUILayout.LabelField("Generate .csproj files for:"); EditorGUI.indentLevel++; SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "", installation); diff --git a/package.json b/package.json index 393432a..3b5e23c 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,13 @@ "name": "com.boxqkrtm.ide.cursor", "displayName": "Cursor Editor", "description": "Cursor editor integration for supporting Cursor as code editor for unity. Adds support for generating csproj files for intellisense purposes, auto discovery of installations, etc.", - "version": "2.0.26", + "version": "2.0.27", "unity": "2019.4", "unityRelease": "25f1", "dependencies": { "com.unity.test-framework": "1.1.9" }, "_upm": { - "changelog": "Integration:\n\n- Add workspace support" + "changelog": "Integration:\n\n- Add Multiple or Single Cursor Instance Options" } }