Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 262
Support provider-backed workspace URIs via pspath#2136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -6,10 +6,14 @@ | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Management.Automation; | ||
| using System.Security; | ||
| using System.Text; | ||
| using System.Threading; | ||
| using Microsoft.Extensions.FileSystemGlobbing; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; | ||
| using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; | ||
| using Microsoft.PowerShell.EditorServices.Services.TextDocument; | ||
| using Microsoft.PowerShell.EditorServices.Services.Workspace; | ||
| using Microsoft.PowerShell.EditorServices.Utility; | ||
| @@ -51,9 +55,12 @@ internal class WorkspaceService | ||
| "**/*" | ||
| }; | ||
| private const string s_psPathScheme = "pspath"; | ||
| private readonly ILogger logger; | ||
| private readonly Version powerShellVersion; | ||
| private readonly ConcurrentDictionary<string, ScriptFile> workspaceFiles = new(); | ||
| private readonly PsesInternalHost psesInternalHost; | ||
| #endregion | ||
| @@ -100,11 +107,18 @@ public WorkspaceService(ILoggerFactory factory) | ||
| FollowSymlinks = true; | ||
| } | ||
| /// <summary> | ||
| /// Creates a new instance of the Workspace class backed by a PowerShell host. | ||
| /// </summary> | ||
| public WorkspaceService(ILoggerFactory factory, PsesInternalHost psesInternalHost) | ||
| : this(factory) => this.psesInternalHost = psesInternalHost; | ||
| #endregion | ||
| #region Public Methods | ||
| public IEnumerable<string> WorkspacePaths => WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath()); | ||
| public IEnumerable<string> WorkspacePaths => WorkspaceFolders.Select( | ||
| folder => folder.Uri.ToUri().IsFile ? folder.Uri.GetFileSystemPath() : GetPowerShellPath(folder.Uri)); | ||
| /// <summary> | ||
| /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. | ||
| @@ -139,18 +153,8 @@ public ScriptFile GetFile(DocumentUri documentUri) | ||
| // Make sure the file isn't already loaded into the workspace | ||
| if (!workspaceFiles.TryGetValue(keyName, out ScriptFile scriptFile)) | ||
| { | ||
| // This method allows FileNotFoundException to bubble up | ||
| // if the file isn't found. | ||
| using (StreamReader streamReader = OpenStreamReader(documentUri)) | ||
| { | ||
| scriptFile = | ||
| new ScriptFile( | ||
| documentUri, | ||
| streamReader, | ||
| powerShellVersion); | ||
| workspaceFiles[keyName] = scriptFile; | ||
| } | ||
| scriptFile = ScriptFile.Create(documentUri, ReadFileContents(documentUri), powerShellVersion); | ||
| workspaceFiles[keyName] = scriptFile; | ||
| logger.LogDebug("Opened file on disk: " + documentUri.ToString()); | ||
| } | ||
| @@ -192,18 +196,10 @@ public bool TryGetFile(Uri fileUri, out ScriptFile scriptFile) => | ||
| /// <param name="scriptFile">The out parameter that will contain the ScriptFile object.</param> | ||
| public bool TryGetFile(DocumentUri documentUri, out ScriptFile scriptFile) | ||
| { | ||
| switch (documentUri.Scheme) | ||
| if (!ScriptFile.IsSupportedScheme(documentUri.Scheme)) | ||
| { | ||
| // List supported schemes here | ||
| case "file": | ||
| case "inmemory": | ||
| case "untitled": | ||
| case "vscode-notebook-cell": | ||
| break; | ||
| default: | ||
| scriptFile = null; | ||
| return false; | ||
| scriptFile = null; | ||
| return false; | ||
| } | ||
| try | ||
| @@ -396,11 +392,58 @@ public IEnumerable<string> EnumeratePSFiles( | ||
| int maxDepth, | ||
| bool ignoreReparsePoints) | ||
| { | ||
| string[] powerShellWorkspacePaths = GetPowerShellWorkspacePaths() | ||
| .Where(path => !string.IsNullOrEmpty(path)) | ||
| .Distinct(StringComparer.OrdinalIgnoreCase) | ||
| .ToArray(); | ||
| string[] fileSystemWorkspacePaths = GetFileSystemWorkspacePaths() | ||
| .Where(path => !string.IsNullOrEmpty(path)) | ||
| .Distinct(StringComparer.OrdinalIgnoreCase) | ||
| .ToArray(); | ||
| if (powerShellWorkspacePaths.Length == 0 && fileSystemWorkspacePaths.Length == 0) | ||
| { | ||
| yield break; | ||
| } | ||
| if (psesInternalHost is not null && powerShellWorkspacePaths.Length > 0) | ||
| { | ||
| PSCommand psCommand = new PSCommand() | ||
| .AddCommand(@"Microsoft.PowerShell.Management\Get-ChildItem") | ||
| .AddParameter("LiteralPath", powerShellWorkspacePaths) | ||
| .AddParameter("Recurse") | ||
| .AddParameter("ErrorAction", ActionPreference.SilentlyContinue) | ||
| .AddParameter("Force") | ||
| .AddParameter("Include", includeGlobs.Concat(VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework).ToArray()) | ||
| .AddParameter("Exclude", excludeGlobs) | ||
| .AddParameter("Depth", maxDepth); | ||
| if (VersionUtils.IsNetCore) | ||
| { | ||
| psCommand.AddParameter("FollowSymlink", !ignoreReparsePoints); | ||
| } | ||
| psCommand | ||
| .AddCommand("Where-Object") | ||
| .AddParameter("Property", "PSIsContainer") | ||
| .AddParameter("EQ") | ||
| .AddParameter("Value", false); | ||
| IReadOnlyList<PSObject> results = psesInternalHost.InvokePSCommand<PSObject>(psCommand, null, CancellationToken.None); | ||
| foreach (string path in results.Select(ConvertWorkspaceItemPath).Where(path => !string.IsNullOrEmpty(path))) | ||
| { | ||
| yield return path; | ||
| } | ||
| yield break; | ||
| } | ||
| Matcher matcher = new(); | ||
| foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); } | ||
| foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); } | ||
| foreach (string rootPath in WorkspacePaths) | ||
| foreach (string rootPath in fileSystemWorkspacePaths) | ||
| { | ||
| if (!Directory.Exists(rootPath)) | ||
| { | ||
| @@ -439,10 +482,133 @@ internal static StreamReader OpenStreamReader(DocumentUri uri) | ||
| return new StreamReader(fileStream, new UTF8Encoding(), detectEncodingFromByteOrderMarks: true); | ||
| } | ||
| internal static string ReadFileContents(DocumentUri uri) | ||
| internal string ReadFileContents(DocumentUri uri) | ||
| { | ||
| using StreamReader reader = OpenStreamReader(uri); | ||
| return reader.ReadToEnd(); | ||
| if (uri.ToUri().IsFile || psesInternalHost is null) | ||
| { | ||
| using StreamReader reader = OpenStreamReader(uri); | ||
| return reader.ReadToEnd(); | ||
| } | ||
| string psPath = GetPowerShellPath(uri); | ||
| try | ||
| { | ||
| IReadOnlyList<string> result = psesInternalHost.InvokePSCommand<string>( | ||
| new PSCommand() | ||
| .AddCommand(@"Microsoft.PowerShell.Management\Get-Content") | ||
| .AddParameter("LiteralPath", psPath) | ||
| .AddParameter("ErrorAction", ActionPreference.Stop), | ||
| new PowerShellExecutionOptions { ThrowOnError = true }, | ||
| CancellationToken.None); | ||
| return string.Join(Environment.NewLine, result); | ||
| } | ||
| catch (ActionPreferenceStopException ex) | ||
| when (ex.ErrorRecord.CategoryInfo.Category == ErrorCategory.ObjectNotFound | ||
| && ex.ErrorRecord.TargetObject is string[] missingFiles | ||
| && missingFiles.Length == 1) | ||
| { | ||
| throw new FileNotFoundException(ex.ErrorRecord.ToString(), missingFiles[0], ex.ErrorRecord.Exception); | ||
| } | ||
| } | ||
| // Return only file-backed workspace roots as filesystem paths. | ||
| // Example: | ||
| // file:///repo -> /repo | ||
| // pspath://FileSystem/C%3A/repo -> excluded | ||
| private IEnumerable<string> GetFileSystemWorkspacePaths() | ||
| { | ||
| if (WorkspaceFolders.Count > 0) | ||
| { | ||
| return WorkspaceFolders | ||
| .Select(folder => folder.Uri) | ||
| .Where(uri => uri.ToUri().IsFile) | ||
| .Select(uri => uri.GetFileSystemPath()); | ||
| } | ||
| return string.IsNullOrEmpty(InitialWorkingDirectory) | ||
| ? Array.Empty<string>() | ||
| : new[] { InitialWorkingDirectory }; | ||
| } | ||
| // Return only provider-backed workspace roots as PowerShell literal paths. | ||
| // Example: | ||
| // pspath://FileSystem/C%3A/repo -> FileSystem::C:/repo | ||
| // file:///repo -> excluded | ||
| private IEnumerable<string> GetPowerShellWorkspacePaths() | ||
dkattan marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| { | ||
| if (WorkspaceFolders.Count > 0) | ||
| { | ||
| return WorkspaceFolders | ||
| .Select(folder => folder.Uri) | ||
| .Where(uri => !uri.ToUri().IsFile) | ||
| .Select(GetPowerShellPath); | ||
| } | ||
| return Array.Empty<string>(); | ||
| } | ||
| // Normalize Get-ChildItem output to a workspace path string. | ||
| // Example: | ||
| // FullName=/repo/a.ps1 -> /repo/a.ps1 | ||
| // PSPath=Registry::HKEY_CURRENT_USER\\Software\\Foo -> pspath://Registry/HKEY_CURRENT_USER/Software/Foo | ||
| private static string ConvertWorkspaceItemPath(PSObject item) | ||
dkattan marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| { | ||
| if (item.Properties["FullName"]?.Value is string fullName && !string.IsNullOrEmpty(fullName)) | ||
| { | ||
| return fullName; | ||
| } | ||
| return item.Properties["PSPath"]?.Value is string psPath && !string.IsNullOrEmpty(psPath) | ||
| ? CreatePowerShellPathUri(psPath) | ||
| : null; | ||
| } | ||
| // Convert a document URI to the literal path PowerShell commands should use. | ||
| // Example: | ||
| // file:///repo/a.ps1 -> /repo/a.ps1 | ||
| // pspath://FileSystem/C%3A/repo/a.ps1 -> FileSystem::C:/repo/a.ps1 | ||
| private static string GetPowerShellPath(DocumentUri uri) | ||
dkattan marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| { | ||
| Uri parsedUri = uri.ToUri(); | ||
| if (parsedUri.IsFile) | ||
| { | ||
| return parsedUri.LocalPath; | ||
| } | ||
| if (string.Equals(uri.Scheme, s_psPathScheme, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| string provider = parsedUri.GetComponents(UriComponents.Host, UriFormat.Unescaped); | ||
| string path = Uri.UnescapeDataString(parsedUri.AbsolutePath); | ||
| if (path.Length >= 3 && path[0] == '/' && char.IsLetter(path[1]) && path[2] == ':') | ||
| { | ||
| path = path.TrimStart('/'); | ||
| } | ||
| return string.IsNullOrEmpty(provider) | ||
| ? path.TrimStart('/') | ||
| : $"{provider}::{path}"; | ||
| } | ||
| throw new NotSupportedException($"Unsupported URI scheme '{uri.Scheme}'."); | ||
| } | ||
| // Convert a PowerShell provider path to the pspath:// document form used by the workspace. | ||
| // Example: | ||
| // FileSystem::C:\\repo\\a.ps1 -> pspath://FileSystem/C%3A/repo/a.ps1 | ||
| // Registry::HKEY_CURRENT_USER\\Software\\Foo -> pspath://Registry/HKEY_CURRENT_USER/Software/Foo | ||
| private static string CreatePowerShellPathUri(string psPath) | ||
dkattan marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| { | ||
| string[] parts = psPath.Split(new[] { "::" }, 2, StringSplitOptions.None); | ||
| if (parts.Length != 2) | ||
| { | ||
| return $"{s_psPathScheme}:///{Uri.EscapeDataString(psPath)}"; | ||
| } | ||
| string provider = parts[0].Split('\\').Last(); | ||
| string normalizedPath = parts[1].Replace('\\', '/'); | ||
| string encodedPath = string.Join("/", normalizedPath.Split('/').Select(Uri.EscapeDataString)); | ||
| return $"{s_psPathScheme}://{Uri.EscapeDataString(provider)}/{encodedPath}"; | ||
| } | ||
| /// <summary> | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.