Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

All notable changes to this package are documented in this file.

## [0.4.2-preview] - 2026-07-30

### Added

- Cross-platform discovery for stable, preview, portable, PATH, Nix, and custom Zed installations.
- SDK-style Unity project generation with support for analyzers registered by Unity.
- Project-local Roslyn, csharp-ls, OmniSharp, Unity file-type, and scan-exclusion settings.
- Documentation for the Zed Unity Snippets, shader-language, and Unity Debugger extensions.
- A disposable Unity CLI smoke test for real package import, compilation, and domain reload validation.

### Changed

- Project synchronization now refreshes package information and tolerates missing or null asset changes.
- Settings synchronization preserves existing values, malformed files, JSONC comments, and read-only projects.
- Warnings from persistent configuration problems are emitted once instead of on every asset import.

### Fixed

- Player projects now receive analyzers from the matching player compilation assembly.
- Missing or unsupported files are no longer passed to Zed.
- A failed compatibility cache reset no longer interrupts project synchronization.
7 changes: 7 additions & 0 deletions CHANGELOG.md.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 91 additions & 10 deletions Editor/ZedDiscovery.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using Unity.CodeEditor;
using System;
using System.Collections.Generic;
using System.Xml.XPath;
using System.Text;
using System.Xml.XPath;
using NiceIO;
using Unity.CodeEditor;

namespace UnityZed
{
Expand All @@ -12,10 +13,11 @@ public CodeEditor.Installation[] GetInstallations()
{
var results = new List<CodeEditor.Installation>();

var candidates = new (NPath path, TryGetVersion tryGetVersion)[] {
var candidates = new List<(NPath path, TryGetVersion tryGetVersion)> {

// [MacOS]
("/Applications/Zed.app/Contents/MacOS/cli", TryGetVersionFromPlist),
("/Applications/Zed Preview.app/Contents/MacOS/cli", TryGetVersionFromPlist),
("/usr/local/bin/zed", null),

// [Linux] (Flatpak)
Expand All @@ -27,33 +29,46 @@ public CodeEditor.Installation[] GetInstallations()
// [Linux] (NixOS)
("/run/current-system/sw/bin/zeditor", null),
// [Linux] (NixOS HomeManager from Zed Flake)
("/etc/profiles/per-user/linx/bin/zed", null),
($"/etc/profiles/per-user/{Environment.UserName}/bin/zed", null),
// [Linux] (NixOS HomeManager from NixPkgs)
("/etc/profiles/per-user/linx/bin/zeditor", null),
($"/etc/profiles/per-user/{Environment.UserName}/bin/zeditor", null),

// [Linux] (Official Website)
(NPath.HomeDirectory.Combine(".local/bin/zed"), null),
(NPath.HomeDirectory.Combine(".local/zed.app/bin/zed"), null),
};

AddWindowsCandidates(candidates);
AddPathCandidates(candidates);

foreach (var candidate in candidates)
{
var candidatePath = candidate.path;
var candidateTryGetVersion = candidate.tryGetVersion ?? TryGetVersionFallback;

if (candidatePath.FileExists())
try
{
if (candidatePath.FileExists() == false)
continue;

var name = new StringBuilder("Zed");

if (candidateTryGetVersion(candidatePath, out var version))
name.Append($" [{version}]");

results.Add(new()
var installation = new CodeEditor.Installation
{
Name = name.ToString(),
Path = candidatePath.MakeAbsolute().ToString(),
});
};

break;
if (results.Exists(result => string.Equals(result.Path, installation.Path, StringComparison.OrdinalIgnoreCase)) == false)
results.Add(installation);
}
catch (Exception)
{
// One inaccessible or malformed candidate must not prevent discovery
// in all other standard locations and PATH entries.
}
}

Expand All @@ -71,14 +86,80 @@ public bool TryGetInstallationForPath(string editorPath, out CodeEditor.Installa
}
}

// Unity allows selecting an executable manually. Do not reject a valid custom,
// preview, portable, or future Zed install merely because it is not in our list.
if (string.IsNullOrWhiteSpace(editorPath))
{
installation = default;
return false;
}

try
{
var customPath = new NPath(editorPath);
if (customPath.FileExists() && customPath.FileNameWithoutExtension.StartsWith("zed", StringComparison.OrdinalIgnoreCase))
{
installation = new()
{
Name = "Zed [Custom]",
Path = customPath.MakeAbsolute().ToString(),
};
return true;
}
}
catch (ArgumentException)
{
// Invalid paths can be stored by older Unity preferences. Treat them as
// unsupported rather than breaking the External Tools preferences UI.
}

installation = default;
return false;
}

//
// TryGetVersion implementations
//
private delegate bool TryGetVersion(NPath path, out string vertion);
private static void AddWindowsCandidates(List<(NPath path, TryGetVersion tryGetVersion)> candidates)
{
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs/Zed/Zed.exe");
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Zed/Zed.exe");
AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Zed/Zed.exe");
}

private static void AddPathCandidates(List<(NPath path, TryGetVersion tryGetVersion)> candidates)
{
var path = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(path))
return;

