|
6 | 6 | using System.Collections.Generic; |
7 | 7 | using System.IO; |
8 | 8 | using System.Linq; |
| 9 | +using System.Management.Automation; |
9 | 10 | using System.Security; |
10 | 11 | using System.Text; |
| 12 | +using System.Threading; |
11 | 13 | using Microsoft.Extensions.FileSystemGlobbing; |
12 | 14 | using Microsoft.Extensions.Logging; |
| 15 | +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; |
| 16 | +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; |
13 | 17 | using Microsoft.PowerShell.EditorServices.Services.TextDocument; |
14 | 18 | using Microsoft.PowerShell.EditorServices.Services.Workspace; |
15 | 19 | using Microsoft.PowerShell.EditorServices.Utility; |
@@ -51,9 +55,12 @@ internal class WorkspaceService |
51 | 55 | "**/*" |
52 | 56 | }; |
53 | 57 |
|
| 58 | + private const string s_psPathScheme = "pspath"; |
| 59 | + |
54 | 60 | private readonly ILogger logger; |
55 | 61 | private readonly Version powerShellVersion; |
56 | 62 | private readonly ConcurrentDictionary<string, ScriptFile> workspaceFiles = new(); |
| 63 | + private readonly PsesInternalHost psesInternalHost; |
57 | 64 |
|
58 | 65 | #endregion |
59 | 66 |
|
@@ -100,6 +107,12 @@ public WorkspaceService(ILoggerFactory factory) |
100 | 107 | FollowSymlinks = true; |
101 | 108 | } |
102 | 109 |
|
| 110 | + /// <summary> |
| 111 | + /// Creates a new instance of the Workspace class backed by a PowerShell host. |
| 112 | + /// </summary> |
| 113 | + public WorkspaceService(ILoggerFactory factory, PsesInternalHost psesInternalHost) |
| 114 | + : this(factory) => this.psesInternalHost = psesInternalHost; |
| 115 | + |
103 | 116 | #endregion |
104 | 117 |
|
105 | 118 | #region Public Methods |
@@ -139,18 +152,8 @@ public ScriptFile GetFile(DocumentUri documentUri) |
139 | 152 | // Make sure the file isn't already loaded into the workspace |
140 | 153 | if (!workspaceFiles.TryGetValue(keyName, out ScriptFile scriptFile)) |
141 | 154 | { |
142 | | - // This method allows FileNotFoundException to bubble up |
143 | | - // if the file isn't found. |
144 | | - using (StreamReader streamReader = OpenStreamReader(documentUri)) |
145 | | - { |
146 | | - scriptFile = |
147 | | - new ScriptFile( |
148 | | - documentUri, |
149 | | - streamReader, |
150 | | - powerShellVersion); |
151 | | - |
152 | | - workspaceFiles[keyName] = scriptFile; |
153 | | - } |
| 155 | + scriptFile = ScriptFile.Create(documentUri, ReadFileContents(documentUri), powerShellVersion); |
| 156 | + workspaceFiles[keyName] = scriptFile; |
154 | 157 |
|
155 | 158 | logger.LogDebug("Opened file on disk: " + documentUri.ToString()); |
156 | 159 | } |
@@ -192,18 +195,10 @@ public bool TryGetFile(Uri fileUri, out ScriptFile scriptFile) => |
192 | 195 | /// <param name="scriptFile">The out parameter that will contain the ScriptFile object.</param> |
193 | 196 | public bool TryGetFile(DocumentUri documentUri, out ScriptFile scriptFile) |
194 | 197 | { |
195 | | - switch (documentUri.Scheme) |
| 198 | + if (ScriptFile.IsUntitledPath(documentUri.ToString()) || !ScriptFile.IsSupportedScheme(documentUri.Scheme)) |
196 | 199 | { |
197 | | - // List supported schemes here |
198 | | - case "file": |
199 | | - case "inmemory": |
200 | | - case "untitled": |
201 | | - case "vscode-notebook-cell": |
202 | | - break; |
203 | | - |
204 | | - default: |
205 | | - scriptFile = null; |
206 | | - return false; |
| 200 | + scriptFile = null; |
| 201 | + return false; |
207 | 202 | } |
208 | 203 |
|
209 | 204 | try |
@@ -396,11 +391,53 @@ public IEnumerable<string> EnumeratePSFiles( |
396 | 391 | int maxDepth, |
397 | 392 | bool ignoreReparsePoints) |
398 | 393 | { |
| 394 | + string[] workspacePaths = GetPowerShellWorkspacePaths() |
| 395 | + .Where(path => !string.IsNullOrEmpty(path)) |
| 396 | + .Distinct(StringComparer.OrdinalIgnoreCase) |
| 397 | + .ToArray(); |
| 398 | + |
| 399 | + if (workspacePaths.Length == 0) |
| 400 | + { |
| 401 | + yield break; |
| 402 | + } |
| 403 | + |
| 404 | + if (psesInternalHost is not null) |
| 405 | + { |
| 406 | + PSCommand psCommand = new PSCommand() |
| 407 | + .AddCommand(@"Microsoft.PowerShell.Management\Get-ChildItem") |
| 408 | + .AddParameter("LiteralPath", workspacePaths) |
| 409 | + .AddParameter("Recurse") |
| 410 | + .AddParameter("ErrorAction", ActionPreference.SilentlyContinue) |
| 411 | + .AddParameter("Force") |
| 412 | + .AddParameter("Include", includeGlobs.Concat(VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework).ToArray()) |
| 413 | + .AddParameter("Exclude", excludeGlobs) |
| 414 | + .AddParameter("Depth", maxDepth); |
| 415 | + |
| 416 | + if (VersionUtils.IsNetCore) |
| 417 | + { |
| 418 | + psCommand.AddParameter("FollowSymlink", !ignoreReparsePoints); |
| 419 | + } |
| 420 | + |
| 421 | + psCommand |
| 422 | + .AddCommand("Where-Object") |
| 423 | + .AddParameter("Property", "PSIsContainer") |
| 424 | + .AddParameter("EQ") |
| 425 | + .AddParameter("Value", false); |
| 426 | + |
| 427 | + IReadOnlyList<PSObject> results = psesInternalHost.InvokePSCommand<PSObject>(psCommand, null, CancellationToken.None); |
| 428 | + foreach (string path in results.Select(ConvertWorkspaceItemPath).Where(path => !string.IsNullOrEmpty(path))) |
| 429 | + { |
| 430 | + yield return path; |
| 431 | + } |
| 432 | + |
| 433 | + yield break; |
| 434 | + } |
| 435 | + |
399 | 436 | Matcher matcher = new(); |
400 | 437 | foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); } |
401 | 438 | foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); } |
402 | 439 |
|
403 | | - foreach (string rootPath in WorkspacePaths) |
| 440 | + foreach (string rootPath in workspacePaths) |
404 | 441 | { |
405 | 442 | if (!Directory.Exists(rootPath)) |
406 | 443 | { |
@@ -439,10 +476,97 @@ internal static StreamReader OpenStreamReader(DocumentUri uri) |
439 | 476 | return new StreamReader(fileStream, new UTF8Encoding(), detectEncodingFromByteOrderMarks: true); |
440 | 477 | } |
441 | 478 |
|
442 | | - internal static string ReadFileContents(DocumentUri uri) |
| 479 | + internal string ReadFileContents(DocumentUri uri) |
| 480 | + { |
| 481 | + if (psesInternalHost is null) |
| 482 | + { |
| 483 | + using StreamReader reader = OpenStreamReader(uri); |
| 484 | + return reader.ReadToEnd(); |
| 485 | + } |
| 486 | + |
| 487 | + string psPath = GetPowerShellPath(uri); |
| 488 | + try |
| 489 | + { |
| 490 | + IReadOnlyList<string> result = psesInternalHost.InvokePSCommand<string>( |
| 491 | + new PSCommand() |
| 492 | + .AddCommand(@"Microsoft.PowerShell.Management\Get-Content") |
| 493 | + .AddParameter("LiteralPath", psPath) |
| 494 | + .AddParameter("ErrorAction", ActionPreference.Stop), |
| 495 | + new PowerShellExecutionOptions { ThrowOnError = true }, |
| 496 | + CancellationToken.None); |
| 497 | + |
| 498 | + return string.Join(Environment.NewLine, result); |
| 499 | + } |
| 500 | + catch (ActionPreferenceStopException ex) |
| 501 | + when (ex.ErrorRecord.CategoryInfo.Category == ErrorCategory.ObjectNotFound |
| 502 | + && ex.ErrorRecord.TargetObject is string[] missingFiles |
| 503 | + && missingFiles.Length == 1) |
| 504 | + { |
| 505 | + throw new FileNotFoundException(ex.ErrorRecord.ToString(), missingFiles[0], ex.ErrorRecord.Exception); |
| 506 | + } |
| 507 | + } |
| 508 | + |
| 509 | + private IEnumerable<string> GetPowerShellWorkspacePaths() |
| 510 | + { |
| 511 | + if (WorkspaceFolders.Count > 0) |
| 512 | + { |
| 513 | + return WorkspaceFolders.Select(folder => GetPowerShellPath(folder.Uri)); |
| 514 | + } |
| 515 | + |
| 516 | + return string.IsNullOrEmpty(InitialWorkingDirectory) |
| 517 | + ? Array.Empty<string>() |
| 518 | + : new[] { InitialWorkingDirectory }; |
| 519 | + } |
| 520 | + |
| 521 | + private static string ConvertWorkspaceItemPath(PSObject item) |
| 522 | + { |
| 523 | + if (item.Properties["FullName"]?.Value is string fullName && !string.IsNullOrEmpty(fullName)) |
| 524 | + { |
| 525 | + return fullName; |
| 526 | + } |
| 527 | + |
| 528 | + return item.Properties["PSPath"]?.Value is string psPath && !string.IsNullOrEmpty(psPath) |
| 529 | + ? CreatePowerShellPathUri(psPath) |
| 530 | + : null; |
| 531 | + } |
| 532 | + |
| 533 | + private static string GetPowerShellPath(DocumentUri uri) |
443 | 534 | { |
444 | | - using StreamReader reader = OpenStreamReader(uri); |
445 | | - return reader.ReadToEnd(); |
| 535 | + Uri parsedUri = uri.ToUri(); |
| 536 | + if (parsedUri.IsFile) |
| 537 | + { |
| 538 | + return parsedUri.LocalPath; |
| 539 | + } |
| 540 | + |
| 541 | + if (string.Equals(uri.Scheme, s_psPathScheme, StringComparison.OrdinalIgnoreCase)) |
| 542 | + { |
| 543 | + string provider = parsedUri.GetComponents(UriComponents.Host, UriFormat.Unescaped); |
| 544 | + string path = Uri.UnescapeDataString(parsedUri.AbsolutePath); |
| 545 | + if (path.Length >= 3 && path[0] == '/' && char.IsLetter(path[1]) && path[2] == ':') |
| 546 | + { |
| 547 | + path = path.TrimStart('/'); |
| 548 | + } |
| 549 | + |
| 550 | + return string.IsNullOrEmpty(provider) |
| 551 | + ? path.TrimStart('/') |
| 552 | + : $"{provider}::{path}"; |
| 553 | + } |
| 554 | + |
| 555 | + throw new NotSupportedException($"Unsupported URI scheme '{uri.Scheme}'."); |
| 556 | + } |
| 557 | + |
| 558 | + private static string CreatePowerShellPathUri(string psPath) |
| 559 | + { |
| 560 | + string[] parts = psPath.Split(new[] { "::" }, 2, StringSplitOptions.None); |
| 561 | + if (parts.Length != 2) |
| 562 | + { |
| 563 | + return $"{s_psPathScheme}:///{Uri.EscapeDataString(psPath)}"; |
| 564 | + } |
| 565 | + |
| 566 | + string provider = parts[0].Split('\\').Last(); |
| 567 | + string normalizedPath = parts[1].Replace('\\', '/'); |
| 568 | + string encodedPath = string.Join("/", normalizedPath.Split('/').Select(Uri.EscapeDataString)); |
| 569 | + return $"{s_psPathScheme}://{Uri.EscapeDataString(provider)}/{encodedPath}"; |
446 | 570 | } |
447 | 571 |
|
448 | 572 | /// <summary> |
|
0 commit comments