Files
2025-11-30 08:35:03 +02:00

341 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
#if UNITY_EDITOR
using Unity.CodeEditor;
using UnityEditor;
#endif
namespace Needle.Engine.Utils
{
public static class WorkspaceUtils
{
public static async void OpenWorkspace(string directory, bool allowChangingTitle = true, bool useDefaultEditor = false, string filename = null, bool allowRemapToWebProject = true)
{
directory = Path.GetFullPath(directory);
directory = PathUtils.ResolveSymlink(directory);
if (allowRemapToWebProject)
{
// first check if the npmdef is a dependency of the current web project
// if yes we try to open the workspace of the web project instead
var exp = ExportInfo.Get();
if (exp && exp.Exists())
{
var packageName = PackageUtils.GetPackageName(directory + "/package.json");
if (PackageUtils.IsDependency(exp.PackageJsonPath, packageName))
{
var newDirectory = exp.GetProjectDirectory();
if (filename != null)
{
if (filename.StartsWith("Packages/") || filename.StartsWith("Assets/"))
filename = Path.GetFullPath(filename);
// try to make the filename path a "node_modules path"
if (Path.IsPathRooted(filename) && filename.StartsWith(directory))
{
filename = filename?.Substring(directory.Length + 1);
}
var newFileName = newDirectory + "/node_modules/" + packageName + "/" + filename;
if (File.Exists(newFileName)) filename = newFileName;
}
// the file is a dependency of the current web project
// make sure the workspace local folders are up to date / synced with the package.json local dependencies
if(TryGetWorkspace(newDirectory, out var workspacePath))
AddLocalPackages(exp.PackageJsonPath, workspacePath);
OpenWorkspace(newDirectory, allowChangingTitle, useDefaultEditor, filename);
return;
}
}
}
if (useDefaultEditor && await OpenWithDefaultEditor(directory, filename))
{
}
else if (TryGetWorkspace(directory, out var workspacePath))
{
var packageJson = directory + "/package.json";
if (File.Exists(directory))
AddLocalPackages(packageJson, workspacePath);
#if UNITY_EDITOR
EditorUtility.OpenWithDefaultApp(workspacePath);
if (!string.IsNullOrEmpty(filename))
{
// sometimes this is a virtual path (so we cannot use "IsRooted"), sometimes it may be absolute already... sometimes it's a relative path to the directory
if(!File.Exists(filename))
filename = Path.Combine(directory, filename);
filename = Path.GetFullPath(filename);
if (File.Exists(filename))
EditorUtility.OpenWithDefaultApp(filename);
}
#endif
if (allowChangingTitle)
{
TryWriteWorkspaceTitle(workspacePath);
}
}
else if(await OpenWithCode(directory))
{
// Ignore
}
else
{
await OpenWithDefaultEditor(directory, filename);
}
}
private static async Task<bool> OpenWithCode(string directory, string file = null)
{
var cmd = "code \"" + directory + "\"";
if(!string.IsNullOrEmpty(file)) cmd += " --file \"" + file + "\"";
return await ProcessHelper.RunCommand(cmd, null);
}
public static async Task<bool> OpenWithDefaultEditor(string directory, string withfile = null)
{
#if UNITY_EDITOR
var inst = CodeEditor.CurrentEditorInstallation;
if (inst.IndexOf("rider", StringComparison.OrdinalIgnoreCase) >= 0)
{
// TODO: open withfile if it exists
await ProcessHelper.RunCommand($"\"{inst}\" \"{directory}\"", null);
return true;
}
if (inst.IndexOf("visual studio", StringComparison.OrdinalIgnoreCase) >= 0)
{
// TODO: open withfile if it exists
await ProcessHelper.RunCommand($"\"{inst}\" /Edit \"{directory}\"", null);
return true;
}
if (inst.IndexOf("code", StringComparison.OrdinalIgnoreCase) >= 0)
{
return await OpenWithCode(directory, withfile);
}
#endif
return await Task.FromResult(false);
}
public static bool TryWriteWorkspaceTitle(string workspacePath, string title = null, bool allowOverride = true)
{
if (File.Exists(workspacePath))
{
try
{
var workspaceFileInfo = new FileInfo(workspacePath);
// Default title name is parent/directory name
if (title == null) title = workspaceFileInfo.Directory?.Name;
var workspace = File.ReadAllText(workspacePath);
var obj = JsonConvert.DeserializeObject<JObject>(workspace);
if (obj["settings"] == null) obj["settings"] = new JObject();
var settings = (JObject)obj["settings"];
var titleObj = settings["window.title"] as JValue;
if (titleObj == null || titleObj.Value<string>() != title)
{
if (allowOverride == false && titleObj != null)
{
return false;
}
settings["window.title"] = title;
File.WriteAllText(workspacePath, obj.ToString());
}
return true;
}
catch
{
// Ignore
}
}
return false;
}
public static bool TryGetWorkspace(string directory, out string workspacePath)
{
if (string.IsNullOrWhiteSpace(directory))
{
workspacePath = null;
return false;
}
directory = directory.TrimEnd('\\').TrimEnd('/');
directory = Path.GetFullPath(directory);
workspacePath = Directory.GetFiles(directory, "*.code-workspace", SearchOption.TopDirectoryOnly).FirstOrDefault();
var success = !string.IsNullOrEmpty(workspacePath);
return success;
}
public static void AddLocalPackages(string packageJson, string workspacePath)
{
if (!PackageUtils.TryReadDependencies(packageJson, out var deps)) return;
if (TryReadWorkspace(workspacePath, out var workspaceContent))
{
var changed = false;
foreach (var dep in deps)
{
if (dep.Value.EndsWith("~") || dep.Value.StartsWith("file:"))
{
if (AddToFolders(workspaceContent, dep.Key))
changed = true;
}
}
changed |= PrettifyWorkspace(workspaceContent);
if (changed)
{
WriteWorkspace(workspaceContent, workspacePath);
}
}
}
private static bool TryEnsureWorkspace(ref string workspacePathOrDirectory)
{
if (!workspacePathOrDirectory.EndsWith(".code-workspace") && (File.Exists(workspacePathOrDirectory) || Directory.Exists(workspacePathOrDirectory)))
{
var attr = File.GetAttributes(workspacePathOrDirectory);
var dir = workspacePathOrDirectory;
if (attr.HasFlag(FileAttributes.Directory) == false)
{
dir = Path.GetDirectoryName(workspacePathOrDirectory);
}
if (dir != null)
{
workspacePathOrDirectory = Directory.GetFiles(dir, "*.code-workspace", SearchOption.TopDirectoryOnly).FirstOrDefault();
}
}
return workspacePathOrDirectory != null && File.Exists(workspacePathOrDirectory);
}
public static bool TryReadWorkspace(string workspacePath, out JObject obj)
{
try
{
obj = null;
if (!TryEnsureWorkspace(ref workspacePath)) return false;
var text = File.ReadAllText(workspacePath);
obj = JsonConvert.DeserializeObject<JObject>(text);
return obj != null;
}
catch (Exception ex)
{
Debug.LogException(ex);
obj = null;
return false;
}
}
public static void WriteWorkspace(JObject workspace, string workspacePath)
{
if (!TryEnsureWorkspace(ref workspacePath)) return;
var result = JsonConvert.SerializeObject(workspace, Formatting.Indented);
File.WriteAllText(workspacePath, result);
}
/// <returns>True if was added, false if already in array</returns>
public static bool AddToFolders(JObject workspace, string name, string path = null)
{
var foldersArray = (JArray)workspace["folders"] ?? new JArray();
for (var index = foldersArray.Count - 1; index >= 0; index--)
{
var jToken = foldersArray[index];
var entry = (JObject)jToken;
var currentName = entry["name"]?.Value<string>();
// get the display name map
foreach (var kvp in displayNameMap)
{
if(kvp.Value == currentName)
{
currentName = kvp.Key;
break;
}
}
if (currentName == name)
{
return false;
}
}
var dependency = new JObject();
var newPath = path ?? "node_modules/" + name;
dependency["name"] = name;
dependency["path"] = newPath;
dependency["@needle:managed"] = true;
foldersArray.Add(dependency);
workspace["folders"] = foldersArray;
return true;
}
public static bool RemoveFromFolders(JObject workspace, string name)
{
var folders = (JArray)workspace["folders"];
if (folders == null) return false;
var changed = false;
for (var index = folders.Count - 1; index >= 0; index--)
{
var jToken = folders[index];
var entry = (JObject)jToken;
var currentName = entry["name"]?.Value<string>();
// get the display name map
foreach (var kvp in displayNameMap)
{
if(kvp.Value == currentName)
{
currentName = kvp.Key;
break;
}
}
if (currentName == name)
{
folders.RemoveAt(index);
changed = true;
}
}
return changed;
}
private static readonly Dictionary<string, string> displayNameMap = new Dictionary<string, string>()
{
{"@needle-tools/engine", "🌵 Needle Engine"},
};
public static bool PrettifyWorkspace(JObject workspace)
{
var folders = (JArray)workspace["folders"];
var changed = false;
if (folders != null)
{
for (var index = 0; index < folders.Count; index++)
{
var jToken = folders[index];
var entry = (JObject)jToken;
var path = entry["path"]?.Value<string>();
// ensure that the engine is always displayed as "🌵 Needle Engine"
if (path != null && path.Contains("node_modules/@needle-tools/engine"))
{
if (entry["name"] == null)
{
entry["name"] = "🌵 Needle Engine";
changed = true;
}
}
var name = entry["name"]?.Value<string>();
if(name != null && displayNameMap.TryGetValue(name, out var displayName))
{
entry["name"] = displayName;
changed = true;
}
}
}
return changed;
}
}
}