A game about forced loneliness, made by TACStudios
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}