foreach (var directory in path.Split(System.IO.Path.PathSeparator))
{
if (string.IsNullOrWhiteSpace(directory))
continue;

try
{
var candidateDirectory = new NPath(directory.Trim().Trim('"'));
candidates.Add((candidateDirectory.Combine("zed"), null));
candidates.Add((candidateDirectory.Combine("zeditor"), null));
candidates.Add((candidateDirectory.Combine("zed.exe"), null));
}
catch (ArgumentException)
{
// Ignore malformed PATH entries; explicitly configured candidates and
// all remaining entries should still be considered.
}
}
}

private static void AddCandidate(List<(NPath path, TryGetVersion tryGetVersion)> candidates, string directory, string relativePath)
{
if (string.IsNullOrEmpty(directory) == false)
candidates.Add((new NPath(directory).Combine(relativePath), null));
}

private delegate bool TryGetVersion(NPath path, out string version);

private static bool TryGetVersionFallback(NPath path, out string version)
{
Expand Down
58 changes: 55 additions & 3 deletions Editor/ZedExternalCodeEditor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using Unity.CodeEditor;
using UnityEngine;
Expand Down Expand Up @@ -28,13 +29,18 @@ private static IGenerator CreateSdkStyleGeneration()
private ZedPreferences m_Preferences;
private ZedSettings m_Settings;
private IGenerator m_Generator;
private bool m_CacheResetWarningLogged;

public void Initialize(string editorInstallationPath)
{
m_Process = new(editorInstallationPath);
m_Generator = CreateSdkStyleGeneration();
m_Preferences = new(m_Generator);
m_Settings = new();
m_Settings.Sync();

if (m_Generator.HasSolutionBeenGenerated() == false)
m_Generator.Sync();
}

//
Expand All @@ -56,9 +62,15 @@ 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))
if (!string.IsNullOrEmpty(filePath) && m_Generator.IsSupportedFile(filePath) == false)
{
sLogger.Log($"File '{filePath}' is not supported by the generator.");
sLogger.LogWarning($"File '{filePath}' is not supported by the project generator.");
return false;
}

if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath) == false)
{
sLogger.LogWarning($"File '{filePath}' does not exist.");
return false;
}

Expand All @@ -72,6 +84,8 @@ public void SyncAll()
{
Assert.IsNotNull(m_Generator);

ResetProjectGenerationCache();
AssetDatabase.Refresh();
m_Generator.Sync();
m_Settings.Sync();
}
Expand All @@ -80,7 +94,45 @@ public void SyncIfNeeded(string[] addedFiles, string[] deletedFiles, string[] mo
{
Assert.IsNotNull(m_Generator);

m_Generator.SyncIfNeeded(addedFiles.Union(deletedFiles).Union(movedFiles).Union(movedFromFiles), importedFiles);
ResetProjectGenerationCache();
m_Generator.SyncIfNeeded(
(addedFiles ?? Array.Empty<string>())
.Union(deletedFiles ?? Array.Empty<string>())
.Union(movedFiles ?? Array.Empty<string>())
.Union(movedFromFiles ?? Array.Empty<string>()),
importedFiles ?? Array.Empty<string>());
m_Settings.Sync();
}

private void ResetProjectGenerationCache()
{
try
{
var provider = m_Generator.AssemblyNameProvider;
for (var type = provider.GetType(); type != null; type = type.BaseType)
{
var method = type.GetMethod(
"ResetPackageInfoCache",
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.DeclaredOnly);
if (method == null)
continue;

method.Invoke(provider, null);
return;
}
}
catch (Exception exception)
{
// This is an optimization for package moves and updates. Project sync must
// continue if a future Visual Studio Editor package changes its internals.
if (m_CacheResetWarningLogged == false)
{
m_CacheResetWarningLogged = true;
sLogger.LogWarning($"Could not reset the project-generation cache: {exception.Message}");
}
}
}

//
Expand Down
13 changes: 9 additions & 4 deletions Editor/ZedLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public static ILogger Create([CallerFilePath] string filePath = null)
var tag = path.FileNameWithoutExtension;

#if UNITY_ZED_DEBUG
var handler = new LogHandler(tag, Debug.unityLogger.logHandler);
var handler = new LogHandler(tag, Debug.unityLogger.logHandler, true);
#else
var handler = new LogHandler(tag, null);
var handler = new LogHandler(tag, Debug.unityLogger.logHandler, false);
#endif

return new Logger(handler);
Expand All @@ -25,18 +25,23 @@ private class LogHandler : ILogHandler
{
private readonly ILogHandler m_LogHandler;
private readonly string m_Tag;
private readonly bool m_LogMessages;

public LogHandler(string tag, ILogHandler logHandler = null)
public LogHandler(string tag, ILogHandler logHandler, bool logMessages)
{
m_Tag = tag;
m_LogHandler = logHandler;
m_LogMessages = logMessages;
}

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);
{
if (m_LogMessages || logType != LogType.Log)
m_LogHandler.LogFormat(logType, context, $"[{m_Tag}] {format}", args);
}
}
}
}
Loading