From 4d36aa9b0ca61e2d25fa2a142d1df5ff43ae67c7 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Wed, 5 Nov 2025 09:07:15 +0100 Subject: [PATCH 01/21] zed discovery on windows & null checks if repo is not used as Packet --- Editor/ZedDiscovery.cs | 37 ++++++++++++++++++++++++++++++++++++- Editor/ZedPreferences.cs | 10 +++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Editor/ZedDiscovery.cs b/Editor/ZedDiscovery.cs index eceb725..c6d39b9 100644 --- a/Editor/ZedDiscovery.cs +++ b/Editor/ZedDiscovery.cs @@ -21,7 +21,7 @@ public CodeEditor.Installation[] GetInstallations() // [Linux] (Flatpak) ("/var/lib/flatpak/app/dev.zed.Zed/current/active/files/bin/zed", null), - // [Linux] (Repo) + // [Linux] (Repo) ("/usr/bin/zeditor", null), // [Linux] (NixOS) @@ -35,6 +35,41 @@ public CodeEditor.Installation[] GetInstallations() (NPath.HomeDirectory.Combine(".local/bin/zed"), null), }; +#if UNITY_EDITOR_WIN + // [Windows] Default install locations + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + var windowsCandidates = new[] + { + Path.Combine(programFiles, "Zed", "zed.exe"), + Path.Combine(programFilesX86, "Zed", "zed.exe"), + Path.Combine(localAppData, "Programs", "Zed", "zed.exe"), // common for user-level installs + Path.Combine(localAppData, "Zed", "zed.exe") + }; + + foreach (var winPath in windowsCandidates) + { + candidates.Add((winPath, null)); + } + + // [Windows] Check if 'zed.exe' is in PATH + var pathEnv = Environment.GetEnvironmentVariable("PATH"); + if (!string.IsNullOrEmpty(pathEnv)) + { + var pathDirs = pathEnv.Split(Path.PathSeparator); + foreach (var dir in pathDirs) + { + var exePath = Path.Combine(dir.Trim(), "zed.exe"); + if (File.Exists(exePath)) + { + candidates.Add((exePath, null)); + } + } + } +#endif + foreach (var candidate in candidates) { var candidatePath = candidate.path; diff --git a/Editor/ZedPreferences.cs b/Editor/ZedPreferences.cs index a193f5b..20963a4 100644 --- a/Editor/ZedPreferences.cs +++ b/Editor/ZedPreferences.cs @@ -1,6 +1,7 @@ using UnityEngine; using UnityEditor; using Microsoft.Unity.VisualStudio.Editor; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; namespace UnityZed { @@ -18,7 +19,14 @@ public void OnGUI() GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); + var version = "unknown"; + var displayName = "Unity Zed"; var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly); + if (package != null) + { + version = package.version; + displayName = package.displayName; + } var style = new GUIStyle { @@ -26,7 +34,7 @@ public void OnGUI() margin = new RectOffset(0, 4, 0, 0) }; - GUILayout.Label($"{package.displayName} v{package.version} enabled", style); + GUILayout.Label($"{displayName} v{version} enabled", style); GUILayout.EndHorizontal(); EditorGUILayout.LabelField("Generate .csproj files for:"); From 4b66715a978801ed5de27000b3f59ecad4e43dba Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Wed, 5 Nov 2025 11:12:31 +0100 Subject: [PATCH 02/21] sintax fixes --- Editor/ZedDiscovery.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Editor/ZedDiscovery.cs b/Editor/ZedDiscovery.cs index c6d39b9..fab931a 100644 --- a/Editor/ZedDiscovery.cs +++ b/Editor/ZedDiscovery.cs @@ -1,5 +1,7 @@ +using System; using Unity.CodeEditor; using System.Collections.Generic; +using System.IO; using System.Xml.XPath; using System.Text; using NiceIO; @@ -12,7 +14,7 @@ public CodeEditor.Installation[] GetInstallations() { var results = new List(); - var candidates = new (NPath path, TryGetVersion tryGetVersion)[] { + var candidates = new List<(NPath path, TryGetVersion tryGetVersion)> { // [MacOS] ("/Applications/Zed.app/Contents/MacOS/cli", TryGetVersionFromPlist), From 73e87d4743b1baa31398148656dca48fbcaa06b8 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Wed, 4 Mar 2026 21:41:24 +0100 Subject: [PATCH 03/21] work --- Editor/.idea/.gitignore | 17 +++++++++++++++++ .../.idea/copilot.data.migration.ask2agent.xml | 6 ++++++ Editor/.idea/encodings.xml | 4 ++++ Editor/.idea/indexLayout.xml | 8 ++++++++ .../inspectionProfiles/Project_Default.xml | 6 ++++++ Editor/.idea/vcs.xml | 6 ++++++ Editor/ZedDiscovery.cs | 8 ++++---- Editor/ZedProcess.cs | 6 +++--- 8 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 Editor/.idea/.gitignore create mode 100644 Editor/.idea/copilot.data.migration.ask2agent.xml create mode 100644 Editor/.idea/encodings.xml create mode 100644 Editor/.idea/indexLayout.xml create mode 100644 Editor/.idea/inspectionProfiles/Project_Default.xml create mode 100644 Editor/.idea/vcs.xml diff --git a/Editor/.idea/.gitignore b/Editor/.idea/.gitignore new file mode 100644 index 0000000..dc983da --- /dev/null +++ b/Editor/.idea/.gitignore @@ -0,0 +1,17 @@ +# Default ignored files +/shelf/ +/workspace.xml + +# Rider ignored files +/modules.xml +/projectSettingsUpdater.xml +/contentModel.xml +/.idea.Editor.iml + +# Ignored default folder with query files +/queries/ + +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml + diff --git a/Editor/.idea/copilot.data.migration.ask2agent.xml b/Editor/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/Editor/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Editor/.idea/encodings.xml b/Editor/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/Editor/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Editor/.idea/indexLayout.xml b/Editor/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/Editor/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Editor/.idea/inspectionProfiles/Project_Default.xml b/Editor/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8b47716 --- /dev/null +++ b/Editor/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/Editor/.idea/vcs.xml b/Editor/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/Editor/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Editor/ZedDiscovery.cs b/Editor/ZedDiscovery.cs index fab931a..d64230c 100644 --- a/Editor/ZedDiscovery.cs +++ b/Editor/ZedDiscovery.cs @@ -45,10 +45,10 @@ public CodeEditor.Installation[] GetInstallations() var windowsCandidates = new[] { - Path.Combine(programFiles, "Zed", "zed.exe"), - Path.Combine(programFilesX86, "Zed", "zed.exe"), - Path.Combine(localAppData, "Programs", "Zed", "zed.exe"), // common for user-level installs - Path.Combine(localAppData, "Zed", "zed.exe") + Path.Combine(programFiles, "Zed", "bin", "zed.exe"), + Path.Combine(programFilesX86, "Zed", "bin", "zed.exe"), + Path.Combine(localAppData, "Programs", "Zed", "bin", "zed.exe"), // common for user-level installs + Path.Combine(localAppData, "Zed", "bin", "zed.exe") }; foreach (var winPath in windowsCandidates) diff --git a/Editor/ZedProcess.cs b/Editor/ZedProcess.cs index cdc77a9..ff1aeb3 100644 --- a/Editor/ZedProcess.cs +++ b/Editor/ZedProcess.cs @@ -23,13 +23,12 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1) sLogger.Log("OpenProject"); // always add project path - var args = new StringBuilder($"\"{m_ProjectPath}\""); + var args = new StringBuilder($"\"{m_ProjectPath}\" "); // if file path is provided, add it too if (!string.IsNullOrEmpty(filePath)) { - args.Append(" -a "); - args.Append($"\"{filePath}\""); + args.Append($"\"{filePath}"); if (line >= 0) { @@ -44,6 +43,7 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1) } } + Debug.Log(m_ExecPath + " " + args); return CodeEditor.OSOpenFile(m_ExecPath.ToString(), args.ToString()); } } From cd93508be0037723817f4d0531caacbb2313c0fb Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Wed, 4 Mar 2026 21:49:33 +0100 Subject: [PATCH 04/21] replaced CodeEditor.OSOpenFile with ProcessStartInfo --- Editor/ZedProcess.cs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Editor/ZedProcess.cs b/Editor/ZedProcess.cs index ff1aeb3..750120d 100644 --- a/Editor/ZedProcess.cs +++ b/Editor/ZedProcess.cs @@ -1,6 +1,6 @@ +using System.Diagnostics; using System.Text; using NiceIO; -using Unity.CodeEditor; using UnityEngine; namespace UnityZed @@ -43,8 +43,25 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1) } } - Debug.Log(m_ExecPath + " " + args); - return CodeEditor.OSOpenFile(m_ExecPath.ToString(), args.ToString()); + UnityEngine.Debug.Log(m_ExecPath + " " + args); + try + { + var startInfo = new ProcessStartInfo + { + FileName = m_ExecPath.ToString(), + Arguments = args.ToString(), + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + using (Process.Start(startInfo)) { } + return true; + } + catch (System.Exception ex) + { + sLogger.LogError("ZedProcess", $"Failed to start Zed: {ex.Message}"); + return false; + } } } } From 2e47db61fd3a10a1427e880c7070b26fbd1d44bf Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Wed, 4 Mar 2026 21:54:56 +0100 Subject: [PATCH 05/21] removed zedLogger --- Editor/ZedExternalCodeEditor.cs | 3 +-- Editor/ZedLogger.cs | 42 --------------------------------- Editor/ZedLogger.cs.meta | 2 -- Editor/ZedProcess.cs | 6 +---- Editor/ZedSettings.cs | 4 +--- 5 files changed, 3 insertions(+), 54 deletions(-) delete mode 100644 Editor/ZedLogger.cs delete mode 100644 Editor/ZedLogger.cs.meta diff --git a/Editor/ZedExternalCodeEditor.cs b/Editor/ZedExternalCodeEditor.cs index 6149185..e8c88e6 100644 --- a/Editor/ZedExternalCodeEditor.cs +++ b/Editor/ZedExternalCodeEditor.cs @@ -21,7 +21,6 @@ private static IGenerator CreateSdkStyleGeneration() return (IGenerator)Activator.CreateInstance(type); } - private static readonly ILogger sLogger = ZedLogger.Create(); private static readonly ZedDiscovery sDiscovery = new(); private ZedProcess m_Process; @@ -58,7 +57,7 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1) if (!string.IsNullOrEmpty(filePath) && !m_Generator.IsSupportedFile(filePath)) { - sLogger.Log($"File '{filePath}' is not supported by the generator."); + Debug.Log($"File '{filePath}' is not supported by the generator."); return false; } diff --git a/Editor/ZedLogger.cs b/Editor/ZedLogger.cs deleted file mode 100644 index 0780092..0000000 --- a/Editor/ZedLogger.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using UnityEngine; -using NiceIO; - -namespace UnityZed -{ - public class ZedLogger - { - public static ILogger Create([CallerFilePath] string filePath = null) - { - var path = new NPath(filePath); - var tag = path.FileNameWithoutExtension; - -#if UNITY_ZED_DEBUG - var handler = new LogHandler(tag, Debug.unityLogger.logHandler); -#else - var handler = new LogHandler(tag, null); -#endif - - return new Logger(handler); - } - - private class LogHandler : ILogHandler - { - private readonly ILogHandler m_LogHandler; - private readonly string m_Tag; - - public LogHandler(string tag, ILogHandler logHandler = null) - { - m_Tag = tag; - m_LogHandler = logHandler; - } - - public void LogException(Exception exception, UnityEngine.Object context) - => m_LogHandler?.LogException(exception, context); - - public void LogFormat(LogType logType, UnityEngine.Object context, string format, params object[] args) - => m_LogHandler?.LogFormat(logType, context, $"[{m_Tag}] {format}", args); - } - } -} diff --git a/Editor/ZedLogger.cs.meta b/Editor/ZedLogger.cs.meta deleted file mode 100644 index 7d84049..0000000 --- a/Editor/ZedLogger.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 151d40a511f47493db4a72bbd73de279 \ No newline at end of file diff --git a/Editor/ZedProcess.cs b/Editor/ZedProcess.cs index 750120d..b87eb44 100644 --- a/Editor/ZedProcess.cs +++ b/Editor/ZedProcess.cs @@ -7,8 +7,6 @@ namespace UnityZed { public class ZedProcess { - private static readonly ILogger sLogger = ZedLogger.Create(); - private readonly NPath m_ExecPath; private readonly NPath m_ProjectPath; @@ -20,8 +18,6 @@ public ZedProcess(string execPath) public bool OpenProject(string filePath = "", int line = -1, int column = -1) { - sLogger.Log("OpenProject"); - // always add project path var args = new StringBuilder($"\"{m_ProjectPath}\" "); @@ -59,7 +55,7 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1) } catch (System.Exception ex) { - sLogger.LogError("ZedProcess", $"Failed to start Zed: {ex.Message}"); + UnityEngine.Debug.LogError($"ZedProcess Failed to start Zed: {ex.Message}"); return false; } } diff --git a/Editor/ZedSettings.cs b/Editor/ZedSettings.cs index 88ddb89..09b64a5 100644 --- a/Editor/ZedSettings.cs +++ b/Editor/ZedSettings.cs @@ -7,8 +7,6 @@ namespace UnityZed { public class ZedSettings { - private static readonly ILogger sLogger = ZedLogger.Create(); - private readonly NPath m_SettingsPath; public ZedSettings() @@ -20,7 +18,7 @@ public void Sync() { if (m_SettingsPath.FileExists() == false) { - sLogger.Log("Zed settings file not found, creating default settings file."); + Debug.Log("Zed settings file not found, creating default settings file."); m_SettingsPath.CreateFile(); m_SettingsPath.WriteAllText(JSON.Parse(kDefaultSettings).ToString()); } From 989e92190bf40fb75444594c8c4b36f195c9f06c Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Wed, 4 Mar 2026 21:58:15 +0100 Subject: [PATCH 06/21] removed NiceIO --- Editor/NiceIO.cs | 2774 ---------------------------------------- Editor/NiceIO.cs.meta | 2 - Editor/ZedDiscovery.cs | 23 +- Editor/ZedProcess.cs | 12 +- Editor/ZedSettings.cs | 17 +- 5 files changed, 25 insertions(+), 2803 deletions(-) delete mode 100644 Editor/NiceIO.cs delete mode 100644 Editor/NiceIO.cs.meta diff --git a/Editor/NiceIO.cs b/Editor/NiceIO.cs deleted file mode 100644 index 3df0cb0..0000000 --- a/Editor/NiceIO.cs +++ /dev/null @@ -1,2774 +0,0 @@ -// The MIT License(MIT) -// ===================== -// -// Copyright © `2015-2017` `Lucas Meijer` -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the “Software”), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -using Microsoft.Win32.SafeHandles; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; -using System.IO; -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.Serialization; -using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; - -//Lets make it hard to accidentally use System.IO.File & System.IO.Directly, and require that it is always completely spelled out. -using File = NiceIO.Do_Not_Use_File_Directly_Use_FileSystem_Active_Instead; -using Directory = NiceIO.Do_Not_Use_Directory_Directly_Use_FileSystem_Active_Instead; - -namespace NiceIO -{ - /// - /// A filesystem path. - /// - /// - /// The path can be absolute or relative; the entity it refers to could be a file or a directory, and may or may not - /// actually exist in the filesystem. - /// - - //niceio gets included willy nilly by various projects, and they end up adding NPath to their public API without realizing it, which can lead to - //very annoying incompatibilities if you're using two projects that happen to do this. Let's make sure the default that it will not be public, - //and in the one or two places that we want it to be public be explicit about doing so - [DebuggerDisplay("{" + nameof(_path) + "}")] - [DataContract] -#if NICEIO_PUBLIC - public class NPath -#else - internal class NPath -#endif - : IComparable, IEquatable - { - // Assume FS is case sensitive on Linux, and case insensitive on macOS and Windows. - - //WARNING: Do not use FileSystem.Active to look for /proc. Since we're doing this in a static initializer, the k_IsWindows is not guaranteed to be set yet, - //and you can get very hard to track down situations where the currently active filesystem on windows is actually set to PosixFileSystem. - static readonly bool k_IsCaseSensitiveFileSystem = !CalculateIsWindows() && System.IO.Directory.Exists("/proc"); - - static readonly bool k_IsWindows = CalculateIsWindows(); - - private static bool CalculateIsWindows() => Environment.OSVersion.Platform == PlatformID.Win32Windows || Environment.OSVersion.Platform == PlatformID.Win32NT; - - private static bool CalculateIsWindows10() - { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) - return false; - - // Environment.OSVersion will only return versions higher than 6.2 if the owning process has been manifested as compatible with Windows 10: - // https://docs.microsoft.com/en-us/windows/win32/w8cookbook/windows-version-check - // - // Because NiceIO is a library, not its own executable, we are at the mercy of the consumer processes to manifest themselves correctly - and - // many of them don't. So, using Environment.OSVersion is not really safe. - // - // StackOverflow suggests using the file version info for a core OS file, such as kernel32.dll: - // https://stackoverflow.com/a/44665238/860530 - - var kernel32 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "kernel32.dll"); - var versionInfo = FileVersionInfo.GetVersionInfo(kernel32); - - return versionInfo.ProductMajorPart >= 10; - } - - static readonly StringComparison PathStringComparison = - k_IsCaseSensitiveFileSystem ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - - [DataMember] - private readonly string _path; - - static NPath Empty => new NPath(""); - - #region construction - - /// - /// Create a new NPath. - /// - /// The path that this NPath should represent. - public NPath(string path) - { - if (path == null) - throw new ArgumentNullException(); - _path = MakeCompletelyWellFormatted(path); - } - - //keep this private, we need to guarantee all NPath's out there are guaranteed well formed. - private NPath(string path, bool guaranteed_well_formed) - { - if (!guaranteed_well_formed) - throw new ArgumentException("For not well formed paths, use the public NPath constructor"); - - _path = path; - } - - static bool IsUNCPath(string path) - { - return path.Length > 2 && path[0] == '\\' && path[1] == '\\'; - } - - static string ConvertToForwardSlashPath(string path) - { - if (IsUNCPath(path)) // UNC path - return @"\\" + path.Substring(2).Replace(@"\", @"/"); - return path.Replace(@"\", @"/"); - } - - static string MakeCompletelyWellFormatted(string path, bool doubleDotsAreCollapsed = false) - { - if (path == ".") - return "."; - if (path.Length == 0) - return "."; - - var numberOfForwardSlashes = 0; - var hasNonDotsOrSeparators = false; - var startsWithDot = false; - char previousChar = '\0'; - for (int i = 0; i != path.Length; i++) - { - var c = path[i]; - var nextChar = path.Length > (i + 1) ? path[i + 1] : '\0'; - var isDot = c == '.'; - if (isDot && i == 0) - startsWithDot = true; - var isSlash = IsSlash(c); - hasNonDotsOrSeparators |= !isDot && !isSlash; - - // MakeCompletelyWellFormatted + CollapseDoubleDots is fairly expensive, so only do it when needed: - // If we have a "..", that is not just a bunch of "../.." in front of the path and nowhere else - // (these have nothing to collapse on anyway) - if (!doubleDotsAreCollapsed && (hasNonDotsOrSeparators || !startsWithDot) && isDot && - previousChar == '.') - return MakeCompletelyWellFormatted(CollapseDoubleDots(path), true); - - if (isDot && (IsSlash(previousChar) || previousChar == '\0') && (IsSlash(nextChar) || nextChar == '\0')) - return MakeCompletelyWellFormatted(CollapseSingleDots(path)); - - if (c == '\\' && (!IsUNCPath(path) || i >= 2)) - return MakeCompletelyWellFormatted(ConvertToForwardSlashPath(path)); - - if (c == '/' && previousChar == '/') - return MakeCompletelyWellFormatted(CollapseDoubleSlashes(path)); - - if (c == '/') - numberOfForwardSlashes++; - - previousChar = c; - } - - var lastChar = path[path.Length - 1]; - var secondToLastChar = path.Length >= 2 ? path[path.Length - 2] : '\0'; - - // Remove trailing (non-root significant slash) - if (lastChar == '/') - { - // this is a root path - if (secondToLastChar == '\0' || secondToLastChar == ':') - return path; - - if (numberOfForwardSlashes == 1 && IsUNCPath(path)) - return path; - - return path.Substring(0, path.Length - 1); - } - - if (numberOfForwardSlashes == 0 && IsUNCPath(path)) - return path + "/"; - - return path; - } - - static string CollapseSingleDots(string path) - { - var result = ConvertToForwardSlashPath(path).Replace("/./", "/"); - if (result.StartsWith("./", StringComparison.Ordinal)) - result = result.Substring(2); - if (result.EndsWith("/.", StringComparison.Ordinal)) - result = result.Substring(0, result.Length - 2); - return result; - } - - static string CollapseDoubleSlashes(string path) - { - return ConvertToForwardSlashPath(path).Replace("//", "/"); - } - - static string CollapseDoubleDots(string path) - { - path = ConvertToForwardSlashPath(path); - var isRegularRoot = path[0] == '/'; - var isRootWithDriveLetter = (path[1] == ':' && path[2] == '/'); - var isUNCRoot = IsUNCPath(path); - bool isRoot = isRegularRoot || isRootWithDriveLetter || isUNCRoot; - - var startIndex = 0; - if (isRoot) startIndex = 1; - if (isRootWithDriveLetter) startIndex = 3; - if (isUNCRoot) startIndex = path.IndexOf('/') + 1; - - var stack = new Stack(); - int segmentStart = startIndex; - for (int i = startIndex; i != path.Length; i++) - { - if (path[i] == '/' || i == path.Length - 1) - { - int extra = (i == path.Length - 1) ? 1 : 0; - var substring = path.Substring(segmentStart, i - segmentStart + extra); - if (substring == "..") - { - if (stack.Count == 0) - { - if (isRoot) - throw new ArgumentException( - $"Cannot parse path because it's ..'ing beyond the root: {path}"); - stack.Push(substring); - } - else - { - if (stack.Peek() == "..") - stack.Push(substring); - else - stack.Pop(); - } - } - else - stack.Push(substring); - - segmentStart = i + 1; - } - } - - return path.Substring(0, startIndex) + string.Join("/", stack.Reverse().ToArray()); - } - - const int MethodImplOptions_AggressiveInlining = 256; // enum value is only in .NET 4.5+ - - [MethodImpl(MethodImplOptions_AggressiveInlining)] - private static bool IsSlash(char c) => c == '/' || c == '\\'; - - /// - /// Create a new NPath by appending a path fragment. - /// - /// The path fragment to append. This can be a filename, or a whole relative path. - /// A new NPath which is the existing path with the fragment appended. - public NPath Combine(string append) - { - if (IsSlash(append[0])) - throw new ArgumentException($"You cannot .Combine a non-relative path: {append}"); - return new NPath(_path + "/" + append); - } - - /// - /// Create a new NPath by appending two path fragments, one after the other. - /// - /// The first path fragment to append. - /// The second path fragment to append. - /// A new NPath which is the existing path with the first fragment appended, then the second fragment appended. - public NPath Combine(string append1, string append2) - { - return new NPath(_path + "/" + append1 + "/" + append2); - } - - /// - /// Create a new NPath by appending a path fragment. - /// - /// The path fragment to append. - /// A new NPath which is the existing path with the fragment appended. - public NPath Combine(NPath append) - { - if (append == null) - throw new ArgumentNullException(nameof(append)); - - var firstChar = append._path[0]; - if (IsSlash(firstChar)) - throw new ArgumentException($"You cannot .Combine a non-relative path: {append._path}"); - - //if the to-append path starts by going up directories, we need to run our normalizing constructor, if not, we can take the fast path - if (firstChar == '.' || _path[0] == '.' || _path.Length == 1) - return new NPath(_path + "/" + append._path); - return new NPath(_path + "/" + append, true); - } - - /// - /// Create a new NPath by appending multiple path fragments. - /// - /// The path fragments to append, in order. - /// A new NPath which is this existing path with all the supplied path fragments appended, in order. - public NPath Combine(params NPath[] append) - { - var sb = new StringBuilder(ToString()); - foreach (var a in append) - { - if (!a.IsRelative) - throw new ArgumentException($"You cannot .Combine a non-relative path: {a}"); - - sb.Append("/"); - sb.Append(a); - } - - return new NPath(sb.ToString()); - } - - /// - /// The parent path fragment (i.e. the directory) of the path. - /// - public NPath Parent - { - get - { - if (IsRoot) - throw new ArgumentException($"Parent invoked on {this}"); - - for (int i = _path.Length - 1; i >= 0; i--) - { - if (i == 0) - return _path[0] == '/' ? new NPath("/") : new NPath(""); - if (_path[i] != '/') continue; - var isRooted = _path[i - 1] == ':' || _path[0] == '/'; - - var length = isRooted ? (i + 1) : i; - - var substring = _path.Substring(0, length); - - return new NPath(substring); - } - - return Empty; - } - } - - /// - /// Create a new NPath by computing the existing path relative to some other base path. - /// - /// The base path that the result should be relative to. - /// A new NPath, which refers to the same target as the existing path, but is described relative to the given base path. - public NPath RelativeTo(NPath path) - { - if (IsRelative || path.IsRelative) - return MakeAbsolute().RelativeTo(path.MakeAbsolute()); - - var thisString = _path; - var pathString = path._path; - - if (thisString == pathString) - return "."; - - if (!HasSameDriveLetter(path) || !HasSameUNCServerName(path)) - return this; - - if (path.IsRoot) - return new NPath(thisString.Substring(pathString.Length)); - - if (thisString.StartsWith(pathString, PathStringComparison)) - { - if (thisString.Length >= pathString.Length && (IsSlash(thisString[pathString.Length]))) - return new NPath(thisString.Substring(Math.Min(pathString.Length + 1, thisString.Length))); - } - - var sb = new StringBuilder(); - foreach (var parent in path.RecursiveParents.ToArray()) - { - sb.Append("../"); - if (IsChildOf(parent)) - { - sb.Append(thisString.Substring(parent.ToString().Length)); - return new NPath(sb.ToString()); - } - } - - throw new ArgumentException(); - } - - /// - /// Create an NPath by changing the extension of this one. - /// - /// The new extension to use. Starting it with a "." character is optional. If you pass an empty string, the resulting path will have the extension stripped entirely, including the dot character. - /// A new NPath which is the existing path but with the new extension at the end. - public NPath ChangeExtension(string extension) - { - ThrowIfRoot(); - - var s = ToString(); - int lastDot = -1; - for (int i = s.Length - 1; i >= 0; i--) - { - if (s[i] == '.') - { - lastDot = i; - break; - } - - if (s[i] == '/') - break; - } - - var newExtension = extension.Length == 0 ? extension : WithDot(extension); - if (lastDot == -1) - return s + newExtension; - return s.Substring(0, lastDot) + newExtension; - } - - #endregion construction - - #region inspection - - /// - /// Whether this path is relative (i.e. not absolute) or not. - /// - public bool IsRelative - { - get - { - if (_path[0] == '/') - return false; - - if (_path.Length >= 3 && _path[1] == ':' && _path[2] == '/') - return false; - - if (_path[0] == '\\') - return false; - - return true; - } - } - - /// - /// The name of the file or directory given at the end of this path, including any extension. - /// - public string FileName - { - get - { - ThrowIfRoot(); - - if (_path.Length == 0) - return string.Empty; - - if (_path == ".") - return string.Empty; - - for (int i = _path.Length - 1; i >= 0; i--) - { - if (_path[i] == '/') - { - return i == _path.Length - 1 ? string.Empty : _path.Substring(i + 1); - } - } - - return _path; - } - } - - /// - /// The name of the file or directory given at the end of this path, excluding the extension. - /// - public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(FileName); - - /// - /// Determines whether the given path is, or is a child of, a directory with the given name. - /// - /// The name of the directory to search for. - /// True if the path describes a file/directory that is or is a child of a directory with the given name; false otherwise. - public bool HasDirectory(string dir) - { - if (dir.Contains("/") || dir.Contains("\\")) - throw new ArgumentException($"Directory cannot contain slash {dir}"); - if (dir == ".") - throw new ArgumentException("Single dot is not an allowed argument"); - - if (_path.StartsWith(dir + "/", PathStringComparison)) - return true; - if (_path.EndsWith("/" + dir, PathStringComparison)) - return true; - return _path.Contains("/" + dir + "/"); - } - - /// - /// The depth of the path, determined by the number of path separators present. - /// - public int Depth - { - get - { - if (IsRoot) - return 0; - if (IsCurrentDir) - return 0; - - var depth = IsRelative ? 1 : 0; - for (var i = 0; i != _path.Length; i++) - { - if (_path[i] == '/') - depth++; - } - - return depth; - } - } - - /// - /// Tests whether the path is the current directory string ".". - /// - public bool IsCurrentDir => ToString() == "."; - - /// - /// Tests whether the path exists. - /// - /// An optional path fragment to append before testing. - /// True if the path (with optional appended fragment) exists, false otherwise. - public bool Exists(NPath append = null) - { - return FileExists(append) || DirectoryExists(append); - } - - /// - /// Tests whether the path exists and is a directory. - /// - /// An optional path fragment to append before testing. - /// True if the path (with optional appended fragment) exists and is a directory, false otherwise. - public bool DirectoryExists(NPath append = null) => FileSystem.Active.Directory_Exists(append != null ? Combine(append) : this); - - /// - /// Tests whether the path exists and is a file. - /// - /// An optional path fragment to append before testing. - /// True if the path (with optional appended fragment) exists and is a file, false otherwise. - public bool FileExists(NPath append = null) => FileSystem.Active.File_Exists(append != null ? Combine(append) : this); - - /// - /// The extension of the file, excluding the initial "." character. - /// - public string Extension - { - get - { - if (IsRoot) - throw new ArgumentException("A root directory does not have an extension"); - - for (int i = _path.Length - 1; i >= 0; i--) - { - var c = _path[i]; - if (c == '.' || c == '/') - return _path.Substring(i + 1); - } - - return string.Empty; - } - } - - /// - /// UNC server name of the path, if present. Null if not present. - /// - public string UNCServerName - { - get - { - if (!IsUNC) - return null; - var indexOfFirstSlash = _path.IndexOf('/'); - if (indexOfFirstSlash < 0) - indexOfFirstSlash = _path.Length; - return _path.Substring(2, indexOfFirstSlash - 2); - } - } - - bool HasSameUNCServerName(NPath other) => UNCServerName == other.UNCServerName; - - /// - /// The Windows drive letter of the path, if present. Null if not present. - /// - public string DriveLetter => _path.Length >= 2 && _path[1] == ':' ? _path[0].ToString() : null; - - bool HasSameDriveLetter(NPath other) => DriveLetter == other.DriveLetter; - - /// - /// Provides a quoted version of the path as a string, with the requested path separator type. - /// - /// The path separator to use. See the SlashMode enum for an explanation of the values. Defaults to SlashMode.Forward. - /// The path, with the requested path separator type, in quotes. - public string InQuotes(SlashMode slashMode = SlashMode.Forward) - { - return "\"" + ToString(slashMode) + "\""; - } - - /// - /// Convert this path to a string, using forward slashes as path separators. - /// - /// The string representation of this path. - public override string ToString() - { - return _path; - } - - /// - /// Convert this path to a string, using the requested path separator type. - /// - /// The path separator type to use. See SlashMode for possible values. - /// The string representation of this path. - public string ToString(SlashMode slashMode) - { - if (slashMode == SlashMode.Forward || (slashMode == SlashMode.Native && !k_IsWindows)) - return _path; - - return _path.Replace("/", "\\"); - } - - /// - /// Checks if this NPath represents the same path as another object. - /// - /// The object to compare to. - /// True if this NPath represents the same path as the other object; false if it does not, if the other object is not an NPath, or is null. - public override bool Equals(object obj) - { - return Equals(obj as NPath); - } - - /// - /// Checks if this NPath is equal to another NPath. - /// - /// The path to compare to. - /// True if this NPath represents the same path as the other NPath; false otherwise. - /// Note that the comparison requires that the paths are the same, not just that the targets are the same; "foo/bar" and "foo/baz/../bar" refer to the same target but will not be treated as equal by this comparison. However, this comparison will ignore case differences when the current operating system does not use case-sensitive filesystems. - public bool Equals(NPath p) - { - return p != null && string.Equals(p._path, _path, PathStringComparison); - } - - /// - /// Compare two NPaths for equality. - /// - /// The first NPath to compare. - /// The second NPath to compare. - /// True if the NPaths are both equal (or both null), false otherwise. See Equals. - public static bool operator ==(NPath a, NPath b) - { - // If both are null, or both are same instance, return true. - if (ReferenceEquals(a, b)) - return true; - - // If one is null, but not both, return false. - if ((object)a == null || (object)b == null) - return false; - - // Return true if the fields match: - return a.Equals(b); - } - - /// - /// Get an appropriate hash value for this NPath. - /// - /// A hash value for this NPath. - public override int GetHashCode() - { - if (k_IsCaseSensitiveFileSystem) - return _path.GetHashCode(); - - uint hash = 27644437; - for (int i = 0, len = _path.Length; i < len; ++i) - { - uint c = _path[i]; - if (c > 0x80) c = 0x80; // All non-ASCII chars may (potentially) compare Equal. - c |= 0x20; // ASCII case folding. - hash ^= (hash << 5) ^ c; - } - - return unchecked((int)hash); - } - - /// - /// Compare this NPath to another NPath, returning a value that can be used to sort the two objects in a stable order. - /// - /// The object to compare to. Note that this object must be castable to NPath. - /// A value that indicates the relative order of the two objects. The return value has these meanings: - /// - /// ValueMeaning - /// Less than zeroThis instance precedes obj in the sort order. - /// ZeroThis instance occurs in the same position as obj in the sort order. - /// Greater than zeroThis instance follows obj in the sort order. - /// - /// - - public int CompareTo(object obj) - { - if (obj == null) - return -1; - - return string.Compare(_path, ((NPath)obj)._path, PathStringComparison); - } - - /// - /// Compare two NPaths for inequality. - /// - /// The first NPath to compare. - /// The second NPath to compare. - /// True if the NPaths are not equal, false otherwise. - public static bool operator !=(NPath a, NPath b) - { - return !(a == b); - } - - /// - /// Tests whether this NPath has the provided extension. - /// - /// The extension to test for. - /// True if this NPath has has the provided extension. False otherwise. - /// The extension "*" is special, and will return true for all paths if specified. - public bool HasExtension(string extension) - { - if (extension == "*") - return true; - - if (IsRoot) - return false; - - if (extension.Length > _path.Length) - return false; - - int extensionOffset = _path.Length - extension.Length; - if (extension.Length == 0 || extension[0] != '.') - { - // Supplied extension doesn't have a dot, so we must check - // that there is a dot or a slash before the extension in our path - if (extensionOffset < 1) - return false; - - char extensionSeparator = _path[extensionOffset - 1]; - if (extensionSeparator != '.' && extensionSeparator != '/') - return false; - } - - return string.Compare(extension, 0, _path, extensionOffset, extension.Length, StringComparison.OrdinalIgnoreCase) == 0; - } - - /// - /// Tests whether this NPath has one of the provided extensions, or if no extensions are provided, whether it has any extension at all. - /// - /// The possible extensions to test for. - /// True if this NPath has one of the provided extensions; or, if no extensions are provided, true if this NPath has an extension. False otherwise. - /// The extension "*" is special, and will return true for all paths if specified. - public bool HasExtension(params string[] extensions) - { - if (extensions.Length == 0) - return FileName.Contains("."); - - foreach (var e in extensions) - { - if (HasExtension(e)) - return true; - } - - return false; - } - - private static string WithDot(string extension) - { - return extension.StartsWith(".", StringComparison.Ordinal) ? extension : "." + extension; - } - - /// - /// Whether this path is rooted or not (begins with a slash character or drive specifier). - /// - public bool IsRoot - { - get - { - if (_path == "/") - return true; - - if (_path.Length == 3 && _path[1] == ':' && _path[2] == '/') - return true; - - if (IsUNC && _path.Length == _path.IndexOf('/') + 1) - return true; - - return false; - } - } - - /// - /// Whether this path starts with an UNC path prefix "\\" or not. - /// - public bool IsUNC => IsUNCPath(_path); - - #endregion inspection - - #region directory enumeration - - /// - /// Find all files within this path that match the given filter. - /// - /// The filter to match against the names of files. Wildcards can be included. - /// If true, search recursively inside subdirectories of this path; if false, search only for files that are immediate children of this path. Defaults to false. - /// An array of files that were found. - public NPath[] Files(string filter, bool recurse = false) => FileSystem.Active.Directory_GetFiles(_path, filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); - - /// - /// Find all files within this path. - /// - /// If true, search recursively inside subdirectories of this path; if false, search only for files that are immediate children of this path. Defaults to false. - /// An array of files that were found. - public NPath[] Files(bool recurse = false) => Files("*", recurse); - - /// - /// Find all files within this path that have one of the provided extensions. - /// - /// The extensions to search for. - /// If true, search recursively inside subdirectories of this path; if false, search only for files that are immediate children of this path. Defaults to false. - /// An array of files that were found. - public NPath[] Files(string[] extensions, bool recurse = false) - { - if (!DirectoryExists() || extensions.Length == 0) - return new NPath[] { }; - - return FileSystem.Active.Directory_GetFiles(this, "*", recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Where(p => extensions.Contains(p.Extension)).ToArray(); - } - - /// - /// Find all files or directories within this path that match the given filter. - /// - /// The filter to match against the names of files and directories. Wildcards can be included. - /// If true, search recursively inside subdirectories of this path; if false, search only for files and directories that are immediate children of this path. Defaults to false. - /// An array of files and directories that were found. - public NPath[] Contents(string filter, bool recurse = false) - { - return Files(filter, recurse).Concat(Directories(filter, recurse)).ToArray(); - } - - /// - /// Find all files and directories within this path. - /// - /// If true, search recursively inside subdirectories of this path; if false, search only for files and directories that are immediate children of this path. Defaults to false. - /// An array of files and directories that were found. - public NPath[] Contents(bool recurse = false) - { - return Contents("*", recurse); - } - - /// - /// Find all directories within this path that match the given filter. - /// - /// The filter to match against the names of directories. Wildcards can be included. - /// If true, search recursively inside subdirectories of this path; if false, search only for directories that are immediate children of this path. Defaults to false. - /// An array of directories that were found. - public NPath[] Directories(string filter, bool recurse = false) - { - return FileSystem.Active.Directory_GetDirectories(this, filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); - } - - /// - /// Find all directories within this path. - /// - /// If true, search recursively inside subdirectories of this path; if false, search only for directories that are immediate children of this path. Defaults to false. - /// An array of directories that were found. - public NPath[] Directories(bool recurse = false) - { - return Directories("*", recurse); - } - - #endregion - - #region filesystem writing operations - - /// - /// Create an empty file at this path. - /// - /// This NPath, for chaining further operations. - /// If a file already exists at this path, it will be overwritten. - public NPath CreateFile() - { - ThrowIfRoot(); - EnsureParentDirectoryExists(); - FileSystem.Active.File_WriteAllBytes(this, new byte[0]); - return this; - } - - /// - /// Append the given path fragment to this path, and create an empty file there. - /// - /// The path fragment to append. - /// The path to the created file, for chaining further operations. - /// If a file already exists at that path, it will be overwritten. - public NPath CreateFile(NPath file) - { - if (!file.IsRelative) - throw new ArgumentException( - "You cannot call CreateFile() on an existing path with a non relative argument"); - return Combine(file).CreateFile(); - } - - /// - /// Create this path as a directory if it does not already exist. - /// - /// This NPath, for chaining further operations. - /// This is identical to , except that EnsureDirectoryExists triggers "Stat" callbacks and this doesn't. - public NPath CreateDirectory() - { - if (IsRoot) - throw new NotSupportedException( - "CreateDirectory is not supported on a root level directory because it would be dangerous:" + - ToString()); - - FileSystem.Active.Directory_CreateDirectory(this); - return this; - } - - /// - /// Append the given path fragment to this path, and create it as a directory if it does not already exist. - /// - /// The path fragment to append. - /// The path to the created directory, for chaining further operations. - public NPath CreateDirectory(NPath directory) - { - if (!directory.IsRelative) - throw new ArgumentException("Cannot call CreateDirectory with an absolute argument"); - - return Combine(directory).CreateDirectory(); - } - - /// - /// Create this path as a symbolic link to another file or directory. - /// - /// The path that this path should be a symbolic link to. Can be relative or absolute. - /// Specifies whether this link is to a file or to a directory (required on Windows). Defaults to file. - /// The path to the created symbolic link, for chaining further operations. - public NPath CreateSymbolicLink(NPath targetPath, bool targetIsFile = true) - { - ThrowIfRoot(); - - if (Exists()) - throw new InvalidOperationException( - "Cannot create symbolic link at {this} because it already exists as a file or directory."); - - FileSystem.Active.CreateSymbolicLink(this, targetPath, targetIsFile); - return this; - } - - /// - /// Checks whether the entity referred to by this path is a symbolic link. - /// - public bool IsSymbolicLink - { - get - { - return FileSystem.Active.IsSymbolicLink(this); - } - } - - /// - /// Copy this NPath to the given destination. - /// - /// The path to copy to. - /// The path to the copied result, for chaining further operations. - public NPath Copy(NPath dest) - { - return Copy(dest, p => true); - } - - /// - /// Copy this NPath to the given destination, applying a filter function to decide which files are copied. - /// - /// The path to copy to. - /// The filter function. Each candidate file is passed to this function; if the function returns true, the file will be copied, otherwise it will not. - /// - public NPath Copy(NPath dest, Func fileFilter) - { - if (dest.DirectoryExists()) - return CopyWithDeterminedDestination(dest.Combine(FileName), fileFilter); - - return CopyWithDeterminedDestination(dest, fileFilter); - } - - /// - /// Create a new NPath by converting this path into an absolute representation. - /// - /// Optional base to use as a root for relative paths. - /// - public NPath MakeAbsolute(NPath @base = null) - { - if (!IsRelative) - return this; - - return (@base ?? CurrentDirectory).Combine(this); - } - - NPath CopyWithDeterminedDestination(NPath destination, Func fileFilter) - { - destination = destination.MakeAbsolute(); - - if (FileExists()) - { - if (!fileFilter(destination)) - return null; - - destination.EnsureParentDirectoryExists(); - - FileSystem.Active.File_Copy(this, destination, true); - return destination; - } - - if (DirectoryExists()) - { - destination.EnsureDirectoryExists(); - foreach (var thing in Contents()) - thing.CopyWithDeterminedDestination(destination.Combine(thing.RelativeTo(this)), - fileFilter); - return destination; - } - - throw new ArgumentException("Copy() called on path that doesnt exist: " + ToString()); - } - - /// - /// Deletes the file or directory referred to by the NPath. - /// - /// The deletion mode to use, see DeleteMode. Defaults to DeleteMode.Normal. - /// The path does not exist. See also DeleteIfExists. - public void Delete(DeleteMode deleteMode = DeleteMode.Normal) - { - if (IsRoot) - throw new NotSupportedException( - "Delete is not supported on a root level directory because it would be dangerous:" + ToString()); - - try - { - if (FileExists()) - FileSystem.Active.File_Delete(this); - else if (DirectoryExists()) - FileSystem.Active.Directory_Delete(this, true); - else - throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString()); - } - catch (IOException) - { - if (deleteMode == DeleteMode.Normal) - throw; - } - catch (UnauthorizedAccessException) - { - if (deleteMode == DeleteMode.Normal) - throw; - } - } - - /// - /// Deletes the file or directory referred to by the NPath, if it exists. - /// - /// The deletion mode to use, see DeleteMode. Defaults to DeleteMode.Normal. - /// This NPath, for chaining further operations. - public NPath DeleteIfExists(DeleteMode deleteMode = DeleteMode.Normal) - { - if (FileExists() || DirectoryExists()) - Delete(deleteMode); - - return this; - } - - /// - /// Deletes all files and directories inside the directory referred to by this NPath. - /// - /// This NPath, for chaining further operations. - /// This NPath refers to a file, rather than a directory. - public NPath DeleteContents() - { - if (IsRoot) - throw new NotSupportedException( - "DeleteContents is not supported on a root level directory because it would be dangerous:" + - ToString()); - - if (FileExists()) - throw new InvalidOperationException("It is not valid to perform this operation on a file"); - - if (DirectoryExists()) - { - try - { - Files().Delete(); - Directories().Delete(); - } - catch (IOException) - { - if (Files(true).Any()) - throw; - } - - return this; - } - - return EnsureDirectoryExists(); - } - - /// - /// Create a temporary directory in the system temporary location and return the NPath of it. - /// - /// A prefix to use for the name of the temporary directory. - /// A new NPath which targets the newly created temporary directory. - public static NPath CreateTempDirectory(string prefix = "") - { - var sb = new StringBuilder(); - - sb.Append(Path.GetTempPath()); - sb.Append("/"); - if (!string.IsNullOrEmpty(prefix)) - { - sb.Append(prefix); - sb.Append("_"); - } - sb.Append(Path.GetRandomFileName()); - - var path = new NPath(sb.ToString()); - return path.CreateDirectory(); - } - - /// - /// Move the file or directory targetted by this NPath to a new location. - /// - /// The destination for the move. - /// An NPath representing the newly moved file or directory. - public NPath Move(NPath dest) - { - if (IsRoot) - throw new NotSupportedException( - "Move is not supported on a root level directory because it would be dangerous:" + ToString()); - - if (dest.DirectoryExists()) - return Move(dest.Combine(FileName)); - - if (FileExists()) - { - dest.EnsureParentDirectoryExists(); - FileSystem.Active.File_Move(this, dest); - return dest; - } - - if (DirectoryExists()) - { - FileSystem.Active.Directory_Move(this, dest); - return dest; - } - - throw new ArgumentException("Move() called on a path that doesn't exist: " + ToString()); - } - - #endregion - - #region special paths - - /// - /// The current directory in use by the process. - /// - /// Note that every read from this property will result in an operating system query, unless WithFrozenCurrentDirectory is used. - public static NPath CurrentDirectory => FileSystem.Active.Directory_GetCurrentDirectory(); - - class SetCurrentDirectoryOnDispose : IDisposable - { - public NPath Directory { get; } - - public SetCurrentDirectoryOnDispose(NPath directory) - { - Directory = directory; - } - - public void Dispose() - { - SetCurrentDirectory(Directory); - } - } - - /// - /// Temporarily change the current directory for the process. - /// - /// The new directory to set as the current directory. - /// A token representing the change in current directory. When this is disposed, the current directory will be returned to its previous value. The usual usage pattern is to capture the token with a using statement, such that it is automatically disposed of when the using block exits. - public static IDisposable SetCurrentDirectory(NPath directory) - { - var result = new SetCurrentDirectoryOnDispose(CurrentDirectory); - FileSystem.Active.Directory_SetCurrentDirectory(directory); - return result; - } - - /// - /// The current user's home directory. - /// - public static NPath HomeDirectory - { - get - { - if (Path.DirectorySeparatorChar == '\\') - return new NPath(Environment.GetEnvironmentVariable("USERPROFILE")); - return new NPath(Environment.GetEnvironmentVariable("HOME")); - } - } - - /// - /// The system temporary directory. - /// - public static NPath SystemTemp => new NPath(Path.GetTempPath()); - - #endregion - - private void ThrowIfRoot() - { - if (IsRoot) - throw new ArgumentException( - "You are attempting an operation that is not valid on a root level directory"); - } - - /// - /// Append an optional path fragment to this NPath, then create it as a directory if it does not already exist. - /// - /// The path fragment to append. - /// The path to the directory that is now guaranteed to exist. - /// This is identical to , except that this triggers "Stat" callbacks and CreateDirectory doesn't. - public NPath EnsureDirectoryExists(NPath append = null) - { - var combined = append != null ? Combine(append) : this; - if (combined.DirectoryExists()) - return combined; - combined.EnsureParentDirectoryExists(); - combined.CreateDirectory(); - return combined; - } - - /// - /// Create the parent directory of this NPath if it does not already exist. - /// - /// This NPath, for chaining further operations. - public NPath EnsureParentDirectoryExists() - { - Parent.EnsureDirectoryExists(); - return this; - } - - /// - /// Throw an exception if this path does not exist as a file. - /// - /// This path, in order to chain further operations. - /// The path does not exist, or is not a file. - public NPath FileMustExist() - { - if (!FileExists()) - throw new FileNotFoundException("File was expected to exist : " + ToString()); - - return this; - } - - /// - /// Throw an exception if this directory does not exist. - /// - /// This path, in order to chain further operations. - /// The path does not exist, or is not a directory. - public NPath DirectoryMustExist() - { - if (!DirectoryExists()) - throw new DirectoryNotFoundException("Expected directory to exist : " + ToString()); - - return this; - } - - /// - /// Check if this path is a child of the given path hierarchy root (i.e. is a file or directory that is inside the given hierachy root directory or one of its descendent directories). - /// - /// The path hierarchy root to check. - /// True if this path is a child of the given root path, false otherwise. - public bool IsChildOf(NPath potentialBasePath) - { - if (IsRelative != potentialBasePath.IsRelative) - return MakeAbsolute().IsChildOf(potentialBasePath.MakeAbsolute()); - - if (!IsRelative && !HasSameDriveLetter(potentialBasePath) || !HasSameUNCServerName(potentialBasePath)) - return false; - - if (potentialBasePath.IsRoot) - return true; - - if (IsRelative && potentialBasePath._path == ".") - return !_path.StartsWith("..", StringComparison.Ordinal); - - var potentialBaseString = potentialBasePath._path; - var potentialBaseStringLength = potentialBaseString.Length; - - return _path.Length > potentialBaseStringLength + 1 && - _path.StartsWith(potentialBaseString, PathStringComparison) && - _path[potentialBaseStringLength] == '/'; - } - - /// - /// Check if this path is a child of the given path hierarchy root (i.e. is a file or directory that is inside the given hierachy root directory or one of its descendent directories), or is equal to it. - /// - /// The path hierarchy root to check. - /// True if this path is equal to or is a child of the given root path, false otherwise. - public bool IsSameAsOrChildOf(NPath potentialBasePath) - { - return MakeAbsolute() == potentialBasePath.MakeAbsolute() || IsChildOf(potentialBasePath); - } - - /// - /// Return each parent directory of this path, starting with the immediate parent, then that directory's parent, and so on, until the root of the path is reached. - /// - public IEnumerable RecursiveParents - { - get - { - var candidate = this; - while (true) - { - if (candidate.IsRoot || candidate._path == ".") - yield break; - - candidate = candidate.Parent; - yield return candidate; - } - } - } - - /// - /// Search all parent directories of this path for one that contains a file or directory with the given name. - /// - /// The name of the file or directory to search for. - /// The path to the parent directory that contains the file or directory, or null if none of the parents contained a file or directory with the requested name. - public NPath ParentContaining(NPath needle) - { - return RecursiveParents.FirstOrDefault(p => p.Exists(needle)); - } - - /// - /// Open this path as a text file, write the given string to it, then close the file. - /// - /// The string to write to the text file. - /// The path to this file, for use in chaining further operations. - public NPath WriteAllText(string contents) - { - EnsureParentDirectoryExists(); - FileSystem.Active.File_WriteAllText(this, contents); - return this; - } - - /// - /// Open this file as a text file, and replace the contents with the provided string, if they do not already match. Then close the file. - /// - /// The string to replace the file's contents with. - /// The path to this file, for use in chaining further operations. - /// Note that if the contents of the file already match the provided string, the file is not modified - this includes not modifying the file's "last written" timestamp. - public NPath ReplaceAllText(string contents) - { - if (FileExists() && ReadAllText() == contents) - return this; - WriteAllText(contents); - return this; - } - - /// - /// Open this path as a file, write the given bytes to it, then close the file. - /// - /// The bytes to write to the file. - /// The path to this file, for use in chaining further operations. - public NPath WriteAllBytes(byte[] bytes) - { - EnsureParentDirectoryExists(); - FileSystem.Active.File_WriteAllBytes(this, bytes); - return this; - } - - /// - /// Opens a text file, reads all the text in the file into a single string, then closes the file. - /// - /// The contents of the text file, as a single string. - public string ReadAllText() => FileSystem.Active.File_ReadAllText(this); - - /// - /// Opens a file, reads all the bytes in the file, then closes the file. - /// - /// The contents of the file, as a bytes array. - public byte[] ReadAllBytes() => FileSystem.Active.File_ReadAllBytes(this); - - /// - /// Opens a text file, writes all entries of a string array as separate lines into the file, then closes the file. - /// - /// The entries to write into the file as separate lines. - /// The path to this file. - public NPath WriteAllLines(string[] contents) - { - EnsureParentDirectoryExists(); - FileSystem.Active.File_WriteAllLines(this, contents); - return this; - } - - /// - /// Opens a text file, reads all lines of the file into a string array, and then closes the file. - /// - /// A string array containing all lines of the file. - public string[] ReadAllLines() => FileSystem.Active.File_ReadAllLines(this); - - /// - /// Copy all files in this NPath to the given destination directory. - /// - /// The directory to copy the files to. - /// If true, files inside subdirectories of this NPath will also be copied. If false, only immediate child files of this NPath will be copied. - /// An optional predicate function that can be used to filter files. It is passed each source file path in turn, and if it returns true, the file is copied; otherwise, the file is not copied. - /// The paths to all the newly copied files. - /// Note that the directory structure of the files relative to this NPath will be preserved within the target directory. - public IEnumerable CopyFiles(NPath destination, bool recurse, Func fileFilter = null) - { - destination.EnsureDirectoryExists(); - return Files(recurse).Where(fileFilter ?? AlwaysTrue) - .Select(file => file.Copy(destination.Combine(file.RelativeTo(this)))).ToArray(); - } - - /// - /// Move all files in this NPath to the given destination directory. - /// - /// The directory to move the files to. - /// If true, files inside subdirectories of this NPath will also be moved. If false, only immediate child files of this NPath will be moved. - /// An optional predicate function that can be used to filter files. It is passed each source file path in turn, and if it returns true, the file is moved; otherwise, the file is not moved. - /// The paths to all the newly moved files. - /// Note that the directory structure of the files relative to this NPath will be preserved within the target directory. - public IEnumerable MoveFiles(NPath destination, bool recurse, Func fileFilter = null) - { - if (IsRoot) - throw new NotSupportedException( - "MoveFiles is not supported on this directory because it would be dangerous:" + ToString()); - - destination.EnsureDirectoryExists(); - return Files(recurse).Where(fileFilter ?? AlwaysTrue) - .Select(file => file.Move(destination.Combine(file.RelativeTo(this)))).ToArray(); - } - - static bool AlwaysTrue(NPath p) - { - return true; - } - - /// - /// Implicitly construct a new NPath from a string. - /// - /// The string to construct the new NPath from. - public static implicit operator NPath(string input) - { - return input != null ? new NPath(input) : null; - } - - /// - /// Set the last time the file was written to, in UTC. - /// - /// The last time the file was written to, in UTC. - /// This is set automatically by the OS when the file is modified, but it can sometimes be useful - /// to explicitly update the timestamp without modifying the file contents. - public NPath SetLastWriteTimeUtc(DateTime lastWriteTimeUtc) - { - FileSystem.Active.File_SetLastWriteTimeUtc(this, lastWriteTimeUtc); - return this; - } - - /// - /// Get the last time the file was written to, in UTC. - /// - /// The last time the file was written to, in UTC. - public DateTime GetLastWriteTimeUtc() - { - return FileSystem.Active.File_GetLastWriteTimeUtc(this); - } - - /// - /// Get the file length in bytes. - /// - /// The file length in bytes. - public long GetFileSize() - { - return FileSystem.Active.File_GetSize(this); - } - - /// - /// The filesystem attributes of the given file, assuming it exists. Note that when you set this property, the - /// OS may still modify the the actual attributes of the file beyond what you requested, so setting and then - /// getting the property is not guaranteed to roundtrip the value. Note also that some attributes (e.g. - /// FileAttributes.Hidden) are not supported on every OS, and may throw exceptions or simply be ignored. - /// - public FileAttributes Attributes - { - get { return FileSystem.Active.File_GetAttributes(this); } - set - { - FileSystem.Active.File_SetAttributes(this, value); - } - } - - /// - /// Until .Dispose is invoked on the returnvalue, makes all NPath's on this thread use the provided filesystem implementation for all filesystem access. - /// - /// - /// An object you can invoke .Dispose() on, which will make all NPath filesystem operations stop using the provided filesystem - public static IDisposable WithFileSystem(FileSystem fileSystem) => new WithFileSystemHelper(fileSystem); - - - /// - /// Lets the currently active filesystem resolve the path. - /// - /// - public NPath ResolveWithFileSystem() => FileSystem.Active.Resolve(this); - - /// - /// Shorthand for .ResolveWithFileSystem.InQuotes() - /// - /// - /// - public string InQuotesResolved(SlashMode slashMode = SlashMode.Forward) => ResolveWithFileSystem().InQuotes(slashMode); - - /// - /// Abstract baseclass you can use to plug in different underlying filesystem behaviour for NPath to operate on - /// - public abstract class FileSystem : IDisposable - { - [ThreadStatic] internal static FileSystem _active; - - /// - /// The currently active filesystem for NPath operations. Set this using NPath.WithFileSystem() - /// - public static FileSystem Active => _active = _active ?? MakeFileSystemForCurrentMachine(); - - private static FileSystem MakeFileSystemForCurrentMachine() - { - if (CalculateIsWindows()) - return new WindowsFileSystem(); - else - return new PosixFileSystem(); - } - - /// - public virtual void Dispose() - { - } - -#pragma warning disable 1591 - public abstract NPath[] Directory_GetFiles(NPath path, string filter, SearchOption searchOptions); - public abstract bool Directory_Exists(NPath path); - public abstract bool File_Exists(NPath path); - public abstract void File_WriteAllBytes(NPath path, byte[] bytes); - public abstract void File_Copy(NPath path, NPath destinationPath, bool overWrite); - public abstract void File_Delete(NPath path); - - public abstract void File_Move(NPath path, NPath destinationPath); - public abstract void File_WriteAllText(NPath path, string contents); - public abstract string File_ReadAllText(NPath path); - public abstract void File_WriteAllLines(NPath path, string[] contents); - public abstract byte[] File_ReadAllBytes(NPath path); - - public abstract string[] File_ReadAllLines(NPath path); - - public abstract void File_SetLastWriteTimeUtc(NPath path, DateTime lastWriteTimeUtc); - - public abstract DateTime File_GetLastWriteTimeUtc(NPath path); - - public abstract long File_GetSize(NPath path); - - public abstract void File_SetAttributes(NPath path, FileAttributes value); - public abstract FileAttributes File_GetAttributes(NPath path); - - public abstract void Directory_CreateDirectory(NPath path); - public abstract void Directory_Delete(NPath path, bool b); - public abstract void Directory_Move(NPath path, NPath destPath); - - public abstract NPath Directory_GetCurrentDirectory(); - public abstract void Directory_SetCurrentDirectory(NPath directoryPath); - public abstract NPath[] Directory_GetDirectories(NPath path, string filter, SearchOption searchOptions); - - /// - /// If your filesystem does any kind of redirection or other magic, Resolve() is required to return the path that can be used against the raw lowlevel filesystem of the OS. - /// - /// The path to resolve - /// The resolved path that is valid to use against the OS's real filesystem - public abstract NPath Resolve(NPath path); - - public abstract bool IsSymbolicLink(NPath path); - - public abstract void CreateSymbolicLink(NPath fromPath, NPath targetPath, bool targetIsFile); - } -#pragma warning restore 1591 - - abstract class SystemIOFileSystem : FileSystem - { - public override NPath[] Directory_GetFiles(NPath path, string filter, SearchOption searchOptions) => System.IO.Directory.GetFiles(path.ToString(SlashMode.Native), filter, searchOptions).ToNPaths().ToArray(); - public override bool Directory_Exists(NPath path) => System.IO.Directory.Exists(path.ToString(SlashMode.Native)); - public override bool File_Exists(NPath path) => System.IO.File.Exists(path.ToString(SlashMode.Native)); - public override void File_WriteAllBytes(NPath path, byte[] bytes) => System.IO.File.WriteAllBytes(path.ToString(SlashMode.Native), bytes); - public override void File_Copy(NPath path, NPath destinationPath, bool overWrite) => System.IO.File.Copy(path.ToString(SlashMode.Native), destinationPath.ToString(SlashMode.Native), overWrite); - public override void File_Delete(NPath path) => System.IO.File.Delete(path.ToString(SlashMode.Native)); - public override void File_Move(NPath path, NPath destinationPath) => System.IO.File.Move(path.ToString(SlashMode.Native), destinationPath.ToString(SlashMode.Native)); - public override void File_WriteAllText(NPath path, string contents) => System.IO.File.WriteAllText(path.ToString(SlashMode.Native), contents); - public override string File_ReadAllText(NPath path) => System.IO.File.ReadAllText(path.ToString(SlashMode.Native)); - public override void File_WriteAllLines(NPath path, string[] contents) => System.IO.File.WriteAllLines(path.ToString(SlashMode.Native), contents); - public override string[] File_ReadAllLines(NPath path) => System.IO.File.ReadAllLines(path.ToString(SlashMode.Native)); - public override byte[] File_ReadAllBytes(NPath path) => System.IO.File.ReadAllBytes(path.ToString(SlashMode.Native)); - public override void File_SetLastWriteTimeUtc(NPath path, DateTime lastWriteTimeUtc) => System.IO.File.SetLastWriteTimeUtc(path.ToString(SlashMode.Native), lastWriteTimeUtc); - public override DateTime File_GetLastWriteTimeUtc(NPath path) => System.IO.File.GetLastWriteTimeUtc(path.ToString(SlashMode.Native)); - public override void File_SetAttributes(NPath path, FileAttributes value) => System.IO.File.SetAttributes(path.ToString(SlashMode.Native), value); - - public override FileAttributes File_GetAttributes(NPath path) => System.IO.File.GetAttributes(path.ToString(SlashMode.Native)); - public override long File_GetSize(NPath path) => new FileInfo(path.ToString(SlashMode.Native)).Length; - public override void Directory_CreateDirectory(NPath path) => System.IO.Directory.CreateDirectory(path.ToString(SlashMode.Native)); - public override void Directory_Delete(NPath path, bool b) => System.IO.Directory.Delete(path.ToString(SlashMode.Native), b); - public override void Directory_Move(NPath path, NPath destPath) => System.IO.Directory.Move(path.ToString(SlashMode.Native), destPath.ToString(SlashMode.Native)); - public override NPath Directory_GetCurrentDirectory() => System.IO.Directory.GetCurrentDirectory(); - public override void Directory_SetCurrentDirectory(NPath path) => System.IO.Directory.SetCurrentDirectory(path.ToString(SlashMode.Native)); - - public override NPath[] Directory_GetDirectories(NPath path, string filter, SearchOption searchOptions) => System.IO.Directory.GetDirectories(path.ToString(SlashMode.Native), filter, searchOptions).ToNPaths().ToArray(); - public override NPath Resolve(NPath path) => path; - } - - class WindowsFileSystem : SystemIOFileSystem - { - public override long File_GetSize(NPath path) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - return base.File_GetSize(path); - - var longPath = MakeLongPath(path).TrimEnd('\\'); - Win32Native.FIND_DATA findData; - IntPtr findHandle = Win32Native.FindFirstFile(longPath, out findData); - if (findHandle == new IntPtr(-1)) - throw new FileNotFoundException($"The path {path} does not exist.", path.ToString()); - Win32Native.FindClose(findHandle); - - var fileSizeLow = (long)findData.nFileSizeLow; - long fileSize; - if (fileSizeLow < 0 && (long)findData.nFileSizeHigh > 0) - fileSize = fileSizeLow + 0x100000000 + findData.nFileSizeHigh * 0x100000000; - else - { - if ((long)findData.nFileSizeHigh > 0) - fileSize = fileSizeLow + findData.nFileSizeHigh * 0x100000000; - else if (fileSizeLow < 0) - fileSize = fileSizeLow + 0x100000000; - else - fileSize = fileSizeLow; - } - - return fileSize; - } - - public override bool File_Exists(NPath path) - { - // Windows .NET implementation of File.Exists() does not handle paths longer than MAX_PATH correctly - if (path._path.Length < Win32Native.MAX_PATH_LEN) - return base.File_Exists(path); - - // If the path is long, fall back to querying with P/Invoke directly - var longPath = MakeLongPath(path).TrimEnd('\\'); - var attributes = Win32Native.GetFileAttributes(longPath); - return attributes != Win32Native.INVALID_FILE_ATTRIBUTES && ((attributes & (uint)Win32Native.FileAttributes.Directory) == 0); - } - - public override void File_Delete(NPath path) - { - if (!IsSymbolicLink(path) && path._path.Length < Win32Native.MAX_PATH_LEN) - { - base.File_Delete(path); - return; - } - - InternalFileDelete(path); - } - - private void InternalFileDelete(NPath path) - { - // Cleaning up symlinks requires slightly special handling on Windows - // Windows .NET implementation of File.Delete() does not handle paths longer than MAX_PATH correctly - var longPath = MakeLongPath(path).TrimEnd('\\'); - if (!Win32Native.DeleteFile(longPath)) - { - // try MoveFile - if (!Win32Native.MoveFileEx(longPath, null, Win32Native.MoveFileExFlags.DelayUntilReboot)) - throw new IOException($"Cannot delete file {path}.", new Win32Exception(Marshal.GetLastWin32Error())); - } - } - - public override void File_Copy(NPath path, NPath destinationPath, bool overWrite) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN && destinationPath._path.Length < Win32Native.MAX_PATH_LEN) - { - base.File_Copy(path, destinationPath, overWrite); - return; - } - - var longPath = MakeLongPath(path).TrimEnd('\\'); - var longDestPath = MakeLongPath(destinationPath).TrimEnd('\\'); - if (!Win32Native.CopyFile(longPath, longDestPath, !overWrite)) - throw new IOException($"Cannot copy file {path} to {destinationPath}.", new Win32Exception(Marshal.GetLastWin32Error())); - } - - public override void File_Move(NPath path, NPath destinationPath) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN && destinationPath._path.Length < Win32Native.MAX_PATH_LEN) - { - base.File_Move(path, destinationPath); - return; - } - - var longPath = MakeLongPath(path).TrimEnd('\\'); - var longDestPath = MakeLongPath(destinationPath).TrimEnd('\\'); - if (!Win32Native.MoveFile(longPath, longDestPath)) - { - var lastWin32Error = Marshal.GetLastWin32Error(); - // try copy/delete - if (!Win32Native.CopyFile(longPath, longDestPath, true)) - throw new IOException($"Cannot move file {path} to {destinationPath}.", new Win32Exception(lastWin32Error)); - - if (!Win32Native.DeleteFile(longPath)) - throw new IOException($"Cannot move file {path} to {destinationPath}.", new Win32Exception(lastWin32Error)); - } - } - - public override bool Directory_Exists(NPath path) - { - // Windows .NET implementation of File.Exists() does not handle paths longer than MAX_PATH correctly - if (path._path.Length < Win32Native.MAX_PATH_LEN) - return base.Directory_Exists(path); - - // If the path is long, fall back to querying with P/Invoke directly - var longPath = MakeLongPath(path).TrimEnd('\\'); - var attributes = Win32Native.GetFileAttributes(longPath); - return attributes != Win32Native.INVALID_FILE_ATTRIBUTES && - ((attributes & (uint)Win32Native.FileAttributes.Directory) != 0 || (attributes & (uint)Win32Native.FileAttributes.ReparsePoint) != 0); - } - - public override void Directory_CreateDirectory(NPath path) - { - // See https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation - // When using an API to create a directory, the specified path cannot be so long that you cannot append an 8.3 file name - // (that is, the directory name cannot exceed MAX_PATH minus 12) - if (path._path.Length < Win32Native.MAX_PATH_LEN - 12) - { - base.Directory_CreateDirectory(path); - return; - } - - InternalCreateDirectory(path); - } - - private void InternalCreateDirectory(NPath directoryPath) - { - if (Directory_Exists(directoryPath)) - return; - - var path = directoryPath.ToString(SlashMode.Native).TrimEnd('\\'); - var pos = path.LastIndexOf(@"\"); - if (pos > 2) - InternalCreateDirectory(new NPath(path.Substring(0, pos))); - - var longPath = MakeLongPath(path, Win32Native.MAX_PATH_LEN - 12); - if (!Win32Native.CreateDirectory(longPath, IntPtr.Zero)) - { - var lastError = Marshal.GetLastWin32Error(); - if (lastError == Win32Native.ERROR_INVALID_NAME && directoryPath.FileName.Length > 255) - throw new PathTooLongException($"Directory name {directoryPath.FileName} exceeds limit of 255 characters."); - throw new IOException($"Cannot create directory {directoryPath}.", new Win32Exception(lastError)); - } - } - - public override void Directory_Delete(NPath path, bool recursive) - { - if (recursive) - { - var files = Directory_GetFiles(path, "*", SearchOption.TopDirectoryOnly); - var dirs = Directory_GetDirectories(path, "*", SearchOption.TopDirectoryOnly); - - try - { - foreach (var file in files) - { - try - { - // remove read-only attribute or delete will fail - var attributes = File_GetAttributes(file); - if ((attributes & FileAttributes.ReadOnly) != 0) - File_SetAttributes(file, attributes & ~FileAttributes.ReadOnly); - - File_Delete(file); - } - // Another process/thread may have deleted (or be in the process of deleting) the file since the time we listed out the directory, causing any of these exceptions. - catch (Exception e) when (e is InvalidOperationException or FileNotFoundException or UnauthorizedAccessException) - { - if (file.FileExists()) - throw; - } - } - - foreach (var dir in dirs) - Directory_Delete(dir, true); - } - catch (IOException e) - { - throw new IOException($"Cannot delete directory {path}.", e); - } - } - - var longPath = MakeLongPath(path).TrimEnd('\\'); - if (!Win32Native.RemoveDirectory(longPath)) - throw new IOException($"Cannot delete directory {path}.", new Win32Exception(Marshal.GetLastWin32Error())); - } - - public override void Directory_Move(NPath path, NPath destinationPath) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN && destinationPath._path.Length < Win32Native.MAX_PATH_LEN) - { - base.Directory_Move(path, destinationPath); - return; - } - - var longPath = MakeLongPath(path).TrimEnd('\\'); - var longDestPath = MakeLongPath(destinationPath).TrimEnd('\\'); - - if (!Win32Native.MoveFileEx(longPath, longDestPath, Win32Native.MoveFileExFlags.CopyAllowed | Win32Native.MoveFileExFlags.WriteThrough)) - { - var lastWin32Error = Marshal.GetLastWin32Error(); - try - { - if (!Directory_Exists(destinationPath)) - Directory_CreateDirectory(destinationPath); - - var files = Directory_GetFiles(path, "*", SearchOption.AllDirectories); - foreach (var file in files) - { - var dest = destinationPath.Combine(file.RelativeTo(path)); - dest.Parent.EnsureDirectoryExists(); - - File_Copy(file, dest, false); - } - - Directory_Delete(path, true); - } - catch (IOException) - { - throw new IOException($"Cannot move directory {path} to {destinationPath}.", new Win32Exception(lastWin32Error)); - } - } - } - - public override bool IsSymbolicLink(NPath path) - { - // Retrieve the reparse tag, using FindFirstFile - Win32Native.FIND_DATA findData; - var longPath = MakeLongPath(path).TrimEnd('\\'); - IntPtr findHandle = Win32Native.FindFirstFile(longPath, out findData); - if (findHandle == new IntPtr(-1)) - throw new FileNotFoundException($"The path {path} does not exist.", path.ToString()); - Win32Native.FindClose(findHandle); - - // We already tested this above, but retest just in case it changed - if ((findData.dwFileAttributes & Win32Native.FileAttributes.ReparsePoint) != Win32Native.FileAttributes.ReparsePoint) - return false; - - return findData.dwReserved0 == Win32Native.IO_REPARSE_TAG_SYMLINK; - } - - public override void CreateSymbolicLink(NPath fromPath, NPath targetPath, bool targetIsFile) - { - var flags = Win32Native.SymbolicLinkFlags.File; - if (CalculateIsWindows10()) - flags |= Win32Native.SymbolicLinkFlags.AllowUnprivilegedCreate; - if (!targetIsFile) - flags |= Win32Native.SymbolicLinkFlags.Directory; - - var path = MakeLongPath(fromPath).TrimEnd('\\'); - var destPath = MakeLongPath(targetPath).TrimEnd('\\'); - if (!Win32Native.CreateSymbolicLink(path, destPath, flags)) - throw new IOException($"Cannot create symbolic link {path} from {targetPath}.", new Win32Exception(Marshal.GetLastWin32Error())); - } - - public override FileAttributes File_GetAttributes(NPath path) - { - // Windows .NET implementation of File.Exists() does not handle paths longer than MAX_PATH correctly - if (path._path.Length < Win32Native.MAX_PATH_LEN) - return base.File_GetAttributes(path); - - // If the path is long, fall back to querying with P/Invoke directly - var longPath = MakeLongPath(path).TrimEnd('\\'); - return (FileAttributes)(Win32Native.GetFileAttributes(longPath) & 0x0000FFFF); - } - - public override void File_SetAttributes(NPath path, FileAttributes value) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - { - base.File_SetAttributes(path, value); - return; - } - - // If the path is long, fall back to querying with P/Invoke directly - var longPath = MakeLongPath(path).TrimEnd('\\'); - Win32Native.SetFileAttributes(longPath, (uint)value); - } - - public override NPath[] Directory_GetFiles(NPath path, string filter, SearchOption searchOptions) - { - if (!Directory_Exists(path)) - throw new DirectoryNotFoundException($"The path {path} does not exist."); - - var results = new List(); - Win32Native.FIND_DATA findData; - var longPath = MakeLongPath(path, Win32Native.MAX_PATH_LEN - filter.Length - 1).TrimEnd('\\') + "\\" + filter; - IntPtr findHandle = Win32Native.FindFirstFile(longPath, out findData); - if (findHandle != new IntPtr(-1)) - { - try - { - bool found; - do - { - var currentFileName = findData.cFileName; - // if this is a file, find its contents - if (((uint)findData.dwFileAttributes & (uint)Win32Native.FileAttributes.Directory) == 0) - { - results.Add(path.Combine(currentFileName)); - } - - // find next - found = Win32Native.FindNextFile(findHandle, out findData); - } - while (found); - } - finally - { - // close the find handle - Win32Native.FindClose(findHandle); - } - } - - if (searchOptions == SearchOption.AllDirectories) - { - foreach (var dir in Directory_GetDirectories(path, "*", SearchOption.TopDirectoryOnly)) - results.AddRange(Directory_GetFiles(dir, filter, searchOptions)); - } - - return results.ToArray(); - } - - public override NPath[] Directory_GetDirectories(NPath path, string filter, SearchOption searchOptions) - { - var results = new List(); - Win32Native.FIND_DATA findData; - var longPath = MakeLongPath(path, Win32Native.MAX_PATH_LEN - filter.Length - 1).TrimEnd('\\') + "\\" + filter; - IntPtr findHandle = Win32Native.FindFirstFile(longPath, out findData); - if (findHandle != new IntPtr(-1)) - { - try - { - bool found; - do - { - var currentFileName = findData.cFileName; - // if this is a directory, find its contents - if (((uint)findData.dwFileAttributes & (uint)Win32Native.FileAttributes.Directory) != 0) - { - if (currentFileName != "." && currentFileName != "..") - results.Add(path.Combine(currentFileName)); - } - - // find next - found = Win32Native.FindNextFile(findHandle, out findData); - } - while (found); - } - finally - { - // close the find handle - Win32Native.FindClose(findHandle); - } - } - - if (searchOptions == SearchOption.AllDirectories) - { - foreach (var dir in Directory_GetDirectories(path, "*", SearchOption.TopDirectoryOnly)) - results.AddRange(Directory_GetDirectories(dir, filter, searchOptions)); - } - - return results.ToArray(); - } - - public override void File_WriteAllText(NPath path, string contents) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - { - base.File_WriteAllText(path, contents); - return; - } - - var longPath = MakeLongPath(path).TrimEnd('\\'); - using (var handle = CreateFileHandle(path, longPath, - Win32Native.CreationDisposition.CreateAlways, - Win32Native.FileAccess.GenericWrite, - FileShare.Read)) - { - using (var fs = new FileStream(handle, FileAccess.Write)) - { - using (var sw = new StreamWriter(fs)) - { - sw.Write(contents, new UTF8Encoding(false, true)); - } - } - } - } - - public override void File_WriteAllLines(NPath path, string[] contents) - { - File_WriteAllText(path, string.Join(Environment.NewLine, contents)); - } - - public override void File_WriteAllBytes(NPath path, byte[] bytes) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - { - base.File_WriteAllBytes(path, bytes); - return; - } - - if (!path.Parent.Exists()) - path.Parent.CreateDirectory(); - - var longPath = MakeLongPath(path).TrimEnd('\\'); - using (var handle = CreateFileHandle(path, longPath, - Win32Native.CreationDisposition.CreateAlways, - Win32Native.FileAccess.GenericWrite, - FileShare.Read)) - { - using (var fs = new FileStream(handle, FileAccess.Write)) - { - fs.Write(bytes, 0, bytes.Length); - } - } - } - - public override string File_ReadAllText(NPath path) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - return base.File_ReadAllText(path); - - var longPath = MakeLongPath(path).TrimEnd('\\'); - string contents; - using (var handle = CreateFileHandle(path, longPath, - Win32Native.CreationDisposition.OpenExisting, - Win32Native.FileAccess.GenericRead, - FileShare.Read)) - { - using (var fs = new FileStream(handle, FileAccess.Read)) - { - using (var sr = new StreamReader(fs, new UTF8Encoding(false, true))) - { - contents = sr.ReadToEnd(); - } - } - return contents; - } - } - - public override string[] File_ReadAllLines(NPath path) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - return base.File_ReadAllLines(path); - - var lines = new List(); - var longPath = MakeLongPath(path).TrimEnd('\\'); - using (var handle = CreateFileHandle(path, longPath, - Win32Native.CreationDisposition.OpenExisting, - Win32Native.FileAccess.GenericRead, - FileShare.Read)) - { - using (var fs = new FileStream(handle, FileAccess.Read)) - { - using (var sr = new StreamReader(fs, new UTF8Encoding(false, true))) - { - string line; - while ((line = sr.ReadLine()) != null) - lines.Add(line); - } - } - } - return lines.ToArray(); - } - - public override byte[] File_ReadAllBytes(NPath path) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - return base.File_ReadAllBytes(path); - - byte[] buffer = null; - var longPath = MakeLongPath(path).TrimEnd('\\'); - using (var handle = CreateFileHandle(path, longPath, - Win32Native.CreationDisposition.OpenExisting, - Win32Native.FileAccess.GenericRead, - FileShare.Read)) - { - using (var fs = new FileStream(handle, FileAccess.Read)) - { - buffer = new byte[fs.Length]; - fs.Read(buffer, 0, buffer.Length); - } - } - return buffer; - } - - public override void File_SetLastWriteTimeUtc(NPath path, DateTime lastWriteTimeUtc) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - { - base.File_SetLastWriteTimeUtc(path, lastWriteTimeUtc); - return; - } - - var longPath = MakeLongPath(path).TrimEnd('\\'); - using (var handle = CreateFileHandle(path, longPath, - Win32Native.CreationDisposition.OpenExisting, - Win32Native.FileAccess.FileWriteAttributes, - FileShare.ReadWrite)) - { - var d = lastWriteTimeUtc.ToFileTime(); - if (!Win32Native.SetLastWriteFileTime(handle.DangerousGetHandle(), IntPtr.Zero, IntPtr.Zero, ref d)) - throw new IOException($"Cannot set last write time to {path}.", new Win32Exception(Marshal.GetLastWin32Error())); - } - } - - public override DateTime File_GetLastWriteTimeUtc(NPath path) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - return base.File_GetLastWriteTimeUtc(path); - - Win32Native.FIND_DATA findData; - var longPath = MakeLongPath(path).TrimEnd('\\'); - IntPtr findHandle = Win32Native.FindFirstFile(longPath, out findData); - if (findHandle == new IntPtr(-1)) - return DateTime.MinValue; - - try - { - if (findHandle.ToInt64() == Win32Native.ERROR_FILE_NOT_FOUND) - return DateTime.MinValue; - - var utcFileTime = ((long)findData.ftLastWriteTime.dwHighDateTime << 32) + findData.ftLastWriteTime.dwLowDateTime; - return DateTime.FromFileTimeUtc(utcFileTime); - } - finally - { - Win32Native.FindClose(findHandle); - } - } - - public override void Directory_SetCurrentDirectory(NPath path) - { - if (path._path.Length < Win32Native.MAX_PATH_LEN) - { - base.Directory_SetCurrentDirectory(path); - return; - } - - - var shortPath = GetShortName(path); - if (!Win32Native.SetCurrentDirectory(shortPath)) - throw new IOException($"Cannot set current directory to {path}.", new Win32Exception(Marshal.GetLastWin32Error())); - } - - public override NPath Directory_GetCurrentDirectory() - { - NPath path = System.IO.Directory.GetCurrentDirectory(); - if (path._path.IndexOf('~') > 0) - return GetLongName(path); - return path; - } - - private static string GetShortName(NPath path) - { - var longPath = MakeLongPath(path).TrimEnd('\\'); - char[] shortPathChars = new char[Win32Native.MAX_PATH_LEN]; - var length = Win32Native.GetShortPathName(longPath, shortPathChars, (uint)shortPathChars.Length); - if (length <= 0) - throw new IOException($"Cannot get short path name for {path}.", new Win32Exception(Marshal.GetLastWin32Error())); - - var shortPath = new string(shortPathChars); - return shortPath.Substring(0, (int)length); - } - - private static string GetLongName(NPath path) - { - char[] longPathChars = null; - var length = Win32Native.GetLongPathName(path._path, longPathChars, 0); - if (length <= 0) - throw new IOException($"Cannot get long path name for {path}.", new Win32Exception(Marshal.GetLastWin32Error())); - - longPathChars = new char[length]; - if (Win32Native.GetLongPathName(path._path, longPathChars, length) <= 0) - throw new IOException($"Cannot get long path name for {path}.", new Win32Exception(Marshal.GetLastWin32Error())); - - var longPath = new string(longPathChars); - return longPath.Substring(0, (int)length - 1); - } - - private static SafeFileHandle CreateFileHandle( - NPath path, - string filePath, - Win32Native.CreationDisposition creationDisposition, - Win32Native.FileAccess fileAccess, - FileShare fileShare) - { - var fileHandle = Win32Native.CreateFile(filePath, fileAccess, fileShare, - IntPtr.Zero, - creationDisposition, - Win32Native.FileAttributes.Normal, - IntPtr.Zero); - if (fileHandle.IsInvalid) - { - var lastError = Marshal.GetLastWin32Error(); - if (lastError == Win32Native.ERROR_INVALID_NAME && path.FileName.Length > 255) - throw new PathTooLongException($"File name {path.FileName} exceeds limit of 255 characters."); - throw new IOException($"Cannot access {filePath}.", new Win32Exception(lastError)); - } - - return fileHandle; - } - - private static string MakeLongPath(NPath path, int maxLength = Win32Native.MAX_PATH_LEN) - { - NPath localPath = path._path; - if (localPath.IsRelative) - localPath = localPath.MakeAbsolute(); - - var longPath = localPath.ToString(SlashMode.Native); - if (string.IsNullOrEmpty(longPath) || longPath.StartsWith(@"\\?\")) - return longPath; - - if (longPath.Length >= maxLength) - return @"\\?\" + longPath; - - return longPath; - } - - static class Win32Native - { - public const long ERROR_FILE_NOT_FOUND = 2; - public const int ERROR_INVALID_NAME = 123; - public const int ERROR_DIR_NOT_EMPTY = 145; - - // See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation - public const int MAX_PATH_LEN = 260; - - [Flags] - public enum SymbolicLinkFlags - { - File = 0, - Directory = 1, - AllowUnprivilegedCreate = 2 - } - - public enum CreationDisposition : uint - { - New = 1, - CreateAlways = 2, - OpenExisting = 3, - OpenAlways = 4, - TruncateExisting = 5 - } - - [Flags] - public enum FileAccess : uint - { - GenericRead = 0x80000000, - GenericWrite = 0x40000000, - GenericExecute = 0x20000000, - GenericAll = 0x10000000, - FileReadAttributes = 0x80, - FileWriteAttributes = 0x100, - FileAppendData = 4 - } - - [Flags] - public enum FileAttributes : uint - { - Readonly = 0x00000001, - Hidden = 0x00000002, - System = 0x00000004, - Directory = 0x00000010, - Archive = 0x00000020, - Device = 0x00000040, - Normal = 0x00000080, - Temporary = 0x00000100, - SparseFile = 0x00000200, - ReparsePoint = 0x00000400, - Compressed = 0x00000800, - Offline = 0x00001000, - NotContentIndexed = 0x00002000, - Encrypted = 0x00004000, - Write_Through = 0x80000000, - Overlapped = 0x40000000, - NoBuffering = 0x20000000, - RandomAccess = 0x10000000, - SequentialScan = 0x08000000, - DeleteOnClose = 0x04000000, - BackupSemantics = 0x02000000, - PosixSemantics = 0x01000000, - OpenReparsePoint = 0x00200000, - OpenNoRecall = 0x00100000, - FirstPipeInstance = 0x00080000 - } - - [Flags] - public enum MoveFileExFlags : uint - { - None = 0x0, - ReplaceExisting = 0x1, - CopyAllowed = 0x2, - DelayUntilReboot = 0x4, - WriteThrough = 0x8, - CreateHardlink = 0x10, - FailIfNotTrackable = 0x20 - } - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLinkFlags dwFlags); - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct FIND_DATA - { - public FileAttributes dwFileAttributes; - public FILETIME ftCreationTime; - public FILETIME ftLastAccessTime; - public FILETIME ftLastWriteTime; - public uint nFileSizeHigh; - public uint nFileSizeLow; - public uint dwReserved0; - public uint dwReserved1; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] - public string cFileName; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] - public string cAlternateFileName; - } - - [DllImport(@"kernel32.dll", CharSet = CharSet.Unicode)] - public static extern IntPtr FindFirstFile(string fileName, out FIND_DATA findData); - - [DllImport(@"kernel32.dll", CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool FindNextFile(IntPtr handle, out FIND_DATA findData); - - [DllImport(@"kernel32.dll", CharSet = CharSet.Unicode)] - public static extern bool FindClose(IntPtr handle); - - public const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C; - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern uint GetFileAttributes(string fileName); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern void SetFileAttributes(string fileName, uint attributes); - - public const uint INVALID_FILE_ATTRIBUTES = 0xffffffff; - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool DeleteFile(string lpFileName); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CopyFile(string sourceFileName, string destFileName, bool failIfExists); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool MoveFile(string existingFileName, string newFileName); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool MoveFileEx(string existingFileName, string newFileName, MoveFileExFlags exFlags); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CreateDirectory(string lpPathName, IntPtr lpSecurityAttributes); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool RemoveDirectory(string lpPathName); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool SetCurrentDirectory(string lpPathName); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - internal static extern uint GetCurrentDirectory(uint nBufferLength, char[] lpBuffer); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern SafeFileHandle CreateFile( - string lpFileName, - FileAccess dwDesiredAccess, - FileShare dwShareMode, - IntPtr lpSecurityAttributes, - CreationDisposition dwCreationDisposition, - FileAttributes dwFlagsAndAttributes, - IntPtr hTemplateFile); - - [DllImport(@"kernel32.dll", EntryPoint = "SetFileTime", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool SetLastWriteFileTime( - IntPtr hFile, - IntPtr lpCreationTime, - IntPtr lpLastAccessTime, - ref long lpLastWriteTime); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern uint GetShortPathName( - string lpszLongPath, - char[] lpszShortPath, - uint cchBuffer); - - [DllImport(@"kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern uint GetLongPathName( - string lpszShortPath, - char[] lpszLongPath, - uint cchBuffer); - } - } - - class PosixFileSystem : SystemIOFileSystem - { - public override bool IsSymbolicLink(NPath path) - { - PosixNative.Stat stat; - if (PosixNative.lstat(path.ToString(SlashMode.Native), out stat) != 0) - { - var errorCode = Marshal.GetLastWin32Error(); - throw new IOException($"Failed to create stat path {this} (error code {errorCode})", errorCode); - } - - return PosixNative.S_ISLNK(stat.st_mode); - } - - public override void CreateSymbolicLink(NPath fromPath, NPath targetPath, bool targetIsFile) - { - int retVal = PosixNative.symlink(targetPath.ToString(SlashMode.Native), fromPath.ToString(SlashMode.Native)); - if (retVal != 0) - { - var errorCode = Marshal.GetLastWin32Error(); - throw new IOException($"Failed to create symlink (error code {errorCode})", errorCode); - } - } - - public override void File_SetAttributes(NPath path, FileAttributes value) - { - if ((value & FileAttributes.Hidden) != 0) - throw new NotSupportedException($"{FileAttributes.Hidden} file attribute is only supported on Windows."); - base.File_SetAttributes(path, value); - } - - static class PosixNative - { - [DllImport("libc", SetLastError = true)] - public static extern int symlink([MarshalAs(UnmanagedType.LPStr)] string targetPath, - [MarshalAs(UnmanagedType.LPStr)] string linkPath); - - // Notice that this is not a mapping for the normal 'stat' structure, but specifically for MonoPosixHelper's - // own Mono_Posix_Stat structure (in support/map.h). This means we don't need to worry about e.g. Darwin's - // 64bit inode stuff. - [StructLayout(LayoutKind.Sequential)] - public struct Stat - { - public ulong st_dev; // device - public ulong st_ino; // inode - public uint st_mode; // protection - [NonSerialized] -#pragma warning disable 169 - private uint _padding_; // padding for structure alignment -#pragma warning restore 169 - public ulong st_nlink; // number of hard links - public uint st_uid; // user ID of owner - public uint st_gid; // group ID of owner - public ulong st_rdev; // device type (if inode device) - public long st_size; // total size, in bytes - public long st_blksize; // blocksize for filesystem I/O - public long st_blocks; // number of blocks allocated - public long st_atime; // time of last access - public long st_mtime; // time of last modification - public long st_ctime; // time of last status change - public long st_atime_nsec; // Timespec.tv_nsec partner to st_atime - public long st_mtime_nsec; // Timespec.tv_nsec partner to st_mtime - public long st_ctime_nsec; // Timespec.tv_nsec partner to st_ctime - } - - [DllImport("MonoPosixHelper", SetLastError = true, EntryPoint = "Mono_Posix_Syscall_lstat")] - public static extern int lstat(string file_name, out Stat buf); - - private const uint Mono_Posix_FilePermissions_S_IFLNK = 0x0000a000; - - public static bool S_ISLNK(uint m) - { - return (m & Mono_Posix_FilePermissions_S_IFLNK) == Mono_Posix_FilePermissions_S_IFLNK; - } - } - } - - /// - /// A Filesystem that forwards all calls to another filesytem. Derive your own filesystem from this if you only want to - /// change the behaviour of a few methods. - /// - public abstract class RelayingFileSystem : FileSystem - { - /// - /// The filesystem all methods will be forwarded to - /// - protected FileSystem BaseFileSystem { get; } - - /// - /// Constructor - /// - /// the filesystem all calls will be forwarded to - protected RelayingFileSystem(NPath.FileSystem baseFileSystem) => BaseFileSystem = baseFileSystem; - /// - public override NPath[] Directory_GetFiles(NPath path, string filter, SearchOption searchOptions) => BaseFileSystem.Directory_GetFiles(path, filter, searchOptions); - /// - public override bool Directory_Exists(NPath path) => BaseFileSystem.Directory_Exists(path); - /// - public override bool File_Exists(NPath path) => BaseFileSystem.File_Exists(path); - /// - public override void File_WriteAllBytes(NPath path, byte[] bytes) => BaseFileSystem.File_WriteAllBytes(path, bytes); - /// - public override void File_Copy(NPath path, NPath destinationPath, bool overWrite) => BaseFileSystem.File_Copy(path, destinationPath, overWrite); - /// - public override void File_Delete(NPath path) => BaseFileSystem.File_Delete(path); - /// - public override void File_Move(NPath path, NPath destinationPath) => BaseFileSystem.File_Move(path, destinationPath); - /// - public override void File_WriteAllText(NPath path, string contents) => BaseFileSystem.File_WriteAllText(path, contents); - /// - public override string File_ReadAllText(NPath path) => BaseFileSystem.File_ReadAllText(path); - /// - public override void File_WriteAllLines(NPath path, string[] contents) => BaseFileSystem.File_WriteAllLines(path, contents); - /// - public override string[] File_ReadAllLines(NPath path) => BaseFileSystem.File_ReadAllLines(path); - /// - public override byte[] File_ReadAllBytes(NPath path) => BaseFileSystem.File_ReadAllBytes(path); - /// - public override void File_SetLastWriteTimeUtc(NPath path, DateTime lastWriteTimeUtc) => BaseFileSystem.File_SetLastWriteTimeUtc(path, lastWriteTimeUtc); - /// - public override DateTime File_GetLastWriteTimeUtc(NPath path) => BaseFileSystem.File_GetLastWriteTimeUtc(path); - /// - public override void File_SetAttributes(NPath path, FileAttributes value) => BaseFileSystem.File_SetAttributes(path, value); - /// - public override FileAttributes File_GetAttributes(NPath path) => BaseFileSystem.File_GetAttributes(path); - /// - public override long File_GetSize(NPath path) => BaseFileSystem.File_GetSize(path); - /// - public override void Directory_CreateDirectory(NPath path) => BaseFileSystem.Directory_CreateDirectory(path); - /// - public override void Directory_Delete(NPath path, bool b) => BaseFileSystem.Directory_Delete(path, b); - /// - public override void Directory_Move(NPath path, NPath destPath) => BaseFileSystem.Directory_Move(path, destPath); - /// - public override NPath Directory_GetCurrentDirectory() => BaseFileSystem.Directory_GetCurrentDirectory(); - /// - public override void Directory_SetCurrentDirectory(NPath directoryPath) => BaseFileSystem.Directory_SetCurrentDirectory(directoryPath); - /// - public override NPath[] Directory_GetDirectories(NPath path, string filter, SearchOption searchOptions) => BaseFileSystem.Directory_GetDirectories(path, filter, searchOptions); - - /// - public override NPath Resolve(NPath path) => BaseFileSystem.Resolve(path); - /// - public override bool IsSymbolicLink(NPath path) => BaseFileSystem.IsSymbolicLink(path); - /// - public override void CreateSymbolicLink(NPath fromPath, NPath targetPath, bool targetIsFile) => BaseFileSystem.CreateSymbolicLink(fromPath, targetPath, targetIsFile); - } - - class WithFileSystemHelper : IDisposable - { - private FileSystem _previousFileSystem; - private FileSystem _newFileSystem; - - public WithFileSystemHelper(FileSystem newFileSystem) - { - _previousFileSystem = FileSystem.Active; - _newFileSystem = newFileSystem; - FileSystem._active = newFileSystem; - } - - public void Dispose() - { - if (FileSystem._active != _newFileSystem) - throw new InvalidOperationException("While disposing WithFileSystem result, the originally set FileSystem was not the active one."); - FileSystem._active = _previousFileSystem; - } - } - - /// - /// Temporarily assume that the current directory is a given value, instead of querying it from the environment when needed, in order to improve performance. - /// - /// The current directory to assume. - /// A token representing the registered callback. This should be disposed of when the assumption is no longer required. The usual usage pattern is to capture the token with a using statement, such that it is automatically disposed of when the using block exits. - [Obsolete("Obsolete. If you need this behaviour you can implement a custom NPath.FileSystem and install it with NPath.WithFileSystem()", true)] - public static IDisposable WithFrozenCurrentDirectory(NPath frozenCurrentDirectory) => throw null; - } - - /// - /// NPath-related extension methods for other common types. - /// -#if NICEIO_PUBLIC - public static class Extensions -#else - internal static class Extensions -#endif - { - /// - /// Copy these NPaths into the given directory. - /// - /// An enumerable sequence of NPaths. - /// The path to the target directory. - /// The paths to the newly copied files. - /// All path information in the source paths is ignored, other than the final file name; the resulting copied files and directories will all be immediate children of the target directory. - public static IEnumerable Copy(this IEnumerable self, NPath dest) - { - if (dest.IsRelative) - throw new ArgumentException("When copying multiple files, the destination cannot be a relative path"); - dest.EnsureDirectoryExists(); - return self.Select(p => p.Copy(dest.Combine(p.FileName))).ToArray(); - } - - /// - /// Move these NPaths into the given directory. - /// - /// An enumerable sequence of NPaths. - /// The path to the target directory. - /// The paths to the newly moved files. - /// All path information in the source paths is ignored, other than the final file name; the resulting moved files and directories will all be immediate children of the target directory. - public static IEnumerable Move(this IEnumerable self, NPath dest) - { - if (dest.IsRelative) - throw new ArgumentException("When moving multiple files, the destination cannot be a relative path"); - dest.EnsureDirectoryExists(); - return self.Select(p => p.Move(dest.Combine(p.FileName))).ToArray(); - } - - /// - /// Delete the files/directories targetted by these paths. - /// - /// The paths to delete. - /// All paths that were passed in to the method. - public static IEnumerable Delete(this IEnumerable self) - { - foreach (var p in self) - p.Delete(); - return self; - } - - /// - /// Convert all these paths to quoted strings, using the requested path separator type. - /// - /// The paths to convert. - /// The path separator type to use. Defaults to SlashMode.Forward. - /// The paths, converted to quoted strings. - public static IEnumerable InQuotes(this IEnumerable self, - SlashMode slashMode = SlashMode.Forward) - { - return self.Select(p => p.InQuotes(slashMode)); - } - - /// - /// Construct a new NPath from this string. - /// - /// The string to construct the path from. - /// A new NPath constructed from this string. - public static NPath ToNPath(this string path) - { - return new NPath(path); - } - - /// - /// Construct new NPaths from each of these strings. - /// - /// The strings to construct NPaths from. - /// The newly constructed NPaths. - public static IEnumerable ToNPaths(this IEnumerable paths) - { - return paths.Select(p => new NPath(p)); - } - - /// - /// Invokes .ResolveWithFileSystem on all NPaths - /// - /// - /// - public static NPath[] ResolveWithFileSystem(this IEnumerable paths) => paths.Select(p => p.ResolveWithFileSystem()).ToArray(); - - /// - /// Invokes InQuotesResolved on all NPaths - /// - /// - /// - public static string[] InQuotesResolved(this IEnumerable paths) => paths.Select(p => p.InQuotesResolved()).ToArray(); - } - - /// - /// Describes the different kinds of path separators that can be used when converting NPaths back into strings. - /// -#if NICEIO_PUBLIC - public enum SlashMode -#else - internal enum SlashMode -#endif - { - /// - /// Use the slash mode that is native for the current platform - backslashes on Windows, forward slashes on macOS and Linux systems. - /// - Native, - - /// - /// Use forward slashes as path separators. - /// - Forward, - - /// - /// Use backslashes as path separators. - /// - Backward - } - - /// - /// Specifies the way that directory deletion should be performed. - /// -#if NICEIO_PUBLIC - public enum DeleteMode -#else - internal enum DeleteMode -#endif - { - /// - /// When deleting a directory, if an IOException occurs, rethrow it. - /// - Normal, - - /// - /// When deleting a directory, if an IOException occurs, ignore it. The deletion request may or may not be later fulfilled by the OS. - /// - Soft - } - - internal struct Do_Not_Use_File_Directly_Use_FileSystem_Active_Instead - { - } - internal struct Do_Not_Use_Directory_Directly_Use_FileSystem_Active_Instead - { - } -} diff --git a/Editor/NiceIO.cs.meta b/Editor/NiceIO.cs.meta deleted file mode 100644 index a8642c8..0000000 --- a/Editor/NiceIO.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 60b63fc17c8e949bab647361ad723f8e \ No newline at end of file diff --git a/Editor/ZedDiscovery.cs b/Editor/ZedDiscovery.cs index d64230c..daad5fd 100644 --- a/Editor/ZedDiscovery.cs +++ b/Editor/ZedDiscovery.cs @@ -4,7 +4,6 @@ using System.IO; using System.Xml.XPath; using System.Text; -using NiceIO; namespace UnityZed { @@ -14,7 +13,7 @@ public CodeEditor.Installation[] GetInstallations() { var results = new List(); - var candidates = new List<(NPath path, TryGetVersion tryGetVersion)> { + var candidates = new List<(string path, TryGetVersion tryGetVersion)> { // [MacOS] ("/Applications/Zed.app/Contents/MacOS/cli", TryGetVersionFromPlist), @@ -34,7 +33,7 @@ public CodeEditor.Installation[] GetInstallations() ("/etc/profiles/per-user/linx/bin/zeditor", null), // [Linux] (Official Website) - (NPath.HomeDirectory.Combine(".local/bin/zed"), null), + (Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "zed"), null), }; #if UNITY_EDITOR_WIN @@ -77,7 +76,7 @@ public CodeEditor.Installation[] GetInstallations() var candidatePath = candidate.path; var candidateTryGetVersion = candidate.tryGetVersion ?? TryGetVersionFallback; - if (candidatePath.FileExists()) + if (File.Exists(candidatePath)) { var name = new StringBuilder("Zed"); @@ -87,7 +86,7 @@ public CodeEditor.Installation[] GetInstallations() results.Add(new() { Name = name.ToString(), - Path = candidatePath.MakeAbsolute().ToString(), + Path = Path.GetFullPath(candidatePath), }); break; @@ -115,23 +114,23 @@ public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installa // // TryGetVersion implementations // - private delegate bool TryGetVersion(NPath path, out string vertion); + private delegate bool TryGetVersion(string path, out string version); - private static bool TryGetVersionFallback(NPath path, out string version) + private static bool TryGetVersionFallback(string path, out string version) { version = null; return false; } - private static bool TryGetVersionFromPlist(NPath path, out string version) + private static bool TryGetVersionFromPlist(string path, out string version) { version = null; - var plistPath = path.Combine("../../").Combine("Info.plist"); - if (plistPath.FileExists() == false) + var plistPath = Path.GetFullPath(Path.Combine(path, "..", "..", "Info.plist")); + if (File.Exists(plistPath) == false) return false; - var xPath = new XPathDocument(plistPath.ToString()); + var xPath = new XPathDocument(plistPath); var xNavigator = xPath.CreateNavigator().SelectSingleNode("/plist/dict/key[text()='CFBundleShortVersionString']/following-sibling::string[1]/text()"); if (xNavigator == null) return false; @@ -140,4 +139,4 @@ private static bool TryGetVersionFromPlist(NPath path, out string version) return true; } } -} +} \ No newline at end of file diff --git a/Editor/ZedProcess.cs b/Editor/ZedProcess.cs index b87eb44..1882401 100644 --- a/Editor/ZedProcess.cs +++ b/Editor/ZedProcess.cs @@ -1,19 +1,19 @@ using System.Diagnostics; +using System.IO; using System.Text; -using NiceIO; using UnityEngine; namespace UnityZed { public class ZedProcess { - private readonly NPath m_ExecPath; - private readonly NPath m_ProjectPath; + private readonly string m_ExecPath; + private readonly string m_ProjectPath; public ZedProcess(string execPath) { m_ExecPath = execPath; - m_ProjectPath = new NPath(Application.dataPath).Parent; + m_ProjectPath = Directory.GetParent(Application.dataPath).FullName; } public bool OpenProject(string filePath = "", int line = -1, int column = -1) @@ -44,7 +44,7 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1) { var startInfo = new ProcessStartInfo { - FileName = m_ExecPath.ToString(), + FileName = m_ExecPath, Arguments = args.ToString(), UseShellExecute = false, CreateNoWindow = true, @@ -60,4 +60,4 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1) } } } -} +} \ No newline at end of file diff --git a/Editor/ZedSettings.cs b/Editor/ZedSettings.cs index 09b64a5..076dcfd 100644 --- a/Editor/ZedSettings.cs +++ b/Editor/ZedSettings.cs @@ -1,26 +1,25 @@ using UnityEngine; -using NiceIO; using SimpleJSON; -using System; +using System.IO; namespace UnityZed { public class ZedSettings { - private readonly NPath m_SettingsPath; + private readonly string m_SettingsPath; public ZedSettings() { - m_SettingsPath = new NPath(Application.dataPath).Parent.Combine(".zed/settings.json"); + m_SettingsPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, ".zed", "settings.json"); } public void Sync() { - if (m_SettingsPath.FileExists() == false) + if (File.Exists(m_SettingsPath) == false) { - Debug.Log("Zed settings file not found, creating default settings file."); - m_SettingsPath.CreateFile(); - m_SettingsPath.WriteAllText(JSON.Parse(kDefaultSettings).ToString()); + UnityEngine.Debug.Log("Zed settings file not found, creating default settings file."); + Directory.CreateDirectory(Path.GetDirectoryName(m_SettingsPath)); + File.WriteAllText(m_SettingsPath, JSON.Parse(kDefaultSettings).ToString()); } } @@ -87,4 +86,4 @@ public void Sync() ] }"; } -} +} \ No newline at end of file From 449c6920feb75a601b62f7f700880b7e7a470ca1 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Wed, 4 Mar 2026 22:06:34 +0100 Subject: [PATCH 07/21] removed simpleJson --- Editor/SimpleJSON.cs | 1434 ------------------------------- Editor/SimpleJSON.cs.meta | 2 - Editor/ZedExternalCodeEditor.cs | 4 +- Editor/ZedSettings.cs | 23 +- 4 files changed, 10 insertions(+), 1453 deletions(-) delete mode 100644 Editor/SimpleJSON.cs delete mode 100644 Editor/SimpleJSON.cs.meta diff --git a/Editor/SimpleJSON.cs b/Editor/SimpleJSON.cs deleted file mode 100644 index 0501553..0000000 --- a/Editor/SimpleJSON.cs +++ /dev/null @@ -1,1434 +0,0 @@ -/* * * * * - * A simple JSON Parser / builder - * ------------------------------ - * - * It mainly has been written as a simple JSON parser. It can build a JSON string - * from the node-tree, or generate a node tree from any valid JSON string. - * - * Written by Bunny83 - * 2012-06-09 - * - * Changelog now external. See Changelog.txt - * - * The MIT License (MIT) - * - * Copyright (c) 2012-2022 Markus Göbel (Bunny83) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - * * * * */ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; - -namespace SimpleJSON -{ - public enum JSONNodeType - { - Array = 1, - Object = 2, - String = 3, - Number = 4, - NullValue = 5, - Boolean = 6, - None = 7, - Custom = 0xFF, - } - public enum JSONTextMode - { - Compact, - Indent - } - - public abstract partial class JSONNode - { - #region Enumerators - public struct Enumerator - { - private enum Type { None, Array, Object } - private Type type; - private Dictionary.Enumerator m_Object; - private List.Enumerator m_Array; - public bool IsValid { get { return type != Type.None; } } - public Enumerator(List.Enumerator aArrayEnum) - { - type = Type.Array; - m_Object = default(Dictionary.Enumerator); - m_Array = aArrayEnum; - } - public Enumerator(Dictionary.Enumerator aDictEnum) - { - type = Type.Object; - m_Object = aDictEnum; - m_Array = default(List.Enumerator); - } - public KeyValuePair Current - { - get - { - if (type == Type.Array) - return new KeyValuePair(string.Empty, m_Array.Current); - else if (type == Type.Object) - return m_Object.Current; - return new KeyValuePair(string.Empty, null); - } - } - public bool MoveNext() - { - if (type == Type.Array) - return m_Array.MoveNext(); - else if (type == Type.Object) - return m_Object.MoveNext(); - return false; - } - } - public struct ValueEnumerator - { - private Enumerator m_Enumerator; - public ValueEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } - public ValueEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } - public ValueEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } - public JSONNode Current { get { return m_Enumerator.Current.Value; } } - public bool MoveNext() { return m_Enumerator.MoveNext(); } - public ValueEnumerator GetEnumerator() { return this; } - } - public struct KeyEnumerator - { - private Enumerator m_Enumerator; - public KeyEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } - public KeyEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } - public KeyEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } - public string Current { get { return m_Enumerator.Current.Key; } } - public bool MoveNext() { return m_Enumerator.MoveNext(); } - public KeyEnumerator GetEnumerator() { return this; } - } - - public class LinqEnumerator : IEnumerator>, IEnumerable> - { - private JSONNode m_Node; - private Enumerator m_Enumerator; - internal LinqEnumerator(JSONNode aNode) - { - m_Node = aNode; - if (m_Node != null) - m_Enumerator = m_Node.GetEnumerator(); - } - public KeyValuePair Current { get { return m_Enumerator.Current; } } - object IEnumerator.Current { get { return m_Enumerator.Current; } } - public bool MoveNext() { return m_Enumerator.MoveNext(); } - - public void Dispose() - { - m_Node = null; - m_Enumerator = new Enumerator(); - } - - public IEnumerator> GetEnumerator() - { - return new LinqEnumerator(m_Node); - } - - public void Reset() - { - if (m_Node != null) - m_Enumerator = m_Node.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new LinqEnumerator(m_Node); - } - } - - #endregion Enumerators - - #region common interface - - public static bool forceASCII = false; // Use Unicode by default - public static bool longAsString = false; // lazy creator creates a JSONString instead of JSONNumber - public static bool allowLineComments = true; // allow "//"-style comments at the end of a line - - public abstract JSONNodeType Tag { get; } - - public virtual JSONNode this[int aIndex] { get { return null; } set { } } - - public virtual JSONNode this[string aKey] { get { return null; } set { } } - - public virtual string Value { get { return ""; } set { } } - - public virtual int Count { get { return 0; } } - - public virtual bool IsNumber { get { return false; } } - public virtual bool IsString { get { return false; } } - public virtual bool IsBoolean { get { return false; } } - public virtual bool IsNull { get { return false; } } - public virtual bool IsArray { get { return false; } } - public virtual bool IsObject { get { return false; } } - - public virtual bool Inline { get { return false; } set { } } - - public virtual void Add(string aKey, JSONNode aItem) - { - } - public virtual void Add(JSONNode aItem) - { - Add("", aItem); - } - - public virtual JSONNode Remove(string aKey) - { - return null; - } - - public virtual JSONNode Remove(int aIndex) - { - return null; - } - - public virtual JSONNode Remove(JSONNode aNode) - { - return aNode; - } - public virtual void Clear() { } - - public virtual JSONNode Clone() - { - return null; - } - - public virtual IEnumerable Children - { - get - { - yield break; - } - } - - public IEnumerable DeepChildren - { - get - { - foreach (var C in Children) - foreach (var D in C.DeepChildren) - yield return D; - } - } - - public virtual bool HasKey(string aKey) - { - return false; - } - - public virtual JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) - { - return aDefault; - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - WriteToStringBuilder(sb, 0, 0, JSONTextMode.Compact); - return sb.ToString(); - } - - public virtual string ToString(int aIndent) - { - StringBuilder sb = new StringBuilder(); - WriteToStringBuilder(sb, 0, aIndent, JSONTextMode.Indent); - return sb.ToString(); - } - internal abstract void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode); - - public abstract Enumerator GetEnumerator(); - public IEnumerable> Linq { get { return new LinqEnumerator(this); } } - public KeyEnumerator Keys { get { return new KeyEnumerator(GetEnumerator()); } } - public ValueEnumerator Values { get { return new ValueEnumerator(GetEnumerator()); } } - - #endregion common interface - - #region typecasting properties - - - public virtual double AsDouble - { - get - { - double v = 0.0; - if (double.TryParse(Value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) - return v; - return 0.0; - } - set - { - Value = value.ToString(CultureInfo.InvariantCulture); - } - } - - public virtual int AsInt - { - get { return (int)AsDouble; } - set { AsDouble = value; } - } - - public virtual float AsFloat - { - get { return (float)AsDouble; } - set { AsDouble = value; } - } - - public virtual bool AsBool - { - get - { - bool v = false; - if (bool.TryParse(Value, out v)) - return v; - return !string.IsNullOrEmpty(Value); - } - set - { - Value = (value) ? "true" : "false"; - } - } - - public virtual long AsLong - { - get - { - long val = 0; - if (long.TryParse(Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) - return val; - return 0L; - } - set - { - Value = value.ToString(CultureInfo.InvariantCulture); - } - } - - public virtual ulong AsULong - { - get - { - ulong val = 0; - if (ulong.TryParse(Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) - return val; - return 0; - } - set - { - Value = value.ToString(CultureInfo.InvariantCulture); - } - } - - public virtual JSONArray AsArray - { - get - { - return this as JSONArray; - } - } - - public virtual JSONObject AsObject - { - get - { - return this as JSONObject; - } - } - - - #endregion typecasting properties - - #region operators - - public static implicit operator JSONNode(string s) - { - return (s == null) ? (JSONNode)JSONNull.CreateOrGet() : new JSONString(s); - } - public static implicit operator string(JSONNode d) - { - return (d == null) ? null : d.Value; - } - - public static implicit operator JSONNode(double n) - { - return new JSONNumber(n); - } - public static implicit operator double(JSONNode d) - { - return (d == null) ? 0 : d.AsDouble; - } - - public static implicit operator JSONNode(float n) - { - return new JSONNumber(n); - } - public static implicit operator float(JSONNode d) - { - return (d == null) ? 0 : d.AsFloat; - } - - public static implicit operator JSONNode(int n) - { - return new JSONNumber(n); - } - public static implicit operator int(JSONNode d) - { - return (d == null) ? 0 : d.AsInt; - } - - public static implicit operator JSONNode(long n) - { - if (longAsString) - return new JSONString(n.ToString(CultureInfo.InvariantCulture)); - return new JSONNumber(n); - } - public static implicit operator long(JSONNode d) - { - return (d == null) ? 0L : d.AsLong; - } - - public static implicit operator JSONNode(ulong n) - { - if (longAsString) - return new JSONString(n.ToString(CultureInfo.InvariantCulture)); - return new JSONNumber(n); - } - public static implicit operator ulong(JSONNode d) - { - return (d == null) ? 0 : d.AsULong; - } - - public static implicit operator JSONNode(bool b) - { - return new JSONBool(b); - } - public static implicit operator bool(JSONNode d) - { - return (d == null) ? false : d.AsBool; - } - - public static implicit operator JSONNode(KeyValuePair aKeyValue) - { - return aKeyValue.Value; - } - - public static bool operator ==(JSONNode a, object b) - { - if (ReferenceEquals(a, b)) - return true; - bool aIsNull = a is JSONNull || ReferenceEquals(a, null) || a is JSONLazyCreator; - bool bIsNull = b is JSONNull || ReferenceEquals(b, null) || b is JSONLazyCreator; - if (aIsNull && bIsNull) - return true; - return !aIsNull && a.Equals(b); - } - - public static bool operator !=(JSONNode a, object b) - { - return !(a == b); - } - - public override bool Equals(object obj) - { - return ReferenceEquals(this, obj); - } - - public override int GetHashCode() - { - return base.GetHashCode(); - } - - #endregion operators - - [ThreadStatic] - private static StringBuilder m_EscapeBuilder; - internal static StringBuilder EscapeBuilder - { - get - { - if (m_EscapeBuilder == null) - m_EscapeBuilder = new StringBuilder(); - return m_EscapeBuilder; - } - } - internal static string Escape(string aText) - { - var sb = EscapeBuilder; - sb.Length = 0; - if (sb.Capacity < aText.Length + aText.Length / 10) - sb.Capacity = aText.Length + aText.Length / 10; - foreach (char c in aText) - { - switch (c) - { - case '\\': - sb.Append("\\\\"); - break; - case '\"': - sb.Append("\\\""); - break; - case '\n': - sb.Append("\\n"); - break; - case '\r': - sb.Append("\\r"); - break; - case '\t': - sb.Append("\\t"); - break; - case '\b': - sb.Append("\\b"); - break; - case '\f': - sb.Append("\\f"); - break; - default: - if (c < ' ' || (forceASCII && c > 127)) - { - ushort val = c; - sb.Append("\\u").Append(val.ToString("X4")); - } - else - sb.Append(c); - break; - } - } - string result = sb.ToString(); - sb.Length = 0; - return result; - } - - private static JSONNode ParseElement(string token, bool quoted) - { - if (quoted) - return token; - if (token.Length <= 5) - { - string tmp = token.ToLower(); - if (tmp == "false" || tmp == "true") - return tmp == "true"; - if (tmp == "null") - return JSONNull.CreateOrGet(); - } - double val; - if (double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out val)) - return val; - else - return token; - } - - public static JSONNode Parse(string aJSON) - { - Stack stack = new Stack(); - JSONNode ctx = null; - int i = 0; - StringBuilder Token = new StringBuilder(); - string TokenName = ""; - bool QuoteMode = false; - bool TokenIsQuoted = false; - bool HasNewlineChar = false; - while (i < aJSON.Length) - { - switch (aJSON[i]) - { - case '{': - if (QuoteMode) - { - Token.Append(aJSON[i]); - break; - } - stack.Push(new JSONObject()); - if (ctx != null) - { - ctx.Add(TokenName, stack.Peek()); - } - TokenName = ""; - Token.Length = 0; - ctx = stack.Peek(); - HasNewlineChar = false; - break; - - case '[': - if (QuoteMode) - { - Token.Append(aJSON[i]); - break; - } - - stack.Push(new JSONArray()); - if (ctx != null) - { - ctx.Add(TokenName, stack.Peek()); - } - TokenName = ""; - Token.Length = 0; - ctx = stack.Peek(); - HasNewlineChar = false; - break; - - case '}': - case ']': - if (QuoteMode) - { - - Token.Append(aJSON[i]); - break; - } - if (stack.Count == 0) - throw new Exception("JSON Parse: Too many closing brackets"); - - stack.Pop(); - if (Token.Length > 0 || TokenIsQuoted) - ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); - if (ctx != null) - ctx.Inline = !HasNewlineChar; - TokenIsQuoted = false; - TokenName = ""; - Token.Length = 0; - if (stack.Count > 0) - ctx = stack.Peek(); - break; - - case ':': - if (QuoteMode) - { - Token.Append(aJSON[i]); - break; - } - TokenName = Token.ToString(); - Token.Length = 0; - TokenIsQuoted = false; - break; - - case '"': - QuoteMode ^= true; - TokenIsQuoted |= QuoteMode; - break; - - case ',': - if (QuoteMode) - { - Token.Append(aJSON[i]); - break; - } - if (Token.Length > 0 || TokenIsQuoted) - ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); - TokenIsQuoted = false; - TokenName = ""; - Token.Length = 0; - TokenIsQuoted = false; - break; - - case '\r': - case '\n': - HasNewlineChar = true; - break; - - case ' ': - case '\t': - if (QuoteMode) - Token.Append(aJSON[i]); - break; - - case '\\': - ++i; - if (QuoteMode) - { - char C = aJSON[i]; - switch (C) - { - case 't': - Token.Append('\t'); - break; - case 'r': - Token.Append('\r'); - break; - case 'n': - Token.Append('\n'); - break; - case 'b': - Token.Append('\b'); - break; - case 'f': - Token.Append('\f'); - break; - case 'u': - { - string s = aJSON.Substring(i + 1, 4); - Token.Append((char)int.Parse( - s, - System.Globalization.NumberStyles.AllowHexSpecifier)); - i += 4; - break; - } - default: - Token.Append(C); - break; - } - } - break; - case '/': - if (allowLineComments && !QuoteMode && i + 1 < aJSON.Length && aJSON[i + 1] == '/') - { - while (++i < aJSON.Length && aJSON[i] != '\n' && aJSON[i] != '\r') ; - break; - } - Token.Append(aJSON[i]); - break; - case '\uFEFF': // remove / ignore BOM (Byte Order Mark) - break; - - default: - Token.Append(aJSON[i]); - break; - } - ++i; - } - if (QuoteMode) - { - throw new Exception("JSON Parse: Quotation marks seems to be messed up."); - } - if (ctx == null) - return ParseElement(Token.ToString(), TokenIsQuoted); - return ctx; - } - - } - // End of JSONNode - - public partial class JSONArray : JSONNode - { - private List m_List = new List(); - private bool inline = false; - public override bool Inline - { - get { return inline; } - set { inline = value; } - } - - public override JSONNodeType Tag { get { return JSONNodeType.Array; } } - public override bool IsArray { get { return true; } } - public override Enumerator GetEnumerator() { return new Enumerator(m_List.GetEnumerator()); } - - public override JSONNode this[int aIndex] - { - get - { - if (aIndex < 0 || aIndex >= m_List.Count) - return new JSONLazyCreator(this); - return m_List[aIndex]; - } - set - { - if (value == null) - value = JSONNull.CreateOrGet(); - if (aIndex < 0 || aIndex >= m_List.Count) - m_List.Add(value); - else - m_List[aIndex] = value; - } - } - - public override JSONNode this[string aKey] - { - get { return new JSONLazyCreator(this); } - set - { - if (value == null) - value = JSONNull.CreateOrGet(); - m_List.Add(value); - } - } - - public override int Count - { - get { return m_List.Count; } - } - - public override void Add(string aKey, JSONNode aItem) - { - if (aItem == null) - aItem = JSONNull.CreateOrGet(); - m_List.Add(aItem); - } - - public override JSONNode Remove(int aIndex) - { - if (aIndex < 0 || aIndex >= m_List.Count) - return null; - JSONNode tmp = m_List[aIndex]; - m_List.RemoveAt(aIndex); - return tmp; - } - - public override JSONNode Remove(JSONNode aNode) - { - m_List.Remove(aNode); - return aNode; - } - - public override void Clear() - { - m_List.Clear(); - } - - public override JSONNode Clone() - { - var node = new JSONArray(); - node.m_List.Capacity = m_List.Capacity; - foreach (var n in m_List) - { - if (n != null) - node.Add(n.Clone()); - else - node.Add(null); - } - return node; - } - - public override IEnumerable Children - { - get - { - foreach (JSONNode N in m_List) - yield return N; - } - } - - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append('['); - int count = m_List.Count; - if (inline) - aMode = JSONTextMode.Compact; - for (int i = 0; i < count; i++) - { - if (i > 0) - aSB.Append(','); - if (aMode == JSONTextMode.Indent) - aSB.AppendLine(); - - if (aMode == JSONTextMode.Indent) - aSB.Append(' ', aIndent + aIndentInc); - m_List[i].WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); - } - if (aMode == JSONTextMode.Indent) - aSB.AppendLine().Append(' ', aIndent); - aSB.Append(']'); - } - } - // End of JSONArray - - public partial class JSONObject : JSONNode - { - private Dictionary m_Dict = new Dictionary(); - - private bool inline = false; - public override bool Inline - { - get { return inline; } - set { inline = value; } - } - - public override JSONNodeType Tag { get { return JSONNodeType.Object; } } - public override bool IsObject { get { return true; } } - - public override Enumerator GetEnumerator() { return new Enumerator(m_Dict.GetEnumerator()); } - - - public override JSONNode this[string aKey] - { - get - { - if (m_Dict.ContainsKey(aKey)) - return m_Dict[aKey]; - else - return new JSONLazyCreator(this, aKey); - } - set - { - if (value == null) - value = JSONNull.CreateOrGet(); - if (m_Dict.ContainsKey(aKey)) - m_Dict[aKey] = value; - else - m_Dict.Add(aKey, value); - } - } - - public override JSONNode this[int aIndex] - { - get - { - if (aIndex < 0 || aIndex >= m_Dict.Count) - return null; - return m_Dict.ElementAt(aIndex).Value; - } - set - { - if (value == null) - value = JSONNull.CreateOrGet(); - if (aIndex < 0 || aIndex >= m_Dict.Count) - return; - string key = m_Dict.ElementAt(aIndex).Key; - m_Dict[key] = value; - } - } - - public override int Count - { - get { return m_Dict.Count; } - } - - public override void Add(string aKey, JSONNode aItem) - { - if (aItem == null) - aItem = JSONNull.CreateOrGet(); - - if (aKey != null) - { - if (m_Dict.ContainsKey(aKey)) - m_Dict[aKey] = aItem; - else - m_Dict.Add(aKey, aItem); - } - else - m_Dict.Add(Guid.NewGuid().ToString(), aItem); - } - - public override JSONNode Remove(string aKey) - { - if (!m_Dict.ContainsKey(aKey)) - return null; - JSONNode tmp = m_Dict[aKey]; - m_Dict.Remove(aKey); - return tmp; - } - - public override JSONNode Remove(int aIndex) - { - if (aIndex < 0 || aIndex >= m_Dict.Count) - return null; - var item = m_Dict.ElementAt(aIndex); - m_Dict.Remove(item.Key); - return item.Value; - } - - public override JSONNode Remove(JSONNode aNode) - { - try - { - var item = m_Dict.Where(k => k.Value == aNode).First(); - m_Dict.Remove(item.Key); - return aNode; - } - catch - { - return null; - } - } - - public override void Clear() - { - m_Dict.Clear(); - } - - public override JSONNode Clone() - { - var node = new JSONObject(); - foreach (var n in m_Dict) - { - node.Add(n.Key, n.Value.Clone()); - } - return node; - } - - public override bool HasKey(string aKey) - { - return m_Dict.ContainsKey(aKey); - } - - public override JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) - { - JSONNode res; - if (m_Dict.TryGetValue(aKey, out res)) - return res; - return aDefault; - } - - public override IEnumerable Children - { - get - { - foreach (KeyValuePair N in m_Dict) - yield return N.Value; - } - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append('{'); - bool first = true; - if (inline) - aMode = JSONTextMode.Compact; - foreach (var k in m_Dict) - { - if (!first) - aSB.Append(','); - first = false; - if (aMode == JSONTextMode.Indent) - aSB.AppendLine(); - if (aMode == JSONTextMode.Indent) - aSB.Append(' ', aIndent + aIndentInc); - aSB.Append('\"').Append(Escape(k.Key)).Append('\"'); - if (aMode == JSONTextMode.Compact) - aSB.Append(':'); - else - aSB.Append(" : "); - k.Value.WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); - } - if (aMode == JSONTextMode.Indent) - aSB.AppendLine().Append(' ', aIndent); - aSB.Append('}'); - } - - } - // End of JSONObject - - public partial class JSONString : JSONNode - { - private string m_Data; - - public override JSONNodeType Tag { get { return JSONNodeType.String; } } - public override bool IsString { get { return true; } } - - public override Enumerator GetEnumerator() { return new Enumerator(); } - - - public override string Value - { - get { return m_Data; } - set - { - m_Data = value; - } - } - - public JSONString(string aData) - { - m_Data = aData; - } - public override JSONNode Clone() - { - return new JSONString(m_Data); - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append('\"').Append(Escape(m_Data)).Append('\"'); - } - public override bool Equals(object obj) - { - if (base.Equals(obj)) - return true; - string s = obj as string; - if (s != null) - return m_Data == s; - JSONString s2 = obj as JSONString; - if (s2 != null) - return m_Data == s2.m_Data; - return false; - } - public override int GetHashCode() - { - return m_Data.GetHashCode(); - } - public override void Clear() - { - m_Data = ""; - } - } - // End of JSONString - - public partial class JSONNumber : JSONNode - { - private double m_Data; - - public override JSONNodeType Tag { get { return JSONNodeType.Number; } } - public override bool IsNumber { get { return true; } } - public override Enumerator GetEnumerator() { return new Enumerator(); } - - public override string Value - { - get { return m_Data.ToString(CultureInfo.InvariantCulture); } - set - { - double v; - if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) - m_Data = v; - } - } - - public override double AsDouble - { - get { return m_Data; } - set { m_Data = value; } - } - public override long AsLong - { - get { return (long)m_Data; } - set { m_Data = value; } - } - public override ulong AsULong - { - get { return (ulong)m_Data; } - set { m_Data = value; } - } - - public JSONNumber(double aData) - { - m_Data = aData; - } - - public JSONNumber(string aData) - { - Value = aData; - } - - public override JSONNode Clone() - { - return new JSONNumber(m_Data); - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append(Value.ToString(CultureInfo.InvariantCulture)); - } - private static bool IsNumeric(object value) - { - return value is int || value is uint - || value is float || value is double - || value is decimal - || value is long || value is ulong - || value is short || value is ushort - || value is sbyte || value is byte; - } - public override bool Equals(object obj) - { - if (obj == null) - return false; - if (base.Equals(obj)) - return true; - JSONNumber s2 = obj as JSONNumber; - if (s2 != null) - return m_Data == s2.m_Data; - if (IsNumeric(obj)) - return Convert.ToDouble(obj) == m_Data; - return false; - } - public override int GetHashCode() - { - return m_Data.GetHashCode(); - } - public override void Clear() - { - m_Data = 0; - } - } - // End of JSONNumber - - public partial class JSONBool : JSONNode - { - private bool m_Data; - - public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } } - public override bool IsBoolean { get { return true; } } - public override Enumerator GetEnumerator() { return new Enumerator(); } - - public override string Value - { - get { return m_Data.ToString(); } - set - { - bool v; - if (bool.TryParse(value, out v)) - m_Data = v; - } - } - public override bool AsBool - { - get { return m_Data; } - set { m_Data = value; } - } - - public JSONBool(bool aData) - { - m_Data = aData; - } - - public JSONBool(string aData) - { - Value = aData; - } - - public override JSONNode Clone() - { - return new JSONBool(m_Data); - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append((m_Data) ? "true" : "false"); - } - public override bool Equals(object obj) - { - if (obj == null) - return false; - if (obj is bool) - return m_Data == (bool)obj; - return false; - } - public override int GetHashCode() - { - return m_Data.GetHashCode(); - } - public override void Clear() - { - m_Data = false; - } - } - // End of JSONBool - - public partial class JSONNull : JSONNode - { - static JSONNull m_StaticInstance = new JSONNull(); - public static bool reuseSameInstance = true; - public static JSONNull CreateOrGet() - { - if (reuseSameInstance) - return m_StaticInstance; - return new JSONNull(); - } - private JSONNull() { } - - public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } } - public override bool IsNull { get { return true; } } - public override Enumerator GetEnumerator() { return new Enumerator(); } - - public override string Value - { - get { return "null"; } - set { } - } - public override bool AsBool - { - get { return false; } - set { } - } - - public override JSONNode Clone() - { - return CreateOrGet(); - } - - public override bool Equals(object obj) - { - if (object.ReferenceEquals(this, obj)) - return true; - return (obj is JSONNull); - } - public override int GetHashCode() - { - return 0; - } - - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append("null"); - } - } - // End of JSONNull - - internal partial class JSONLazyCreator : JSONNode - { - private JSONNode m_Node = null; - private string m_Key = null; - public override JSONNodeType Tag { get { return JSONNodeType.None; } } - public override Enumerator GetEnumerator() { return new Enumerator(); } - - public JSONLazyCreator(JSONNode aNode) - { - m_Node = aNode; - m_Key = null; - } - - public JSONLazyCreator(JSONNode aNode, string aKey) - { - m_Node = aNode; - m_Key = aKey; - } - - private T Set(T aVal) where T : JSONNode - { - if (m_Key == null) - m_Node.Add(aVal); - else - m_Node.Add(m_Key, aVal); - m_Node = null; // Be GC friendly. - return aVal; - } - - public override JSONNode this[int aIndex] - { - get { return new JSONLazyCreator(this); } - set { Set(new JSONArray()).Add(value); } - } - - public override JSONNode this[string aKey] - { - get { return new JSONLazyCreator(this, aKey); } - set { Set(new JSONObject()).Add(aKey, value); } - } - - public override void Add(JSONNode aItem) - { - Set(new JSONArray()).Add(aItem); - } - - public override void Add(string aKey, JSONNode aItem) - { - Set(new JSONObject()).Add(aKey, aItem); - } - - public static bool operator ==(JSONLazyCreator a, object b) - { - if (b == null) - return true; - return System.Object.ReferenceEquals(a, b); - } - - public static bool operator !=(JSONLazyCreator a, object b) - { - return !(a == b); - } - - public override bool Equals(object obj) - { - if (obj == null) - return true; - return System.Object.ReferenceEquals(this, obj); - } - - public override int GetHashCode() - { - return 0; - } - - public override int AsInt - { - get { Set(new JSONNumber(0)); return 0; } - set { Set(new JSONNumber(value)); } - } - - public override float AsFloat - { - get { Set(new JSONNumber(0.0f)); return 0.0f; } - set { Set(new JSONNumber(value)); } - } - - public override double AsDouble - { - get { Set(new JSONNumber(0.0)); return 0.0; } - set { Set(new JSONNumber(value)); } - } - - public override long AsLong - { - get - { - if (longAsString) - Set(new JSONString("0")); - else - Set(new JSONNumber(0.0)); - return 0L; - } - set - { - if (longAsString) - Set(new JSONString(value.ToString(CultureInfo.InvariantCulture))); - else - Set(new JSONNumber(value)); - } - } - - public override ulong AsULong - { - get - { - if (longAsString) - Set(new JSONString("0")); - else - Set(new JSONNumber(0.0)); - return 0L; - } - set - { - if (longAsString) - Set(new JSONString(value.ToString(CultureInfo.InvariantCulture))); - else - Set(new JSONNumber(value)); - } - } - - public override bool AsBool - { - get { Set(new JSONBool(false)); return false; } - set { Set(new JSONBool(value)); } - } - - public override JSONArray AsArray - { - get { return Set(new JSONArray()); } - } - - public override JSONObject AsObject - { - get { return Set(new JSONObject()); } - } - internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) - { - aSB.Append("null"); - } - } - // End of JSONLazyCreator - - public static class JSON - { - public static JSONNode Parse(string aJSON) - { - return JSONNode.Parse(aJSON); - } - } -} diff --git a/Editor/SimpleJSON.cs.meta b/Editor/SimpleJSON.cs.meta deleted file mode 100644 index b05076f..0000000 --- a/Editor/SimpleJSON.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 317f4441cfdad49f4ae220c0508e9806 \ No newline at end of file diff --git a/Editor/ZedExternalCodeEditor.cs b/Editor/ZedExternalCodeEditor.cs index e8c88e6..38d5b7b 100644 --- a/Editor/ZedExternalCodeEditor.cs +++ b/Editor/ZedExternalCodeEditor.cs @@ -62,7 +62,7 @@ public bool OpenProject(string filePath = "", int line = -1, int column = -1) } m_Generator.Sync(); - m_Settings.Sync(); + m_Settings.sync(); return m_Process.OpenProject(filePath, line, column); } @@ -72,7 +72,7 @@ public void SyncAll() Assert.IsNotNull(m_Generator); m_Generator.Sync(); - m_Settings.Sync(); + m_Settings.sync(); } public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles) diff --git a/Editor/ZedSettings.cs b/Editor/ZedSettings.cs index 076dcfd..f34bcf6 100644 --- a/Editor/ZedSettings.cs +++ b/Editor/ZedSettings.cs @@ -1,29 +1,22 @@ using UnityEngine; -using SimpleJSON; using System.IO; namespace UnityZed { public class ZedSettings { - private readonly string m_SettingsPath; - - public ZedSettings() - { - m_SettingsPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, ".zed", "settings.json"); - } - - public void Sync() + public void sync() { - if (File.Exists(m_SettingsPath) == false) + string settingsPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, ".zed", "settings.json"); + if (!File.Exists(settingsPath)) { - UnityEngine.Debug.Log("Zed settings file not found, creating default settings file."); - Directory.CreateDirectory(Path.GetDirectoryName(m_SettingsPath)); - File.WriteAllText(m_SettingsPath, JSON.Parse(kDefaultSettings).ToString()); + Debug.Log("[ZED] settings file not found, creating default settings file."); + Directory.CreateDirectory(Path.GetDirectoryName(settingsPath)); + File.WriteAllText(settingsPath, defaultSettings); } } - private const string kDefaultSettings = @"{ + const string defaultSettings = @"{ ""file_scan_exclusions"": [ ""**/.*"", ""**/*~"", @@ -86,4 +79,4 @@ public void Sync() ] }"; } -} \ No newline at end of file +} From 2991c6f7463f34a5f0099409eb0fb5fc664d3be6 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Wed, 4 Mar 2026 22:27:51 +0100 Subject: [PATCH 08/21] refactoring --- Editor/Zed.cs | 223 ++++++++++++++++++++++++++++++++ Editor/Zed.cs.meta | 2 + Editor/ZedExternalCodeEditor.cs | 73 ++--------- Editor/ZedPreferences.cs | 73 ----------- Editor/ZedPreferences.cs.meta | 2 - Editor/ZedProcess.cs | 63 --------- Editor/ZedProcess.cs.meta | 2 - Editor/ZedSettings.cs | 82 ------------ Editor/ZedSettings.cs.meta | 2 - 9 files changed, 234 insertions(+), 288 deletions(-) create mode 100644 Editor/Zed.cs create mode 100644 Editor/Zed.cs.meta delete mode 100644 Editor/ZedPreferences.cs delete mode 100644 Editor/ZedPreferences.cs.meta delete mode 100644 Editor/ZedProcess.cs delete mode 100644 Editor/ZedProcess.cs.meta delete mode 100644 Editor/ZedSettings.cs delete mode 100644 Editor/ZedSettings.cs.meta diff --git a/Editor/Zed.cs b/Editor/Zed.cs new file mode 100644 index 0000000..49cac2e --- /dev/null +++ b/Editor/Zed.cs @@ -0,0 +1,223 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Unity.VisualStudio.Editor; +using UnityEditor; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace UnityZed +{ + public class Zed + { + readonly string editorInstallationPath; + readonly string projectPath; + + readonly IGenerator generator; + + public Zed(string editorInstallationPath) + { + this.editorInstallationPath = editorInstallationPath; + projectPath = Directory.GetParent(Application.dataPath).FullName; + + var assembly = typeof(IGenerator).Assembly; + var type = assembly.GetType("Microsoft.Unity.VisualStudio.Editor.SdkStyleProjectGeneration"); + generator = (IGenerator)Activator.CreateInstance(type); + } + + public void addSettingsFileIfNeeded() + { + const string defaultSettings = @"{ + ""file_scan_exclusions"": [ + ""**/.*"", + ""**/*~"", + + ""*.csproj"", + ""*.sln"", + + ""**/*.meta"", + ""**/*.booproj"", + ""**/*.pibd"", + ""**/*.suo"", + ""**/*.user"", + ""**/*.userprefs"", + ""**/*.unityproj"", + ""**/*.dll"", + ""**/*.exe"", + ""**/*.pdf"", + ""**/*.mid"", + ""**/*.midi"", + ""**/*.wav"", + ""**/*.gif"", + ""**/*.ico"", + ""**/*.jpg"", + ""**/*.jpeg"", + ""**/*.png"", + ""**/*.psd"", + ""**/*.tga"", + ""**/*.tif"", + ""**/*.tiff"", + ""**/*.3ds"", + ""**/*.3DS"", + ""**/*.fbx"", + ""**/*.FBX"", + ""**/*.lxo"", + ""**/*.LXO"", + ""**/*.ma"", + ""**/*.MA"", + ""**/*.obj"", + ""**/*.OBJ"", + ""**/*.asset"", + ""**/*.cubemap"", + ""**/*.flare"", + ""**/*.mat"", + ""**/*.meta"", + ""**/*.prefab"", + ""**/*.unity"", + + ""build/"", + ""Build/"", + ""library/"", + ""Library/"", + ""obj/"", + ""Obj/"", + ""ProjectSettings/"", + ""UserSettings/"", + ""temp/"", + ""Temp/"", + ""logs"", + ""Logs"", + ] + }"; + + var settingsPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, ".zed", "settings.json"); + if (!File.Exists(settingsPath)) + { + Debug.Log("[ZED] settings file not found, creating default settings file."); + Directory.CreateDirectory(Path.GetDirectoryName(settingsPath)); + File.WriteAllText(settingsPath, defaultSettings); + } + } + + public void syncAll() + { + generator.Sync(); + addSettingsFileIfNeeded(); + } + + public void syncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles) + { + generator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles); + } + + public bool openProject(string filePath = "", int line = -1, int column = -1) + { + if (!string.IsNullOrEmpty(filePath) && !generator.IsSupportedFile(filePath)) + { + Debug.Log($"[ZED] File '{filePath}' is not supported by the generator."); + return false; + } + + generator.Sync(); + addSettingsFileIfNeeded(); + + var args = new StringBuilder($"\"{projectPath}\" "); + + if (!string.IsNullOrEmpty(filePath)) + { + args.Append($"\"{filePath}"); + + if (line >= 0) + { + args.Append(":"); + args.Append(line); + + if (column >= 0) + { + args.Append(":"); + args.Append(column); + } + } + } + + Debug.Log(editorInstallationPath + " " + args); + try + { + var startInfo = new ProcessStartInfo + { + FileName = editorInstallationPath, + Arguments = args.ToString(), + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + using (Process.Start(startInfo)) + { + } + + return true; + } + catch (System.Exception ex) + { + Debug.LogError($"[ZED] Failed to start Zed: {ex.Message}"); + return false; + } + } + + public void drawExternalTool() + { + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + var version = "0.9"; + var displayName = "Unity Zed"; + var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly); + if (package != null) + { + version = package.version; + displayName = package.displayName; + } + + var style = new GUIStyle + { + richText = true, + margin = new RectOffset(0, 4, 0, 0) + }; + + GUILayout.Label($"{displayName} v{version} enabled", style); + GUILayout.EndHorizontal(); + + EditorGUILayout.LabelField("Generate .csproj files for:"); + EditorGUI.indentLevel++; + settingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", ""); + settingsButton(ProjectGenerationFlag.Local, "Local packages", ""); + settingsButton(ProjectGenerationFlag.Registry, "Registry packages", ""); + settingsButton(ProjectGenerationFlag.Git, "Git packages", ""); + settingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", ""); + settingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", ""); + settingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", ""); + settingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'"); + var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect()); + rect.width = 252; + if (GUI.Button(rect, "Regenerate project files")) + { + generator.Sync(); + } + + EditorGUI.indentLevel--; + + void settingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip) + { + var prevValue = generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference); + + var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue); + if (newValue != prevValue) + { + generator.AssemblyNameProvider.ToggleProjectGeneration(preference); + } + } + } + } +} \ No newline at end of file diff --git a/Editor/Zed.cs.meta b/Editor/Zed.cs.meta new file mode 100644 index 0000000..4cf76f5 --- /dev/null +++ b/Editor/Zed.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c8a621f1ed7024349b6a844009cb5894 \ No newline at end of file diff --git a/Editor/ZedExternalCodeEditor.cs b/Editor/ZedExternalCodeEditor.cs index 38d5b7b..42a52ba 100644 --- a/Editor/ZedExternalCodeEditor.cs +++ b/Editor/ZedExternalCodeEditor.cs @@ -8,85 +8,30 @@ namespace UnityZed { - public partial class ZedExternalCodeEditor : IExternalCodeEditor + public class ZedExternalCodeEditor : IExternalCodeEditor { [InitializeOnLoadMethod] private static void Initialize() => CodeEditor.Register(new ZedExternalCodeEditor()); - private static IGenerator CreateSdkStyleGeneration() - { - var assembly = typeof(IGenerator).Assembly; - var type = assembly.GetType("Microsoft.Unity.VisualStudio.Editor.SdkStyleProjectGeneration"); - return (IGenerator)Activator.CreateInstance(type); - } - private static readonly ZedDiscovery sDiscovery = new(); - - private ZedProcess m_Process; - private ZedPreferences m_Preferences; - private ZedSettings m_Settings; - private IGenerator m_Generator; + + Zed zed; public void Initialize(string editorInstallationPath) { - m_Process = new(editorInstallationPath); - m_Generator = CreateSdkStyleGeneration(); - m_Preferences = new(m_Generator); - m_Settings = new(); + zed = new Zed(editorInstallationPath); } - // - // Discovery - // - public CodeEditor.Installation[] Installations => sDiscovery.GetInstallations(); public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation) => sDiscovery.TryGetInstallationForPath(editorPath, out installation); - - // - // Interopt - // - - public bool OpenProject(string filePath = "", int line = -1, int column = -1) - { - Assert.IsNotNull(m_Process); - Assert.IsNotNull(m_Generator); - - if (!string.IsNullOrEmpty(filePath) && !m_Generator.IsSupportedFile(filePath)) - { - Debug.Log($"File '{filePath}' is not supported by the generator."); - return false; - } - - m_Generator.Sync(); - m_Settings.sync(); - - return m_Process.OpenProject(filePath, line, column); - } - - public void SyncAll() - { - Assert.IsNotNull(m_Generator); - - m_Generator.Sync(); - m_Settings.sync(); - } - - public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles) - { - Assert.IsNotNull(m_Generator); - - m_Generator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles); - } - - // - // Preference GUI - // - - public void OnGUI() - => m_Preferences.OnGUI(); + + public bool OpenProject(string filePath = "", int line = -1, int column = -1) => zed.openProject(filePath, line, column); + public void SyncAll() => zed.syncAll(); + public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles) => zed.syncIfNeeded(addedFiles, deletedFiles, movedFiles, movedFromFiles, importedFiles); + public void OnGUI() => zed.drawExternalTool(); } } diff --git a/Editor/ZedPreferences.cs b/Editor/ZedPreferences.cs deleted file mode 100644 index 20963a4..0000000 --- a/Editor/ZedPreferences.cs +++ /dev/null @@ -1,73 +0,0 @@ -using UnityEngine; -using UnityEditor; -using Microsoft.Unity.VisualStudio.Editor; -using PackageInfo = UnityEditor.PackageManager.PackageInfo; - -namespace UnityZed -{ - public class ZedPreferences - { - private readonly IGenerator m_Generator; - - public ZedPreferences(IGenerator generator) - { - m_Generator = generator; - } - - public void OnGUI() - { - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - - var version = "unknown"; - var displayName = "Unity Zed"; - var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly); - if (package != null) - { - version = package.version; - displayName = package.displayName; - } - - var style = new GUIStyle - { - richText = true, - margin = new RectOffset(0, 4, 0, 0) - }; - - GUILayout.Label($"{displayName} v{version} enabled", style); - GUILayout.EndHorizontal(); - - EditorGUILayout.LabelField("Generate .csproj files for:"); - EditorGUI.indentLevel++; - SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "", m_Generator); - SettingsButton(ProjectGenerationFlag.Local, "Local packages", "", m_Generator); - SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "", m_Generator); - SettingsButton(ProjectGenerationFlag.Git, "Git packages", "", m_Generator); - SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "", m_Generator); - SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "", m_Generator); - SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "", m_Generator); - SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'", m_Generator); - RegenerateProjectFiles(m_Generator); - EditorGUI.indentLevel--; - } - - private static void RegenerateProjectFiles(IGenerator generator) - { - var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect()); - rect.width = 252; - if (GUI.Button(rect, "Regenerate project files")) - { - generator.Sync(); - } - } - - private static void SettingsButton(ProjectGenerationFlag preference, string guiMessage, string toolTip, IGenerator generator) - { - var prevValue = generator.AssemblyNameProvider.ProjectGenerationFlag.HasFlag(preference); - - var newValue = EditorGUILayout.Toggle(new GUIContent(guiMessage, toolTip), prevValue); - if (newValue != prevValue) - generator.AssemblyNameProvider.ToggleProjectGeneration(preference); - } - } -} diff --git a/Editor/ZedPreferences.cs.meta b/Editor/ZedPreferences.cs.meta deleted file mode 100644 index b668077..0000000 --- a/Editor/ZedPreferences.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 96c771f2489c84be28d6c3e4ea52e2d5 \ No newline at end of file diff --git a/Editor/ZedProcess.cs b/Editor/ZedProcess.cs deleted file mode 100644 index 1882401..0000000 --- a/Editor/ZedProcess.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Diagnostics; -using System.IO; -using System.Text; -using UnityEngine; - -namespace UnityZed -{ - public class ZedProcess - { - private readonly string m_ExecPath; - private readonly string m_ProjectPath; - - public ZedProcess(string execPath) - { - m_ExecPath = execPath; - m_ProjectPath = Directory.GetParent(Application.dataPath).FullName; - } - - public bool OpenProject(string filePath = "", int line = -1, int column = -1) - { - // always add project path - var args = new StringBuilder($"\"{m_ProjectPath}\" "); - - // if file path is provided, add it too - if (!string.IsNullOrEmpty(filePath)) - { - args.Append($"\"{filePath}"); - - if (line >= 0) - { - args.Append(":"); - args.Append(line); - - if (column >= 0) - { - args.Append(":"); - args.Append(column); - } - } - } - - UnityEngine.Debug.Log(m_ExecPath + " " + args); - try - { - var startInfo = new ProcessStartInfo - { - FileName = m_ExecPath, - Arguments = args.ToString(), - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden - }; - using (Process.Start(startInfo)) { } - return true; - } - catch (System.Exception ex) - { - UnityEngine.Debug.LogError($"ZedProcess Failed to start Zed: {ex.Message}"); - return false; - } - } - } -} \ No newline at end of file diff --git a/Editor/ZedProcess.cs.meta b/Editor/ZedProcess.cs.meta deleted file mode 100644 index ab8b08c..0000000 --- a/Editor/ZedProcess.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 07a81d15fbf03419cb72889bbfbde513 \ No newline at end of file diff --git a/Editor/ZedSettings.cs b/Editor/ZedSettings.cs deleted file mode 100644 index f34bcf6..0000000 --- a/Editor/ZedSettings.cs +++ /dev/null @@ -1,82 +0,0 @@ -using UnityEngine; -using System.IO; - -namespace UnityZed -{ - public class ZedSettings - { - public void sync() - { - string settingsPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, ".zed", "settings.json"); - if (!File.Exists(settingsPath)) - { - Debug.Log("[ZED] settings file not found, creating default settings file."); - Directory.CreateDirectory(Path.GetDirectoryName(settingsPath)); - File.WriteAllText(settingsPath, defaultSettings); - } - } - - const string defaultSettings = @"{ - ""file_scan_exclusions"": [ - ""**/.*"", - ""**/*~"", - - ""*.csproj"", - ""*.sln"", - - ""**/*.meta"", - ""**/*.booproj"", - ""**/*.pibd"", - ""**/*.suo"", - ""**/*.user"", - ""**/*.userprefs"", - ""**/*.unityproj"", - ""**/*.dll"", - ""**/*.exe"", - ""**/*.pdf"", - ""**/*.mid"", - ""**/*.midi"", - ""**/*.wav"", - ""**/*.gif"", - ""**/*.ico"", - ""**/*.jpg"", - ""**/*.jpeg"", - ""**/*.png"", - ""**/*.psd"", - ""**/*.tga"", - ""**/*.tif"", - ""**/*.tiff"", - ""**/*.3ds"", - ""**/*.3DS"", - ""**/*.fbx"", - ""**/*.FBX"", - ""**/*.lxo"", - ""**/*.LXO"", - ""**/*.ma"", - ""**/*.MA"", - ""**/*.obj"", - ""**/*.OBJ"", - ""**/*.asset"", - ""**/*.cubemap"", - ""**/*.flare"", - ""**/*.mat"", - ""**/*.meta"", - ""**/*.prefab"", - ""**/*.unity"", - - ""build/"", - ""Build/"", - ""library/"", - ""Library/"", - ""obj/"", - ""Obj/"", - ""ProjectSettings/"", - ""UserSettings/"", - ""temp/"", - ""Temp/"", - ""logs"", - ""Logs"", - ] - }"; - } -} diff --git a/Editor/ZedSettings.cs.meta b/Editor/ZedSettings.cs.meta deleted file mode 100644 index 0e261c9..0000000 --- a/Editor/ZedSettings.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f98a38ae53cba4c3ba4df26bbab3479f \ No newline at end of file From 2e9bd71e544a03ac73d13ba491f5de927357ee1c Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 08:58:41 +0100 Subject: [PATCH 09/21] work --- Editor/Zed.cs | 159 +++++++++++++++++++++++++++++--- Editor/ZedDiscovery.cs | 142 ---------------------------- Editor/ZedDiscovery.cs.meta | 2 - Editor/ZedExternalCodeEditor.cs | 23 ++--- 4 files changed, 153 insertions(+), 173 deletions(-) delete mode 100644 Editor/ZedDiscovery.cs delete mode 100644 Editor/ZedDiscovery.cs.meta diff --git a/Editor/Zed.cs b/Editor/Zed.cs index 49cac2e..a79fdfc 100644 --- a/Editor/Zed.cs +++ b/Editor/Zed.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; +using System.Xml.XPath; using Microsoft.Unity.VisualStudio.Editor; +using Unity.CodeEditor; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; @@ -12,21 +15,24 @@ namespace UnityZed { public class Zed { - readonly string editorInstallationPath; - readonly string projectPath; + string editorInstallationPath; + string projectPath; readonly IGenerator generator; - public Zed(string editorInstallationPath) + public Zed() { - this.editorInstallationPath = editorInstallationPath; - projectPath = Directory.GetParent(Application.dataPath).FullName; - var assembly = typeof(IGenerator).Assembly; var type = assembly.GetType("Microsoft.Unity.VisualStudio.Editor.SdkStyleProjectGeneration"); generator = (IGenerator)Activator.CreateInstance(type); } + public void setEditorInstallationPath(string path) + { + this.editorInstallationPath = path; + projectPath = Directory.GetParent(Application.dataPath).FullName; + } + public void addSettingsFileIfNeeded() { const string defaultSettings = @"{ @@ -106,12 +112,12 @@ public void syncAll() generator.Sync(); addSettingsFileIfNeeded(); } - + public void syncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles) { generator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles); } - + public bool openProject(string filePath = "", int line = -1, int column = -1) { if (!string.IsNullOrEmpty(filePath) && !generator.IsSupportedFile(filePath)) @@ -122,7 +128,7 @@ public bool openProject(string filePath = "", int line = -1, int column = -1) generator.Sync(); addSettingsFileIfNeeded(); - + var args = new StringBuilder($"\"{projectPath}\" "); if (!string.IsNullOrEmpty(filePath)) @@ -182,11 +188,12 @@ public void drawExternalTool() var style = new GUIStyle { - richText = true, margin = new RectOffset(0, 4, 0, 0) }; - GUILayout.Label($"{displayName} v{version} enabled", style); + GUI.color = Color.gray; + GUILayout.Label($"{displayName} v{version} enabled", style); + GUI.color = Color.white; GUILayout.EndHorizontal(); EditorGUILayout.LabelField("Generate .csproj files for:"); @@ -219,5 +226,133 @@ void settingsButton(ProjectGenerationFlag preference, string guiMessage, string } } } + + public CodeEditor.Installation[] getInstallations() + { + var results = new List(); + + var candidates = new List<(string path, TryGetVersion tryGetVersion)> + { + // [MacOS] + ("/Applications/Zed.app/Contents/MacOS/cli", TryGetVersionFromPlist), + ("/usr/local/bin/zed", null), + + // [Linux] (Flatpak) + ("/var/lib/flatpak/app/dev.zed.Zed/current/active/files/bin/zed", null), + + // [Linux] (Repo) + ("/usr/bin/zeditor", null), + + // [Linux] (NixOS) + ("/run/current-system/sw/bin/zeditor", null), + // [Linux] (NixOS HomeManager from Zed Flake) + ("/etc/profiles/per-user/linx/bin/zed", null), + // [Linux] (NixOS HomeManager from NixPkgs) + ("/etc/profiles/per-user/linx/bin/zeditor", null), + + // [Linux] (Official Website) + (Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "zed"), null), + }; + +#if UNITY_EDITOR_WIN + // [Windows] Default install locations + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + var windowsCandidates = new[] + { + Path.Combine(programFiles, "Zed", "bin", "zed.exe"), + Path.Combine(programFilesX86, "Zed", "bin", "zed.exe"), + Path.Combine(localAppData, "Programs", "Zed", "bin", "zed.exe"), // common for user-level installs + Path.Combine(localAppData, "Zed", "bin", "zed.exe") + }; + + foreach (var winPath in windowsCandidates) + { + candidates.Add((winPath, null)); + } + + // [Windows] Check if 'zed.exe' is in PATH + var pathEnv = Environment.GetEnvironmentVariable("PATH"); + if (!string.IsNullOrEmpty(pathEnv)) + { + var pathDirs = pathEnv.Split(Path.PathSeparator); + foreach (var dir in pathDirs) + { + var exePath = Path.Combine(dir.Trim(), "zed.exe"); + if (File.Exists(exePath)) + { + candidates.Add((exePath, null)); + } + } + } +#endif + + foreach (var candidate in candidates) + { + var candidatePath = candidate.path; + var candidateTryGetVersion = candidate.tryGetVersion ?? TryGetVersionFallback; + + if (File.Exists(candidatePath)) + { + var name = new StringBuilder("Zed"); + + if (candidateTryGetVersion(candidatePath, out var version)) + name.Append($" [{version}]"); + + results.Add(new() + { + Name = name.ToString(), + Path = Path.GetFullPath(candidatePath), + }); + + break; + } + } + + return results.ToArray(); + + static bool TryGetVersionFallback(string path, out string version) + { + version = null; + return false; + } + + static bool TryGetVersionFromPlist(string path, out string version) + { + version = null; + + var plistPath = Path.GetFullPath(Path.Combine(path, "..", "..", "Info.plist")); + if (File.Exists(plistPath) == false) + return false; + + var xPath = new XPathDocument(plistPath); + var xNavigator = xPath.CreateNavigator().SelectSingleNode("/plist/dict/key[text()='CFBundleShortVersionString']/following-sibling::string[1]/text()"); + if (xNavigator == null) + return false; + + version = xNavigator.Value; + return true; + } + } + + public bool tryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation) + { + foreach (var installed in getInstallations()) + { + if (installed.Path == editorPath) + { + installation = installed; + return true; + } + } + + installation = default; + return false; + } + + + delegate bool TryGetVersion(string path, out string version); } -} \ No newline at end of file +} diff --git a/Editor/ZedDiscovery.cs b/Editor/ZedDiscovery.cs deleted file mode 100644 index daad5fd..0000000 --- a/Editor/ZedDiscovery.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Unity.CodeEditor; -using System.Collections.Generic; -using System.IO; -using System.Xml.XPath; -using System.Text; - -namespace UnityZed -{ - public class ZedDiscovery - { - public CodeEditor.Installation[] GetInstallations() - { - var results = new List(); - - var candidates = new List<(string path, TryGetVersion tryGetVersion)> { - - // [MacOS] - ("/Applications/Zed.app/Contents/MacOS/cli", TryGetVersionFromPlist), - ("/usr/local/bin/zed", null), - - // [Linux] (Flatpak) - ("/var/lib/flatpak/app/dev.zed.Zed/current/active/files/bin/zed", null), - - // [Linux] (Repo) - ("/usr/bin/zeditor", null), - - // [Linux] (NixOS) - ("/run/current-system/sw/bin/zeditor", null), - // [Linux] (NixOS HomeManager from Zed Flake) - ("/etc/profiles/per-user/linx/bin/zed", null), - // [Linux] (NixOS HomeManager from NixPkgs) - ("/etc/profiles/per-user/linx/bin/zeditor", null), - - // [Linux] (Official Website) - (Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "zed"), null), - }; - -#if UNITY_EDITOR_WIN - // [Windows] Default install locations - var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); - var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - - var windowsCandidates = new[] - { - Path.Combine(programFiles, "Zed", "bin", "zed.exe"), - Path.Combine(programFilesX86, "Zed", "bin", "zed.exe"), - Path.Combine(localAppData, "Programs", "Zed", "bin", "zed.exe"), // common for user-level installs - Path.Combine(localAppData, "Zed", "bin", "zed.exe") - }; - - foreach (var winPath in windowsCandidates) - { - candidates.Add((winPath, null)); - } - - // [Windows] Check if 'zed.exe' is in PATH - var pathEnv = Environment.GetEnvironmentVariable("PATH"); - if (!string.IsNullOrEmpty(pathEnv)) - { - var pathDirs = pathEnv.Split(Path.PathSeparator); - foreach (var dir in pathDirs) - { - var exePath = Path.Combine(dir.Trim(), "zed.exe"); - if (File.Exists(exePath)) - { - candidates.Add((exePath, null)); - } - } - } -#endif - - foreach (var candidate in candidates) - { - var candidatePath = candidate.path; - var candidateTryGetVersion = candidate.tryGetVersion ?? TryGetVersionFallback; - - if (File.Exists(candidatePath)) - { - var name = new StringBuilder("Zed"); - - if (candidateTryGetVersion(candidatePath, out var version)) - name.Append($" [{version}]"); - - results.Add(new() - { - Name = name.ToString(), - Path = Path.GetFullPath(candidatePath), - }); - - break; - } - } - - return results.ToArray(); - } - - public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation) - { - foreach (var installed in GetInstallations()) - { - if (installed.Path == editorPath) - { - installation = installed; - return true; - } - } - - installation = default; - return false; - } - - // - // TryGetVersion implementations - // - private delegate bool TryGetVersion(string path, out string version); - - private static bool TryGetVersionFallback(string path, out string version) - { - version = null; - return false; - } - - private static bool TryGetVersionFromPlist(string path, out string version) - { - version = null; - - var plistPath = Path.GetFullPath(Path.Combine(path, "..", "..", "Info.plist")); - if (File.Exists(plistPath) == false) - return false; - - var xPath = new XPathDocument(plistPath); - var xNavigator = xPath.CreateNavigator().SelectSingleNode("/plist/dict/key[text()='CFBundleShortVersionString']/following-sibling::string[1]/text()"); - if (xNavigator == null) - return false; - - version = xNavigator.Value; - return true; - } - } -} \ No newline at end of file diff --git a/Editor/ZedDiscovery.cs.meta b/Editor/ZedDiscovery.cs.meta deleted file mode 100644 index 1f2f741..0000000 --- a/Editor/ZedDiscovery.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 269d046c929614d7dad1cb25b6419a45 \ No newline at end of file diff --git a/Editor/ZedExternalCodeEditor.cs b/Editor/ZedExternalCodeEditor.cs index 42a52ba..2e45448 100644 --- a/Editor/ZedExternalCodeEditor.cs +++ b/Editor/ZedExternalCodeEditor.cs @@ -10,25 +10,14 @@ namespace UnityZed { public class ZedExternalCodeEditor : IExternalCodeEditor { - [InitializeOnLoadMethod] - private static void Initialize() - => CodeEditor.Register(new ZedExternalCodeEditor()); - - private static readonly ZedDiscovery sDiscovery = new(); + readonly Zed zed = new(); - Zed zed; - - public void Initialize(string editorInstallationPath) - { - zed = new Zed(editorInstallationPath); - } - - public CodeEditor.Installation[] Installations - => sDiscovery.GetInstallations(); + [InitializeOnLoadMethod] + static void Initialize() => CodeEditor.Register(new ZedExternalCodeEditor()); - public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation) - => sDiscovery.TryGetInstallationForPath(editorPath, out installation); - + public void Initialize(string editorInstallationPath) => zed.setEditorInstallationPath(editorInstallationPath); + public CodeEditor.Installation[] Installations => zed.getInstallations(); + public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installation installation) => zed.tryGetInstallationForPath(editorPath, out installation); public bool OpenProject(string filePath = "", int line = -1, int column = -1) => zed.openProject(filePath, line, column); public void SyncAll() => zed.syncAll(); public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] movedFiles, string[] movedFromFiles, string[] importedFiles) => zed.syncIfNeeded(addedFiles, deletedFiles, movedFiles, movedFromFiles, importedFiles); From 18001958867788c7562547629dee6a242851c723 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 09:08:51 +0100 Subject: [PATCH 10/21] work --- Editor/Zed.cs | 7 +++---- Editor/ZedExternalCodeEditor.cs | 4 ++-- ....asmdef => com.superjura.unity-zed-integration.asmdef} | 4 ++-- ...ta => com.superjura.unity-zed-integration.asmdef.meta} | 0 LICENSE.txt | 2 +- package.json | 8 ++++---- 6 files changed, 12 insertions(+), 13 deletions(-) rename Editor/{com.maligan.unity-zed.asmdef => com.superjura.unity-zed-integration.asmdef} (79%) rename Editor/{com.maligan.unity-zed.asmdef.meta => com.superjura.unity-zed-integration.asmdef.meta} (100%) diff --git a/Editor/Zed.cs b/Editor/Zed.cs index a79fdfc..3cf41e5 100644 --- a/Editor/Zed.cs +++ b/Editor/Zed.cs @@ -11,7 +11,7 @@ using UnityEngine; using Debug = UnityEngine.Debug; -namespace UnityZed +namespace UnityZedIntegration { public class Zed { @@ -188,12 +188,11 @@ public void drawExternalTool() var style = new GUIStyle { + richText = true, margin = new RectOffset(0, 4, 0, 0) }; - GUI.color = Color.gray; - GUILayout.Label($"{displayName} v{version} enabled", style); - GUI.color = Color.white; + GUILayout.Label($"{displayName} v{version} enabled", style); GUILayout.EndHorizontal(); EditorGUILayout.LabelField("Generate .csproj files for:"); diff --git a/Editor/ZedExternalCodeEditor.cs b/Editor/ZedExternalCodeEditor.cs index 2e45448..28d0acd 100644 --- a/Editor/ZedExternalCodeEditor.cs +++ b/Editor/ZedExternalCodeEditor.cs @@ -6,12 +6,12 @@ using UnityEditor; using Microsoft.Unity.VisualStudio.Editor; -namespace UnityZed +namespace UnityZedIntegration { public class ZedExternalCodeEditor : IExternalCodeEditor { readonly Zed zed = new(); - + [InitializeOnLoadMethod] static void Initialize() => CodeEditor.Register(new ZedExternalCodeEditor()); diff --git a/Editor/com.maligan.unity-zed.asmdef b/Editor/com.superjura.unity-zed-integration.asmdef similarity index 79% rename from Editor/com.maligan.unity-zed.asmdef rename to Editor/com.superjura.unity-zed-integration.asmdef index afcbfc4..01e141a 100644 --- a/Editor/com.maligan.unity-zed.asmdef +++ b/Editor/com.superjura.unity-zed-integration.asmdef @@ -1,6 +1,6 @@ { - "name": "com.maligan.zed-unity", - "rootNamespace": "UnityZed", + "name": "com.superjura.unity-zed-integration", + "rootNamespace": "UnityZedIntegration", "references": [ "GUID:b93f844d45cfcc44fa2b0eed5c9ec6bb" ], diff --git a/Editor/com.maligan.unity-zed.asmdef.meta b/Editor/com.superjura.unity-zed-integration.asmdef.meta similarity index 100% rename from Editor/com.maligan.unity-zed.asmdef.meta rename to Editor/com.superjura.unity-zed-integration.asmdef.meta diff --git a/LICENSE.txt b/LICENSE.txt index f63ceab..44fa2dd 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Alexandr Frolov +Copyright (c) 2024 Jurica Adamek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package.json b/package.json index 4bc32ac..a2cc3ea 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "com.maligan.unity-zed", - "displayName": "Zed Editor", - "description": "Zed Editor integration for Unity", - "version": "0.2.3-preview", + "name": "com.superjura.unity-zed-integration", + "displayName": "Unity Zed Integration", + "description": "Unity Zed Integration", + "version": "0.9", "dependencies": { "com.unity.ide.visualstudio": "2.0.20" } From c767041fc16d35c908a4497ef5698e9dd022c13b Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 09:16:06 +0100 Subject: [PATCH 11/21] readme --- README.md | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3a444ca..049ff47 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,28 @@ -# Zed for Unity [![openupm](https://img.shields.io/npm/v/com.maligan.unity-zed?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.maligan.unity-zed/) +# Unity Zed Integration (UZI) -This is a homemade package to integrate [Zed](https://zed.dev) as Unity external script editor. +A lightweight package to integrate the [Zed editor](https://zed.dev) as your external script editor in Unity. -## Roadmap +> *Initially forked from [Unity-zed](https://github.com/maligan/unity-zed).* -- [x] Discovery of Zed installations -- [x] Register as Unity external tools -- [x] Support C# sln/csproj generation -- [ ] Write Zed extension to deeper integration via IPC +--- + +## Supported Platforms + +* **Windows** *(Actively daily-driven)* +* **macOS** +* **Linux** + +--- ## Installation -```sh -# 1. Via OpenUPM -openupm add com.maligan.unity-zed +You can install this package directly through the Unity Package Manager using the GitHub repository URL: -# 2. Via PackageManger & GitHub URL -https://github.com/maligan/unity-zed.git +1. Open Unity and navigate to **Window > Package Management > Package Manager**. +2. Click the **+** (plus) button in the top left corner of the window. +3. Select **Add package from git URL...** +4. Paste the following link and click **Add**: -# 3. Via copy this repository content into Packages/ folder +```text +https://github.com/SuperJura/unity-zed-integration ``` From ac652e02b5f099f52f795f089a58f7084af09e81 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 09:26:00 +0100 Subject: [PATCH 12/21] fixes --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 049ff47..27c1055 100644 --- a/README.md +++ b/README.md @@ -24,5 +24,5 @@ You can install this package directly through the Unity Package Manager using th 4. Paste the following link and click **Add**: ```text -https://github.com/SuperJura/unity-zed-integration +https://github.com/SuperJura/unity-zed-integration.git ``` diff --git a/package.json b/package.json index a2cc3ea..1a6a043 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "com.superjura.unity-zed-integration", "displayName": "Unity Zed Integration", "description": "Unity Zed Integration", - "version": "0.9", + "version": "0.9.0", "dependencies": { "com.unity.ide.visualstudio": "2.0.20" } From 6695a752f33b8a3e7cc3175b473caf46f9316a5b Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 09:31:41 +0100 Subject: [PATCH 13/21] w --- package.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/package.json b/package.json index 1a6a043..8ca7005 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,18 @@ "displayName": "Unity Zed Integration", "description": "Unity Zed Integration", "version": "0.9.0", + "author": { + "name": "Jurica Adamek", + "email": "jurica.adamek@gmail.com", + "url": "https://github.com/SuperJura" + }, + "license": "MIT", + "keywords": [ + "zed", + "unity", + "editor", + "ide" + ], "dependencies": { "com.unity.ide.visualstudio": "2.0.20" } From e148e03924824462dcf35537d8bc635204a18ce9 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 09:32:47 +0100 Subject: [PATCH 14/21] name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8ca7005..cafdb61 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Unity Zed Integration", "version": "0.9.0", "author": { - "name": "Jurica Adamek", + "name": "SuperJura", "email": "jurica.adamek@gmail.com", "url": "https://github.com/SuperJura" }, From 31f8601d7976fb191782696835c26037f8eea690 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 09:34:08 +0100 Subject: [PATCH 15/21] Update LICENSE.txt --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 44fa2dd..df51b78 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Jurica Adamek +Copyright (c) 2026 Jurica Adamek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 89538910c7ea29e362c96db56481e19c913ce354 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 09:36:20 +0100 Subject: [PATCH 16/21] Delete Editor/.idea directory --- Editor/.idea/.gitignore | 17 ----------------- .../.idea/copilot.data.migration.ask2agent.xml | 6 ------ Editor/.idea/encodings.xml | 4 ---- Editor/.idea/indexLayout.xml | 8 -------- .../inspectionProfiles/Project_Default.xml | 6 ------ Editor/.idea/vcs.xml | 6 ------ 6 files changed, 47 deletions(-) delete mode 100644 Editor/.idea/.gitignore delete mode 100644 Editor/.idea/copilot.data.migration.ask2agent.xml delete mode 100644 Editor/.idea/encodings.xml delete mode 100644 Editor/.idea/indexLayout.xml delete mode 100644 Editor/.idea/inspectionProfiles/Project_Default.xml delete mode 100644 Editor/.idea/vcs.xml diff --git a/Editor/.idea/.gitignore b/Editor/.idea/.gitignore deleted file mode 100644 index dc983da..0000000 --- a/Editor/.idea/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml - -# Rider ignored files -/modules.xml -/projectSettingsUpdater.xml -/contentModel.xml -/.idea.Editor.iml - -# Ignored default folder with query files -/queries/ - -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml - diff --git a/Editor/.idea/copilot.data.migration.ask2agent.xml b/Editor/.idea/copilot.data.migration.ask2agent.xml deleted file mode 100644 index 1f2ea11..0000000 --- a/Editor/.idea/copilot.data.migration.ask2agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Editor/.idea/encodings.xml b/Editor/.idea/encodings.xml deleted file mode 100644 index df87cf9..0000000 --- a/Editor/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Editor/.idea/indexLayout.xml b/Editor/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/Editor/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Editor/.idea/inspectionProfiles/Project_Default.xml b/Editor/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 8b47716..0000000 --- a/Editor/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/Editor/.idea/vcs.xml b/Editor/.idea/vcs.xml deleted file mode 100644 index 6c0b863..0000000 --- a/Editor/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From bd00be6e9fb04e8827b6df0e3dc07314128df034 Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 09:52:23 +0100 Subject: [PATCH 17/21] license txt->md --- LICENSE.txt => LICENSE.md | 0 LICENSE.txt.meta => LICENSE.md.meta | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename LICENSE.txt => LICENSE.md (100%) rename LICENSE.txt.meta => LICENSE.md.meta (100%) diff --git a/LICENSE.txt b/LICENSE.md similarity index 100% rename from LICENSE.txt rename to LICENSE.md diff --git a/LICENSE.txt.meta b/LICENSE.md.meta similarity index 100% rename from LICENSE.txt.meta rename to LICENSE.md.meta From fbbe131f1d308dc78f2c5736f5548dbb4409432c Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Thu, 5 Mar 2026 22:46:09 +0100 Subject: [PATCH 18/21] Add Windows support note to README Updated README to mention Windows support. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 27c1055..71a6126 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A lightweight package to integrate the [Zed editor](https://zed.dev) as your external script editor in Unity. -> *Initially forked from [Unity-zed](https://github.com/maligan/unity-zed).* +> *Initially forked from [Unity-zed](https://github.com/maligan/unity-zed). Forked to add Windows support.* --- From aee81c5ea6966997422c479aafcf0b2d88320dcc Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Fri, 6 Mar 2026 09:30:40 +0100 Subject: [PATCH 19/21] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71a6126..cdbb710 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A lightweight package to integrate the [Zed editor](https://zed.dev) as your ext ## Supported Platforms -* **Windows** *(Actively daily-driven)* +* **Windows** * **macOS** * **Linux** From b75f9933aee73a45ce54d400735e4254982ed949 Mon Sep 17 00:00:00 2001 From: NiurouLee <112259092+NiurouLee@users.noreply.github.com> Date: Fri, 13 Mar 2026 21:04:32 +0800 Subject: [PATCH 20/21] Simplify file path argument construction Refactor file path handling to simplify argument construction. --- Editor/Zed.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Editor/Zed.cs b/Editor/Zed.cs index 3cf41e5..61c16f2 100644 --- a/Editor/Zed.cs +++ b/Editor/Zed.cs @@ -131,21 +131,16 @@ public bool openProject(string filePath = "", int line = -1, int column = -1) var args = new StringBuilder($"\"{projectPath}\" "); - if (!string.IsNullOrEmpty(filePath)) + if (!string.IsNullOrEmpty(filePath)) { - args.Append($"\"{filePath}"); - + string fileArg = filePath; if (line >= 0) { - args.Append(":"); - args.Append(line); - - if (column >= 0) - { - args.Append(":"); - args.Append(column); - } + fileArg += $":{line}"; + if (column >= 0) fileArg += $":{column}"; } + // 确保整个文件路径+行列号被一对双引号包裹 + args.Append($"\"{fileArg}\""); } Debug.Log(editorInstallationPath + " " + args); From f0db8fd5b16aa3609d6904825960b3ec233a5dde Mon Sep 17 00:00:00 2001 From: Jurica Adamek Date: Fri, 20 Mar 2026 09:26:09 +0100 Subject: [PATCH 21/21] small cleanup --- Editor/Zed.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Editor/Zed.cs b/Editor/Zed.cs index 61c16f2..a2a8595 100644 --- a/Editor/Zed.cs +++ b/Editor/Zed.cs @@ -131,7 +131,7 @@ public bool openProject(string filePath = "", int line = -1, int column = -1) var args = new StringBuilder($"\"{projectPath}\" "); - if (!string.IsNullOrEmpty(filePath)) + if (!string.IsNullOrEmpty(filePath)) { string fileArg = filePath; if (line >= 0) @@ -139,11 +139,10 @@ public bool openProject(string filePath = "", int line = -1, int column = -1) fileArg += $":{line}"; if (column >= 0) fileArg += $":{column}"; } - // 确保整个文件路径+行列号被一对双引号包裹 args.Append($"\"{fileArg}\""); } - Debug.Log(editorInstallationPath + " " + args); + //Debug.Log(editorInstallationPath + " " + args); try { var startInfo = new ProcessStartInfo