A game about forced loneliness, made by TACStudios
at master 15 kB view raw
1/*--------------------------------------------------------------------------------------------- 2 * Copyright (c) Microsoft Corporation. All rights reserved. 3 * Licensed under the MIT License. See License.txt in the project root for license information. 4 *--------------------------------------------------------------------------------------------*/ 5 6using System; 7using System.Collections.Generic; 8using System.Diagnostics; 9using System.IO; 10using System.Linq; 11using System.Text.RegularExpressions; 12using UnityEngine; 13using SimpleJSON; 14using IOPath = System.IO.Path; 15 16namespace Microsoft.Unity.VisualStudio.Editor 17{ 18 internal class VisualStudioCodeInstallation : VisualStudioInstallation 19 { 20 private static readonly IGenerator _generator = new SdkStyleProjectGeneration(); 21 22 public override bool SupportsAnalyzers 23 { 24 get 25 { 26 return true; 27 } 28 } 29 30 public override Version LatestLanguageVersionSupported 31 { 32 get 33 { 34 return new Version(11, 0); 35 } 36 } 37 38 private string GetExtensionPath() 39 { 40 var vscode = IsPrerelease ? ".vscode-insiders" : ".vscode"; 41 var extensionsPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), vscode, "extensions"); 42 if (!Directory.Exists(extensionsPath)) 43 return null; 44 45 return Directory 46 .EnumerateDirectories(extensionsPath, $"{MicrosoftUnityExtensionId}*") // publisherid.extensionid 47 .OrderByDescending(n => n) 48 .FirstOrDefault(); 49 } 50 51 public override string[] GetAnalyzers() 52 { 53 var vstuPath = GetExtensionPath(); 54 if (string.IsNullOrEmpty(vstuPath)) 55 return Array.Empty<string>(); 56 57 return GetAnalyzers(vstuPath); } 58 59 public override IGenerator ProjectGenerator 60 { 61 get 62 { 63 return _generator; 64 } 65 } 66 67 private static bool IsCandidateForDiscovery(string path) 68 { 69#if UNITY_EDITOR_OSX 70 return Directory.Exists(path) && Regex.IsMatch(path, ".*Code.*.app$", RegexOptions.IgnoreCase); 71#elif UNITY_EDITOR_WIN 72 return File.Exists(path) && Regex.IsMatch(path, ".*Code.*.exe$", RegexOptions.IgnoreCase); 73#else 74 return File.Exists(path) && path.EndsWith("code", StringComparison.OrdinalIgnoreCase); 75#endif 76 } 77 78 [Serializable] 79 internal class VisualStudioCodeManifest 80 { 81 public string name; 82 public string version; 83 } 84 85 public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation) 86 { 87 installation = null; 88 89 if (string.IsNullOrEmpty(editorPath)) 90 return false; 91 92 if (!IsCandidateForDiscovery(editorPath)) 93 return false; 94 95 Version version = null; 96 var isPrerelease = false; 97 98 try 99 { 100 var manifestBase = GetRealPath(editorPath); 101 102#if UNITY_EDITOR_WIN 103 // on Windows, editorPath is a file, resources as subdirectory 104 manifestBase = IOPath.GetDirectoryName(manifestBase); 105#elif UNITY_EDITOR_OSX 106 // on Mac, editorPath is a directory 107 manifestBase = IOPath.Combine(manifestBase, "Contents"); 108#else 109 // on Linux, editorPath is a file, in a bin sub-directory 110 var parent = Directory.GetParent(manifestBase); 111 // but we can link to [vscode]/code or [vscode]/bin/code 112 manifestBase = parent?.Name == "bin" ? parent.Parent?.FullName : parent?.FullName; 113#endif 114 115 if (manifestBase == null) 116 return false; 117 118 var manifestFullPath = IOPath.Combine(manifestBase, "resources", "app", "package.json"); 119 if (File.Exists(manifestFullPath)) 120 { 121 var manifest = JsonUtility.FromJson<VisualStudioCodeManifest>(File.ReadAllText(manifestFullPath)); 122 Version.TryParse(manifest.version.Split('-').First(), out version); 123 isPrerelease = manifest.version.ToLower().Contains("insider"); 124 } 125 } 126 catch (Exception) 127 { 128 // do not fail if we are not able to retrieve the exact version number 129 } 130 131 isPrerelease = isPrerelease || editorPath.ToLower().Contains("insider"); 132 installation = new VisualStudioCodeInstallation() 133 { 134 IsPrerelease = isPrerelease, 135 Name = "Visual Studio Code" + (isPrerelease ? " - Insider" : string.Empty) + (version != null ? $" [{version.ToString(3)}]" : string.Empty), 136 Path = editorPath, 137 Version = version ?? new Version() 138 }; 139 140 return true; 141 } 142 143 public static IEnumerable<IVisualStudioInstallation> GetVisualStudioInstallations() 144 { 145 var candidates = new List<string>(); 146 147#if UNITY_EDITOR_WIN 148 var localAppPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs"); 149 var programFiles = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)); 150 151 foreach (var basePath in new[] {localAppPath, programFiles}) 152 { 153 candidates.Add(IOPath.Combine(basePath, "Microsoft VS Code", "Code.exe")); 154 candidates.Add(IOPath.Combine(basePath, "Microsoft VS Code Insiders", "Code - Insiders.exe")); 155 } 156#elif UNITY_EDITOR_OSX 157 var appPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)); 158 candidates.AddRange(Directory.EnumerateDirectories(appPath, "Visual Studio Code*.app")); 159#elif UNITY_EDITOR_LINUX 160 // Well known locations 161 candidates.Add("/usr/bin/code"); 162 candidates.Add("/bin/code"); 163 candidates.Add("/usr/local/bin/code"); 164 165 // Preference ordered base directories relative to which desktop files should be searched 166 candidates.AddRange(GetXdgCandidates()); 167#endif 168 169 foreach (var candidate in candidates.Distinct()) 170 { 171 if (TryDiscoverInstallation(candidate, out var installation)) 172 yield return installation; 173 } 174 } 175 176#if UNITY_EDITOR_LINUX 177 private static readonly Regex DesktopFileExecEntry = new Regex(@"Exec=(\S+)", RegexOptions.Singleline | RegexOptions.Compiled); 178 179 private static IEnumerable<string> GetXdgCandidates() 180 { 181 var envdirs = Environment.GetEnvironmentVariable("XDG_DATA_DIRS"); 182 if (string.IsNullOrEmpty(envdirs)) 183 yield break; 184 185 var dirs = envdirs.Split(':'); 186 foreach(var dir in dirs) 187 { 188 Match match = null; 189 190 try 191 { 192 var desktopFile = IOPath.Combine(dir, "applications/code.desktop"); 193 if (!File.Exists(desktopFile)) 194 continue; 195 196 var content = File.ReadAllText(desktopFile); 197 match = DesktopFileExecEntry.Match(content); 198 } 199 catch 200 { 201 // do not fail if we cannot read desktop file 202 } 203 204 if (match == null || !match.Success) 205 continue; 206 207 yield return match.Groups[1].Value; 208 break; 209 } 210 } 211 212 [System.Runtime.InteropServices.DllImport ("libc")] 213 private static extern int readlink(string path, byte[] buffer, int buflen); 214 215 internal static string GetRealPath(string path) 216 { 217 byte[] buf = new byte[512]; 218 int ret = readlink(path, buf, buf.Length); 219 if (ret == -1) return path; 220 char[] cbuf = new char[512]; 221 int chars = System.Text.Encoding.Default.GetChars(buf, 0, ret, cbuf, 0); 222 return new String(cbuf, 0, chars); 223 } 224#else 225 internal static string GetRealPath(string path) 226 { 227 return path; 228 } 229#endif 230 231 public override void CreateExtraFiles(string projectDirectory) 232 { 233 try 234 { 235 var vscodeDirectory = IOPath.Combine(projectDirectory.NormalizePathSeparators(), ".vscode"); 236 Directory.CreateDirectory(vscodeDirectory); 237 238 var enablePatch = !File.Exists(IOPath.Combine(vscodeDirectory, ".vstupatchdisable")); 239 240 CreateRecommendedExtensionsFile(vscodeDirectory, enablePatch); 241 CreateSettingsFile(vscodeDirectory, enablePatch); 242 CreateLaunchFile(vscodeDirectory, enablePatch); 243 } 244 catch (IOException) 245 { 246 } 247 } 248 249 private const string DefaultLaunchFileContent = @"{ 250 ""version"": ""0.2.0"", 251 ""configurations"": [ 252 { 253 ""name"": ""Attach to Unity"", 254 ""type"": ""vstuc"", 255 ""request"": ""attach"" 256 } 257 ] 258}"; 259 260 private static void CreateLaunchFile(string vscodeDirectory, bool enablePatch) 261 { 262 var launchFile = IOPath.Combine(vscodeDirectory, "launch.json"); 263 if (File.Exists(launchFile)) 264 { 265 if (enablePatch) 266 PatchLaunchFile(launchFile); 267 268 return; 269 } 270 271 File.WriteAllText(launchFile, DefaultLaunchFileContent); 272 } 273 274 private static void PatchLaunchFile(string launchFile) 275 { 276 try 277 { 278 const string configurationsKey = "configurations"; 279 const string typeKey = "type"; 280 281 var content = File.ReadAllText(launchFile); 282 var launch = JSONNode.Parse(content); 283 284 var configurations = launch[configurationsKey] as JSONArray; 285 if (configurations == null) 286 { 287 configurations = new JSONArray(); 288 launch.Add(configurationsKey, configurations); 289 } 290 291 if (configurations.Linq.Any(entry => entry.Value[typeKey].Value == "vstuc")) 292 return; 293 294 var defaultContent = JSONNode.Parse(DefaultLaunchFileContent); 295 configurations.Add(defaultContent[configurationsKey][0]); 296 297 WriteAllTextFromJObject(launchFile, launch); 298 } 299 catch (Exception) 300 { 301 // do not fail if we cannot patch the launch.json file 302 } 303 } 304 305 private void CreateSettingsFile(string vscodeDirectory, bool enablePatch) 306 { 307 var settingsFile = IOPath.Combine(vscodeDirectory, "settings.json"); 308 if (File.Exists(settingsFile)) 309 { 310 if (enablePatch) 311 PatchSettingsFile(settingsFile); 312 313 return; 314 } 315 316 const string excludes = @" ""files.exclude"": { 317 ""**/.DS_Store"": true, 318 ""**/.git"": true, 319 ""**/.vs"": true, 320 ""**/.gitmodules"": true, 321 ""**/.vsconfig"": true, 322 ""**/*.booproj"": true, 323 ""**/*.pidb"": true, 324 ""**/*.suo"": true, 325 ""**/*.user"": true, 326 ""**/*.userprefs"": true, 327 ""**/*.unityproj"": true, 328 ""**/*.dll"": true, 329 ""**/*.exe"": true, 330 ""**/*.pdf"": true, 331 ""**/*.mid"": true, 332 ""**/*.midi"": true, 333 ""**/*.wav"": true, 334 ""**/*.gif"": true, 335 ""**/*.ico"": true, 336 ""**/*.jpg"": true, 337 ""**/*.jpeg"": true, 338 ""**/*.png"": true, 339 ""**/*.psd"": true, 340 ""**/*.tga"": true, 341 ""**/*.tif"": true, 342 ""**/*.tiff"": true, 343 ""**/*.3ds"": true, 344 ""**/*.3DS"": true, 345 ""**/*.fbx"": true, 346 ""**/*.FBX"": true, 347 ""**/*.lxo"": true, 348 ""**/*.LXO"": true, 349 ""**/*.ma"": true, 350 ""**/*.MA"": true, 351 ""**/*.obj"": true, 352 ""**/*.OBJ"": true, 353 ""**/*.asset"": true, 354 ""**/*.cubemap"": true, 355 ""**/*.flare"": true, 356 ""**/*.mat"": true, 357 ""**/*.meta"": true, 358 ""**/*.prefab"": true, 359 ""**/*.unity"": true, 360 ""build/"": true, 361 ""Build/"": true, 362 ""Library/"": true, 363 ""library/"": true, 364 ""obj/"": true, 365 ""Obj/"": true, 366 ""Logs/"": true, 367 ""logs/"": true, 368 ""ProjectSettings/"": true, 369 ""UserSettings/"": true, 370 ""temp/"": true, 371 ""Temp/"": true 372 }"; 373 374 var content = @"{ 375" + excludes + @", 376 ""dotnet.defaultSolution"": """ + IOPath.GetFileName(ProjectGenerator.SolutionFile()) + @""" 377}"; 378 379 File.WriteAllText(settingsFile, content); 380 } 381 382 private void PatchSettingsFile(string settingsFile) 383 { 384 try 385 { 386 const string excludesKey = "files.exclude"; 387 const string solutionKey = "dotnet.defaultSolution"; 388 389 var content = File.ReadAllText(settingsFile); 390 var settings = JSONNode.Parse(content); 391 392 var excludes = settings[excludesKey] as JSONObject; 393 if (excludes == null) 394 return; 395 396 var patchList = new List<string>(); 397 var patched = false; 398 399 // Remove files.exclude for solution+project files in the project root 400 foreach (var exclude in excludes) 401 { 402 if (!bool.TryParse(exclude.Value, out var exc) || !exc) 403 continue; 404 405 var key = exclude.Key; 406 407 if (!key.EndsWith(".sln") && !key.EndsWith(".csproj")) 408 continue; 409 410 if (!Regex.IsMatch(key, "^(\\*\\*[\\\\\\/])?\\*\\.(sln|csproj)$")) 411 continue; 412 413 patchList.Add(key); 414 patched = true; 415 } 416 417 // Check default solution 418 var defaultSolution = settings[solutionKey]; 419 var solutionFile = IOPath.GetFileName(ProjectGenerator.SolutionFile()); 420 if (defaultSolution == null || defaultSolution.Value != solutionFile) 421 { 422 settings[solutionKey] = solutionFile; 423 patched = true; 424 } 425 426 if (!patched) 427 return; 428 429 foreach (var patch in patchList) 430 excludes.Remove(patch); 431 432 WriteAllTextFromJObject(settingsFile, settings); 433 } 434 catch (Exception) 435 { 436 // do not fail if we cannot patch the settings.json file 437 } 438 } 439 440 private const string MicrosoftUnityExtensionId = "visualstudiotoolsforunity.vstuc"; 441 private const string DefaultRecommendedExtensionsContent = @"{ 442 ""recommendations"": [ 443 """+ MicrosoftUnityExtensionId + @""" 444 ] 445} 446"; 447 448 private static void CreateRecommendedExtensionsFile(string vscodeDirectory, bool enablePatch) 449 { 450 // see https://tattoocoder.com/recommending-vscode-extensions-within-your-open-source-projects/ 451 var extensionFile = IOPath.Combine(vscodeDirectory, "extensions.json"); 452 if (File.Exists(extensionFile)) 453 { 454 if (enablePatch) 455 PatchRecommendedExtensionsFile(extensionFile); 456 457 return; 458 } 459 460 File.WriteAllText(extensionFile, DefaultRecommendedExtensionsContent); 461 } 462 463 private static void PatchRecommendedExtensionsFile(string extensionFile) 464 { 465 try 466 { 467 const string recommendationsKey = "recommendations"; 468 469 var content = File.ReadAllText(extensionFile); 470 var extensions = JSONNode.Parse(content); 471 472 var recommendations = extensions[recommendationsKey] as JSONArray; 473 if (recommendations == null) 474 { 475 recommendations = new JSONArray(); 476 extensions.Add(recommendationsKey, recommendations); 477 } 478 479 if (recommendations.Linq.Any(entry => entry.Value.Value == MicrosoftUnityExtensionId)) 480 return; 481 482 recommendations.Add(MicrosoftUnityExtensionId); 483 WriteAllTextFromJObject(extensionFile, extensions); 484 } 485 catch (Exception) 486 { 487 // do not fail if we cannot patch the extensions.json file 488 } 489 } 490 491 private static void WriteAllTextFromJObject(string file, JSONNode node) 492 { 493 using (var fs = File.Open(file, FileMode.Create)) 494 using (var sw = new StreamWriter(fs)) 495 { 496 // Keep formatting/indent in sync with default contents 497 sw.Write(node.ToString(aIndent: 4)); 498 } 499 } 500 501 public override bool Open(string path, int line, int column, string solution) 502 { 503 line = Math.Max(1, line); 504 column = Math.Max(0, column); 505 506 var directory = IOPath.GetDirectoryName(solution); 507 var application = Path; 508 509 ProcessRunner.Start(string.IsNullOrEmpty(path) ? 510 ProcessStartInfoFor(application, $"\"{directory}\"") : 511 ProcessStartInfoFor(application, $"\"{directory}\" -g \"{path}\":{line}:{column}")); 512 513 return true; 514 } 515 516 private static ProcessStartInfo ProcessStartInfoFor(string application, string arguments) 517 { 518#if UNITY_EDITOR_OSX 519 // wrap with built-in OSX open feature 520 arguments = $"-n \"{application}\" --args {arguments}"; 521 application = "open"; 522 return ProcessRunner.ProcessStartInfoFor(application, arguments, redirect:false, shell: true); 523#else 524 return ProcessRunner.ProcessStartInfoFor(application, arguments, redirect: false); 525#endif 526 } 527 528 public static void Initialize() 529 { 530 } 531 } 532}