No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VisualStudioCodeInstallation.cs 15KB

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