From e72a21968a5eb724b9c86b9f61b85afeec45a2f6 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Thu, 15 Feb 2024 11:41:37 -0600 Subject: [PATCH 1/6] Replaced WorkspaceFileSystemWrapper with Get-Content and Get-ChildItem --- .../Handlers/DidChangeWatchedFilesHandler.cs | 2 +- .../Workspace/WorkspaceFileSystemWrapper.cs | 363 ------------------ .../Services/Workspace/WorkspaceService.cs | 137 ++++--- .../Debugging/DebugServiceTests.cs | 2 +- .../Language/CompletionHandlerTests.cs | 10 +- .../Language/SymbolsServiceTests.cs | 2 +- .../Services/Symbols/AstOperationsTests.cs | 3 +- .../Services/Symbols/PSScriptAnalyzerTests.cs | 2 +- .../Session/WorkspaceTests.cs | 5 +- .../packages.lock.json | 22 +- 10 files changed, 105 insertions(+), 443 deletions(-) delete mode 100644 src/PowerShellEditorServices/Services/Workspace/WorkspaceFileSystemWrapper.cs diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DidChangeWatchedFilesHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DidChangeWatchedFilesHandler.cs index edbe9b0ad..8c19796d9 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DidChangeWatchedFilesHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DidChangeWatchedFilesHandler.cs @@ -100,7 +100,7 @@ public Task Handle(DidChangeWatchedFilesParams request, CancellationToken string fileContents; try { - fileContents = WorkspaceService.ReadFileContents(change.Uri); + fileContents = _workspaceService.ReadFileContents(change.Uri); } catch { diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceFileSystemWrapper.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceFileSystemWrapper.cs deleted file mode 100644 index a8861cee2..000000000 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceFileSystemWrapper.cs +++ /dev/null @@ -1,363 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Security; -using Microsoft.Extensions.FileSystemGlobbing.Abstractions; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Logging; - -namespace Microsoft.PowerShell.EditorServices.Services.Workspace -{ - /// - /// A FileSystem wrapper class which only returns files and directories that the consumer is interested in, - /// with a maximum recursion depth and silently ignores most file system errors. Typically this is used by the - /// Microsoft.Extensions.FileSystemGlobbing library. - /// - internal class WorkspaceFileSystemWrapperFactory - { - private readonly string[] _allowedExtensions; - private readonly bool _ignoreReparsePoints; - - /// - /// Gets the maximum depth of the directories that will be searched - /// - internal int MaxRecursionDepth { get; } - - /// - /// Gets the logging facility - /// - internal ILogger Logger { get; } - - /// - /// Gets the directory where the factory is rooted. Only files and directories at this level, or deeper, will be visible - /// by the wrapper - /// - public DirectoryInfoBase RootDirectory { get; } - - /// - /// Creates a new FileWrapper Factory - /// - /// The path to the root directory for the factory. - /// The maximum directory depth. - /// An array of file extensions that will be visible from the factory. For example [".ps1", ".psm1"] - /// Whether objects which are Reparse Points should be ignored. https://docs.microsoft.com/en-us/windows/desktop/fileio/reparse-points - /// An ILogger implementation used for writing log messages. - public WorkspaceFileSystemWrapperFactory(string rootPath, int recursionDepthLimit, string[] allowedExtensions, bool ignoreReparsePoints, ILogger logger) - { - MaxRecursionDepth = recursionDepthLimit; - RootDirectory = new WorkspaceFileSystemDirectoryWrapper(this, new DirectoryInfo(rootPath), 0); - _allowedExtensions = allowedExtensions; - _ignoreReparsePoints = ignoreReparsePoints; - Logger = logger; - } - - /// - /// Creates a wrapped object from . - /// - internal DirectoryInfoBase CreateDirectoryInfoWrapper(DirectoryInfo dirInfo, int depth) => - new WorkspaceFileSystemDirectoryWrapper(this, dirInfo, depth >= 0 ? depth : 0); - - /// - /// Creates a wrapped object from . - /// - internal FileInfoBase CreateFileInfoWrapper(FileInfo fileInfo, int depth) => - new WorkspaceFileSystemFileInfoWrapper(this, fileInfo, depth >= 0 ? depth : 0); - - /// - /// Enumerates all objects in the specified directory and ignores most errors - /// - internal IEnumerable SafeEnumerateFileSystemInfos(DirectoryInfo dirInfo) - { - // Find the subdirectories - string[] subDirs; - try - { - subDirs = Directory.GetDirectories(dirInfo.FullName, "*", SearchOption.TopDirectoryOnly); - } - catch (DirectoryNotFoundException e) - { - Logger.LogHandledException( - $"Could not enumerate directories in the path '{dirInfo.FullName}' due to it being an invalid path", - e); - - yield break; - } - catch (PathTooLongException e) - { - Logger.LogHandledException( - $"Could not enumerate directories in the path '{dirInfo.FullName}' due to the path being too long", - e); - - yield break; - } - catch (Exception e) when (e is SecurityException or UnauthorizedAccessException) - { - Logger.LogHandledException( - $"Could not enumerate directories in the path '{dirInfo.FullName}' due to the path not being accessible", - e); - - yield break; - } - catch (Exception e) - { - Logger.LogHandledException( - $"Could not enumerate directories in the path '{dirInfo.FullName}' due to an exception", - e); - - yield break; - } - foreach (string dirPath in subDirs) - { - DirectoryInfo subDirInfo = new(dirPath); - if (_ignoreReparsePoints && (subDirInfo.Attributes & FileAttributes.ReparsePoint) != 0) { continue; } - yield return subDirInfo; - } - - // Find the files - string[] filePaths; - try - { - filePaths = Directory.GetFiles(dirInfo.FullName, "*", SearchOption.TopDirectoryOnly); - } - catch (DirectoryNotFoundException e) - { - Logger.LogHandledException( - $"Could not enumerate files in the path '{dirInfo.FullName}' due to it being an invalid path", - e); - - yield break; - } - catch (PathTooLongException e) - { - Logger.LogHandledException( - $"Could not enumerate files in the path '{dirInfo.FullName}' due to the path being too long", - e); - - yield break; - } - catch (Exception e) when (e is SecurityException or UnauthorizedAccessException) - { - Logger.LogHandledException( - $"Could not enumerate files in the path '{dirInfo.FullName}' due to the path not being accessible", - e); - - yield break; - } - catch (Exception e) - { - Logger.LogHandledException( - $"Could not enumerate files in the path '{dirInfo.FullName}' due to an exception", - e); - - yield break; - } - foreach (string filePath in filePaths) - { - FileInfo fileInfo = new(filePath); - if (_allowedExtensions == null || _allowedExtensions.Length == 0) { yield return fileInfo; continue; } - if (_ignoreReparsePoints && (fileInfo.Attributes & FileAttributes.ReparsePoint) != 0) { continue; } - foreach (string extension in _allowedExtensions) - { - if (fileInfo.Extension == extension) { yield return fileInfo; break; } - } - } - } - } - - /// - /// Wraps an instance of and provides implementation of - /// . - /// Based on https://github.com/aspnet/Extensions/blob/c087cadf1dfdbd2b8785ef764e5ef58a1a7e5ed0/src/FileSystemGlobbing/src/Abstractions/DirectoryInfoWrapper.cs - /// - internal class WorkspaceFileSystemDirectoryWrapper : DirectoryInfoBase - { - private readonly DirectoryInfo _concreteDirectoryInfo; - private readonly bool _isParentPath; - private readonly WorkspaceFileSystemWrapperFactory _fsWrapperFactory; - private readonly int _depth; - - /// - /// Initializes an instance of . - /// - public WorkspaceFileSystemDirectoryWrapper(WorkspaceFileSystemWrapperFactory factory, DirectoryInfo directoryInfo, int depth) - { - _concreteDirectoryInfo = directoryInfo; - _isParentPath = depth == 0; - _fsWrapperFactory = factory; - _depth = depth; - } - - /// - public override IEnumerable EnumerateFileSystemInfos() - { - if (!_concreteDirectoryInfo.Exists || _depth >= _fsWrapperFactory.MaxRecursionDepth) { yield break; } - foreach (FileSystemInfo fileSystemInfo in _fsWrapperFactory.SafeEnumerateFileSystemInfos(_concreteDirectoryInfo)) - { - switch (fileSystemInfo) - { - case DirectoryInfo dirInfo: - yield return _fsWrapperFactory.CreateDirectoryInfoWrapper(dirInfo, _depth + 1); - break; - case FileInfo fileInfo: - yield return _fsWrapperFactory.CreateFileInfoWrapper(fileInfo, _depth); - break; - default: - // We should NEVER get here, but if we do just continue on - break; - } - } - } - - /// - /// Returns an instance of that represents a subdirectory. - /// - /// - /// If equals '..', this returns the parent directory. - /// - /// The directory name. - /// The directory - public override DirectoryInfoBase GetDirectory(string name) - { - bool isParentPath = string.Equals(name, "..", StringComparison.Ordinal); - - if (isParentPath) { return ParentDirectory; } - - DirectoryInfo[] dirs = _concreteDirectoryInfo.GetDirectories(name); - - if (dirs.Length == 1) { return _fsWrapperFactory.CreateDirectoryInfoWrapper(dirs[0], _depth + 1); } - if (dirs.Length == 0) { return null; } - // This shouldn't happen. The parameter name isn't supposed to contain wild card. - throw new InvalidOperationException( - string.Format( - System.Globalization.CultureInfo.CurrentCulture, - "More than one sub directories are found under {0} with name {1}.", - _concreteDirectoryInfo.FullName, name)); - } - - /// - public override FileInfoBase GetFile(string name) => _fsWrapperFactory.CreateFileInfoWrapper(new FileInfo(Path.Combine(_concreteDirectoryInfo.FullName, name)), _depth); - - /// - public override string Name => _isParentPath ? ".." : _concreteDirectoryInfo.Name; - - /// - /// Returns the full path to the directory. - /// - public override string FullName => _concreteDirectoryInfo.FullName; - - /// - /// Safely calculates the parent of this directory, swallowing most errors. - /// - private DirectoryInfoBase SafeParentDirectory() - { - try - { - return _fsWrapperFactory.CreateDirectoryInfoWrapper(_concreteDirectoryInfo.Parent, _depth - 1); - } - catch (DirectoryNotFoundException e) - { - _fsWrapperFactory.Logger.LogHandledException( - $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to it being an invalid path", - e); - } - catch (PathTooLongException e) - { - _fsWrapperFactory.Logger.LogHandledException( - $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to the path being too long", - e); - } - catch (Exception e) when (e is SecurityException or UnauthorizedAccessException) - { - _fsWrapperFactory.Logger.LogHandledException( - $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to the path not being accessible", - e); - } - catch (Exception e) - { - _fsWrapperFactory.Logger.LogHandledException( - $"Could not get parent of '{_concreteDirectoryInfo.FullName}' due to an exception", - e); - } - return null; - } - - /// - /// Returns the parent directory. (Overrides ). - /// - public override DirectoryInfoBase ParentDirectory => SafeParentDirectory(); - } - - /// - /// Wraps an instance of to provide implementation of . - /// - internal class WorkspaceFileSystemFileInfoWrapper : FileInfoBase - { - private readonly FileInfo _concreteFileInfo; - private readonly WorkspaceFileSystemWrapperFactory _fsWrapperFactory; - private readonly int _depth; - - /// - /// Initializes instance of to wrap the specified object . - /// - public WorkspaceFileSystemFileInfoWrapper(WorkspaceFileSystemWrapperFactory factory, FileInfo fileInfo, int depth) - { - _fsWrapperFactory = factory; - _concreteFileInfo = fileInfo; - _depth = depth; - } - - /// - /// The file name. (Overrides ). - /// - public override string Name => _concreteFileInfo.Name; - - /// - /// The full path of the file. (Overrides ). - /// - public override string FullName => _concreteFileInfo.FullName; - - /// - /// Safely calculates the parent of this file, swallowing most errors. - /// - private DirectoryInfoBase SafeParentDirectory() - { - try - { - return _fsWrapperFactory.CreateDirectoryInfoWrapper(_concreteFileInfo.Directory, _depth); - } - catch (DirectoryNotFoundException e) - { - _fsWrapperFactory.Logger.LogHandledException( - $"Could not get parent of '{_concreteFileInfo.FullName}' due to it being an invalid path", - e); - } - catch (PathTooLongException e) - { - _fsWrapperFactory.Logger.LogHandledException( - $"Could not get parent of '{_concreteFileInfo.FullName}' due to the path being too long", - e); - } - catch (Exception e) when (e is SecurityException or UnauthorizedAccessException) - { - _fsWrapperFactory.Logger.LogHandledException( - $"Could not get parent of '{_concreteFileInfo.FullName}' due to the path not being accessible", - e); - } - catch (Exception e) - { - _fsWrapperFactory.Logger.LogHandledException( - $"Could not get parent of '{_concreteFileInfo.FullName}' due to an exception", - e); - } - return null; - } - - /// - /// The directory containing the file. (Overrides ). - /// - public override DirectoryInfoBase ParentDirectory => SafeParentDirectory(); - } -} diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index 54a1f2894..cd26d9289 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -6,12 +6,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Management.Automation; using System.Security; using System.Text; -using Microsoft.Extensions.FileSystemGlobbing; +using System.Threading; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Services.Workspace; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -84,6 +85,8 @@ internal class WorkspaceService /// public bool FollowSymlinks { get; set; } + private readonly IInternalPowerShellExecutionService executionService; + #endregion #region Constructors @@ -91,13 +94,14 @@ internal class WorkspaceService /// /// Creates a new instance of the Workspace class. /// - public WorkspaceService(ILoggerFactory factory) + public WorkspaceService(ILoggerFactory factory, IInternalPowerShellExecutionService executionService) { powerShellVersion = VersionUtils.PSVersion; logger = factory.CreateLogger(); WorkspaceFolders = new List(); ExcludeFilesGlob = new List(); FollowSymlinks = true; + this.executionService = executionService; } #endregion @@ -139,19 +143,9 @@ 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; - } - + string fileContent = ReadFileContents(documentUri); + scriptFile = new ScriptFile(documentUri, fileContent, powerShellVersion); + workspaceFiles[keyName] = scriptFile; logger.LogDebug("Opened file on disk: " + documentUri.ToString()); } @@ -348,7 +342,6 @@ public IEnumerable EnumeratePSFiles() ignoreReparsePoints: !FollowSymlinks ); } - /// /// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace folders in a /// recursive manner. Falls back to initial working directory if there are no workspace folders. @@ -360,33 +353,22 @@ public IEnumerable EnumeratePSFiles( int maxDepth, bool ignoreReparsePoints) { - Matcher matcher = new(); - foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); } - foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); } + PSCommand psCommand = new(); + psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-ChildItem") + .AddParameter("Path", WorkspacePaths) + .AddParameter("File") + .AddParameter("Recurse") + .AddParameter("ErrorAction", "SilentlyContinue") + .AddParameter("Force") + .AddParameter("Include", includeGlobs.Concat(VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework)) + .AddParameter("Exclude", excludeGlobs) + .AddParameter("Depth", maxDepth) + .AddParameter("FollowSymlink", !ignoreReparsePoints) + .AddCommand("Select-Object") + .AddParameter("ExpandObject", "FullName"); + IEnumerable results = executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + return results; - foreach (string rootPath in WorkspacePaths) - { - if (!Directory.Exists(rootPath)) - { - continue; - } - - WorkspaceFileSystemWrapperFactory fsFactory = new( - rootPath, - maxDepth, - VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework, - ignoreReparsePoints, - logger); - - PatternMatchingResult fileMatchResult = matcher.Execute(fsFactory.RootDirectory); - foreach (FilePatternMatch item in fileMatchResult.Files) - { - // item.Path always contains forward slashes in paths when it should be backslashes on Windows. - // Since we're returning strings here, it's important to use the correct directory separator. - string path = VersionUtils.IsWindows ? item.Path.Replace('/', Path.DirectorySeparatorChar) : item.Path; - yield return Path.Combine(rootPath, path); - } - } } #endregion @@ -403,10 +385,57 @@ 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(); + PSCommand psCommand = new(); + string pspath; + if (uri.Scheme == Uri.UriSchemeFile) + { + pspath = uri.ToUri().LocalPath; + } + else + { + string PSProvider = uri.Authority; + string path = uri.Path; + pspath = $"{PSProvider}::{path}"; + } + /* uri - "file:///c:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" + * Authority = "" + * Fragment = "" + * Path = "/C:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" + * Query = "" + * Scheme = "file" + * PSPath - "Microsoft.PowerShell.Core\FileSystem::C:\Users\dkattan\source\repos\immybot-ref\submodules\PowerShellEditorServices\test\PowerShellEditorServices.Test.Shared\Completion\CompletionExamples.psm1" + * + * Suggested Format: + * Authority = "Microsoft.PowerShell.Core\FileSystem" + * Scheme = "PSProvider" + * Path = "/C:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" + * Result -> "PSProvider://Microsoft.PowerShell.Core/FileSystem::C:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" + * + * Suggested Format 2: + * Authority = "" + * Scheme = "FileSystem" + * Path = "/C:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" + * Result "FileSystem://c:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" + + */ + psCommand.AddCommand("Get-Content") + .AddParameter("LiteralPath", pspath) + .AddParameter("Raw", true) + .AddParameter("ErrorAction", ActionPreference.Stop); + try + { + IEnumerable result = executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None, new PowerShell.Execution.PowerShellExecutionOptions() + { + ThrowOnError = true + }).ConfigureAwait(false).GetAwaiter().GetResult(); + return result.FirstOrDefault(); + } + catch (ActionPreferenceStopException ex) when (ex.ErrorRecord.CategoryInfo.Category == ErrorCategory.ObjectNotFound && ex.ErrorRecord.TargetObject is string[] missingFiles && missingFiles.Count() == 1) + { + throw new FileNotFoundException(ex.ErrorRecord.ToString(), missingFiles.First(), ex.ErrorRecord.Exception); + } } internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(InitialWorkingDirectory, path); @@ -429,10 +458,18 @@ internal string ResolveRelativeScriptPath(string baseFilePath, string relativePa // Get the directory of the original script file, combine it // with the given path and then resolve the absolute file path. combinedPath = - Path.GetFullPath( - Path.Combine( - baseFilePath, - relativePath)); + Path.GetFullPath( + Path.Combine( + baseFilePath, + relativePath)); + + PSCommand psCommand = new(); + psCommand.AddCommand("Resolve-Path") + .AddParameter("Relative", true) + .AddParameter("Path", relativePath) + .AddParameter("RelativeBasePath", baseFilePath); + IEnumerable result = executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + combinedPath = result.FirstOrDefault(); } catch (NotSupportedException e) { diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 30b020c30..670cac4e1 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -70,7 +70,7 @@ public DebugServiceTests() debugService.DebuggerStopped += OnDebuggerStopped; // Load the test debug files. - workspace = new WorkspaceService(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance, PsesHostFactory.Create(NullLoggerFactory.Instance)); debugScriptFile = GetDebugScript("DebugTest.ps1"); oddPathScriptFile = GetDebugScript("Debug' W&ith $Params [Test].ps1"); variableScriptFile = GetDebugScript("VariableTest.ps1"); diff --git a/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs b/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs index fae0f8104..bed56fe45 100644 --- a/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs @@ -30,7 +30,7 @@ public class CompletionHandlerTests : IDisposable public CompletionHandlerTests() { psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance, psesHost); completionHandler = new PsesCompletionHandler(NullLoggerFactory.Instance, psesHost, psesHost, workspace); } @@ -42,12 +42,12 @@ public void Dispose() GC.SuppressFinalize(this); } - private ScriptFile GetScriptFile(ScriptRegion scriptRegion) => workspace.GetFile(TestUtilities.GetSharedPath(scriptRegion.File)); + private async Task GetScriptFile(ScriptRegion scriptRegion) => workspace.GetFile(TestUtilities.GetSharedPath(scriptRegion.File)); - private Task GetCompletionResultsAsync(ScriptRegion scriptRegion) + private async Task GetCompletionResultsAsync(ScriptRegion scriptRegion) { - return completionHandler.GetCompletionsInFileAsync( - GetScriptFile(scriptRegion), + return await completionHandler.GetCompletionsInFileAsync( + await GetScriptFile(scriptRegion), scriptRegion.StartLineNumber, scriptRegion.StartColumnNumber, CancellationToken.None); diff --git a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs index f078d8d42..3a9a793b5 100644 --- a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs @@ -40,7 +40,7 @@ public class SymbolsServiceTests : IDisposable public SymbolsServiceTests() { psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance, psesHost); workspace.WorkspaceFolders.Add(new WorkspaceFolder { Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("References")) diff --git a/test/PowerShellEditorServices.Test/Services/Symbols/AstOperationsTests.cs b/test/PowerShellEditorServices.Test/Services/Symbols/AstOperationsTests.cs index 649ef32fd..fe8048d4f 100644 --- a/test/PowerShellEditorServices.Test/Services/Symbols/AstOperationsTests.cs +++ b/test/PowerShellEditorServices.Test/Services/Symbols/AstOperationsTests.cs @@ -7,6 +7,7 @@ using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Test; using Microsoft.PowerShell.EditorServices.Test.Shared; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using Xunit; @@ -20,7 +21,7 @@ public class AstOperationsTests public AstOperationsTests() { - WorkspaceService workspace = new(NullLoggerFactory.Instance); + WorkspaceService workspace = new(NullLoggerFactory.Instance, PsesHostFactory.Create(NullLoggerFactory.Instance)); scriptFile = workspace.GetFile(TestUtilities.GetSharedPath("References/FunctionReference.ps1")); } diff --git a/test/PowerShellEditorServices.Test/Services/Symbols/PSScriptAnalyzerTests.cs b/test/PowerShellEditorServices.Test/Services/Symbols/PSScriptAnalyzerTests.cs index 1155db102..ed7782999 100644 --- a/test/PowerShellEditorServices.Test/Services/Symbols/PSScriptAnalyzerTests.cs +++ b/test/PowerShellEditorServices.Test/Services/Symbols/PSScriptAnalyzerTests.cs @@ -15,7 +15,7 @@ namespace PowerShellEditorServices.Test.Services.Symbols [Trait("Category", "PSScriptAnalyzer")] public class PSScriptAnalyzerTests { - private readonly WorkspaceService workspaceService = new(NullLoggerFactory.Instance); + private readonly WorkspaceService workspaceService = new(NullLoggerFactory.Instance, PsesHostFactory.Create(NullLoggerFactory.Instance)); private readonly AnalysisService analysisService; private const string script = "function Do-Work {}"; diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index 01f31325a..c35a17304 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -12,6 +12,7 @@ using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol; +using Microsoft.PowerShell.EditorServices.Test; namespace PowerShellEditorServices.Test.Session { @@ -39,7 +40,7 @@ public void CanResolveWorkspaceRelativePath() ScriptFile testPathOutside = CreateScriptFile("c:/Test/PeerPath/FilePath.ps1"); ScriptFile testPathAnotherDrive = CreateScriptFile("z:/TryAndFindMe/FilePath.ps1"); - WorkspaceService workspace = new(NullLoggerFactory.Instance); + WorkspaceService workspace = new(NullLoggerFactory.Instance, PsesHostFactory.Create(NullLoggerFactory.Instance)); // Test with zero workspace folders Assert.Equal( @@ -77,7 +78,7 @@ public void CanResolveWorkspaceRelativePath() internal static WorkspaceService FixturesWorkspace() { - return new WorkspaceService(NullLoggerFactory.Instance) + return new WorkspaceService(NullLoggerFactory.Instance, PsesHostFactory.Create(NullLoggerFactory.Instance)) { WorkspaceFolders = { diff --git a/test/PowerShellEditorServices.Test/packages.lock.json b/test/PowerShellEditorServices.Test/packages.lock.json index 002721a39..7ab35988d 100644 --- a/test/PowerShellEditorServices.Test/packages.lock.json +++ b/test/PowerShellEditorServices.Test/packages.lock.json @@ -11,15 +11,6 @@ "Microsoft.CodeCoverage": "17.8.0" } }, - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3" - } - }, "Microsoft.PowerShell.5.ReferenceAssemblies": { "type": "Direct", "requested": "[1.1.0, )", @@ -183,11 +174,6 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "Microsoft.NETFramework.ReferenceAssemblies.net462": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ==" - }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "17.8.0", @@ -550,7 +536,7 @@ "Microsoft.PowerShell.EditorServices.Test.Shared": { "type": "Project", "dependencies": { - "Microsoft.PowerShell.EditorServices": "[3.16.0, )" + "Microsoft.PowerShell.EditorServices": "[3.17.0, )" } } }, @@ -1691,7 +1677,7 @@ "Microsoft.PowerShell.EditorServices.Test.Shared": { "type": "Project", "dependencies": { - "Microsoft.PowerShell.EditorServices": "[3.16.0, )" + "Microsoft.PowerShell.EditorServices": "[3.17.0, )" } } }, @@ -2820,7 +2806,7 @@ "Microsoft.PowerShell.EditorServices.Test.Shared": { "type": "Project", "dependencies": { - "Microsoft.PowerShell.EditorServices": "[3.16.0, )" + "Microsoft.PowerShell.EditorServices": "[3.17.0, )" } } }, @@ -3948,7 +3934,7 @@ "Microsoft.PowerShell.EditorServices.Test.Shared": { "type": "Project", "dependencies": { - "Microsoft.PowerShell.EditorServices": "[3.16.0, )" + "Microsoft.PowerShell.EditorServices": "[3.17.0, )" } } } From 974e92e870549ddf587fcccbe01c20889c93c419 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Thu, 15 Feb 2024 13:38:00 -0600 Subject: [PATCH 2/6] Added test for pspath schema. --- .../Services/TextDocument/ScriptFile.cs | 28 +++++++++++++--- .../Services/Workspace/WorkspaceService.cs | 33 +++---------------- .../Debugging/DebugServiceTests.cs | 24 ++++++++++++++ .../Session/ScriptFileTests.cs | 1 + 4 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index 4909d1020..c2372369e 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -184,12 +184,32 @@ internal static List GetLines(string text) /// True if the path is an untitled file, false otherwise. internal static bool IsUntitledPath(string path) { - Validate.IsNotNull(nameof(path), path); - // This may not have been given a URI, so return false instead of throwing. - return Uri.IsWellFormedUriString(path, UriKind.RelativeOrAbsolute) && - !string.Equals(DocumentUri.From(path).Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase); + if (!Uri.IsWellFormedUriString(path, UriKind.RelativeOrAbsolute)) + { + return false; + } + DocumentUri documentUri = DocumentUri.From(path); + if (!IsSupportedScheme(documentUri.Scheme)) + { + return false; + } + return documentUri.Scheme switch + { + // List supported schemes here + "inmemory" or "untitled" or "vscode-notebook-cell" => true, + _ => false, + }; } + internal static bool IsSupportedScheme(string? scheme) + { + return scheme switch + { + // List supported schemes here + "file" or "inmemory" or "untitled" or "vscode-notebook-cell" or "pspath" => true, + _ => false, + }; + } /// /// Gets a line from the file's contents. /// diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index cd26d9289..817d2844b 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Management.Automation; using System.Security; -using System.Text; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.PowerShell; @@ -186,20 +185,6 @@ public bool TryGetFile(Uri fileUri, out ScriptFile scriptFile) => /// The out parameter that will contain the ScriptFile object. public bool TryGetFile(DocumentUri documentUri, out ScriptFile scriptFile) { - switch (documentUri.Scheme) - { - // List supported schemes here - case "file": - case "inmemory": - case "untitled": - case "vscode-notebook-cell": - break; - - default: - scriptFile = null; - return false; - } - try { scriptFile = GetFile(documentUri); @@ -375,31 +360,23 @@ public IEnumerable EnumeratePSFiles( #region Private Methods - internal static StreamReader OpenStreamReader(DocumentUri uri) - { - FileStream fileStream = new(uri.GetFileSystemPath(), FileMode.Open, FileAccess.Read); - // Default to UTF8 no BOM if a BOM is not present. Note that `Encoding.UTF8` is *with* - // BOM, so we call the ctor here to get the BOM-less version. - // - // TODO: Honor workspace encoding settings for the fallback. - return new StreamReader(fileStream, new UTF8Encoding(), detectEncodingFromByteOrderMarks: true); - } - internal string ReadFileContents(DocumentUri uri) { PSCommand psCommand = new(); string pspath; if (uri.Scheme == Uri.UriSchemeFile) { + // uri - "file:///c:/Users/me/test.ps1" + pspath = uri.ToUri().LocalPath; } else { string PSProvider = uri.Authority; - string path = uri.Path; - pspath = $"{PSProvider}::{path}"; + string path = uri.Path.TrimStart('/'); + pspath = $"{PSProvider.Replace("-", "\\")}::{path}"; } - /* uri - "file:///c:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" + /* * Authority = "" * Fragment = "" * Path = "/C:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 670cac4e1..c4568d971 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -19,6 +19,7 @@ using Microsoft.PowerShell.EditorServices.Test; using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.LanguageServer.Protocol; using Xunit; namespace PowerShellEditorServices.Test.Debugging @@ -42,6 +43,7 @@ public class DebugServiceTests : IDisposable private readonly WorkspaceService workspace; private readonly ScriptFile debugScriptFile; private readonly ScriptFile oddPathScriptFile; + private readonly ScriptFile psProviderPathScriptFile; private readonly ScriptFile variableScriptFile; private readonly TestReadLine testReadLine = new(); @@ -74,6 +76,12 @@ public DebugServiceTests() debugScriptFile = GetDebugScript("DebugTest.ps1"); oddPathScriptFile = GetDebugScript("Debug' W&ith $Params [Test].ps1"); variableScriptFile = GetDebugScript("VariableTest.ps1"); + string variableScriptFilePath = TestUtilities.GetSharedPath(Path.Combine("Debugging", "VariableTest.ps1")); + dynamic psitem = psesHost.ExecutePSCommandAsync(new PSCommand().AddCommand("Get-Item").AddParameter("LiteralPath", variableScriptFilePath), CancellationToken.None).GetAwaiter().GetResult().FirstOrDefault(); + Uri fileUri = new Uri(psitem.FullName); + string pspathUriString = new DocumentUri(scheme: "pspath", authority: $"{psitem.PSProvider.ToString().Replace("\\", "-")}", path: $"/{fileUri.AbsolutePath}", query: string.Empty, fragment: string.Empty).ToString(); + // pspath://microsoft.powershell.core-filesystem/c:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 + psProviderPathScriptFile = workspace.GetFile(pspathUriString); } public void Dispose() @@ -621,6 +629,22 @@ public async Task OddFilePathsLaunchCorrectly() Assert.Equal(". " + PSCommandHelpers.EscapeScriptFilePath(oddPathScriptFile.FilePath), Assert.Single(historyResult)); } + + [Fact] + public async Task PSProviderPathsLaunchCorrectly() + { + ConfigurationDoneHandler configurationDoneHandler = new( + NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost); + await configurationDoneHandler.LaunchScriptAsync(psProviderPathScriptFile.FilePath); + + IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync( + new PSCommand().AddScript("(Get-History).CommandLine"), + CancellationToken.None); + + // Check the PowerShell history + Assert.Equal(". " + PSCommandHelpers.EscapeScriptFilePath(oddPathScriptFile.FilePath), Assert.Single(historyResult)); + } + [Fact] public async Task DebuggerVariableStringDisplaysCorrectly() { diff --git a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs index 11c27c4cf..2c00acfa6 100644 --- a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs +++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs @@ -669,6 +669,7 @@ public void DocumentUriReturnsCorrectStringForAbsolutePath() [InlineData("Untitled:Untitled-1", true)] [InlineData(@"'a log statement' > 'c:\Users\me\Documents\test.txt' ", false)] + [InlineData(@"PSPath://FileSystem/C:/Users/me/Documents/test.ps1", false)] public void IsUntitledFileIsCorrect(string path, bool expected) => Assert.Equal(expected, ScriptFile.IsUntitledPath(path)); } } From 8aec52d245fa759dad86013ffa689cdf00cc46e6 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Fri, 16 Feb 2024 13:49:46 -0600 Subject: [PATCH 3/6] WIP --- .../packages.lock.json | 14 ------------- .../Debugging/DebugServiceTests.cs | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/packages.lock.json b/src/PowerShellEditorServices.Hosting/packages.lock.json index 8be3c7711..72c35d82a 100644 --- a/src/PowerShellEditorServices.Hosting/packages.lock.json +++ b/src/PowerShellEditorServices.Hosting/packages.lock.json @@ -2,15 +2,6 @@ "version": 1, "dependencies": { ".NETFramework,Version=v4.6.2": { - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3" - } - }, "NETStandard.Library": { "type": "Direct", "requested": "[2.0.3, )", @@ -175,11 +166,6 @@ "resolved": "1.1.0", "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" }, - "Microsoft.NETFramework.ReferenceAssemblies.net462": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ==" - }, "Microsoft.VisualStudio.Threading": { "type": "Transitive", "resolved": "17.6.40", diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index c4568d971..60915bf41 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -78,12 +78,32 @@ public DebugServiceTests() variableScriptFile = GetDebugScript("VariableTest.ps1"); string variableScriptFilePath = TestUtilities.GetSharedPath(Path.Combine("Debugging", "VariableTest.ps1")); dynamic psitem = psesHost.ExecutePSCommandAsync(new PSCommand().AddCommand("Get-Item").AddParameter("LiteralPath", variableScriptFilePath), CancellationToken.None).GetAwaiter().GetResult().FirstOrDefault(); + string psPath = psitem.PSPath.ToString(); + + + + Uri fileUri = new Uri(psitem.FullName); string pspathUriString = new DocumentUri(scheme: "pspath", authority: $"{psitem.PSProvider.ToString().Replace("\\", "-")}", path: $"/{fileUri.AbsolutePath}", query: string.Empty, fragment: string.Empty).ToString(); // pspath://microsoft.powershell.core-filesystem/c:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 psProviderPathScriptFile = workspace.GetFile(pspathUriString); } + // the following function converts Microsoft.PowerShell.Core\FileSystem::C:\Users\dkattan\source\repos\immybot-ref\submodules\PowerShellEditorServices\test\PowerShellEditorServices.Test.Shared\Debugging\VariableTest.ps1 to filesystem://c:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1 + private string ConvertPSPathToUri(string pspath) + { + string[] pspathParts = pspath.Split("::"); + string[] provider = pspathParts[0]; + if (provider.Contains("\\")) + { + provider = provider.Split("\\")[1]; + } + string[] driveAndPath = pspathParts[1].Split("\\"); + string drive = driveAndPath[0]; + string path = driveAndPath[1]; + return new DocumentUri(scheme: "filesystem", authority: string.Empty, path: $"/{drive}:{path}", query: string.Empty, fragment: string.Empty).ToString(); + } + public void Dispose() { debugService.Abort(); From 18dd57c8a82cbcb81806b9040f133f9e44e671a8 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Sat, 17 Feb 2024 15:24:52 -0600 Subject: [PATCH 4/6] Replaced GetAwaiter().GetResult() with SynchronousPowerShellTask --- .../Services/Workspace/WorkspaceService.cs | 91 ++++++++++++------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index 817d2844b..c769f9d31 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -10,7 +10,8 @@ using System.Security; using System.Threading; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol; @@ -84,7 +85,7 @@ internal class WorkspaceService /// public bool FollowSymlinks { get; set; } - private readonly IInternalPowerShellExecutionService executionService; + private readonly PsesInternalHost psesInternalHost; #endregion @@ -93,14 +94,14 @@ internal class WorkspaceService /// /// Creates a new instance of the Workspace class. /// - public WorkspaceService(ILoggerFactory factory, IInternalPowerShellExecutionService executionService) + public WorkspaceService(ILoggerFactory factory, PsesInternalHost executionService) { powerShellVersion = VersionUtils.PSVersion; logger = factory.CreateLogger(); WorkspaceFolders = new List(); ExcludeFilesGlob = new List(); FollowSymlinks = true; - this.executionService = executionService; + this.psesInternalHost = executionService; } #endregion @@ -185,6 +186,11 @@ public bool TryGetFile(Uri fileUri, out ScriptFile scriptFile) => /// The out parameter that will contain the ScriptFile object. public bool TryGetFile(DocumentUri documentUri, out ScriptFile scriptFile) { + if (ScriptFile.IsUntitledPath(documentUri.ToString())) + { + scriptFile = null; + return false; + } try { scriptFile = GetFile(documentUri); @@ -338,22 +344,29 @@ public IEnumerable EnumeratePSFiles( int maxDepth, bool ignoreReparsePoints) { - PSCommand psCommand = new(); - psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Get-ChildItem") - .AddParameter("Path", WorkspacePaths) - .AddParameter("File") - .AddParameter("Recurse") - .AddParameter("ErrorAction", "SilentlyContinue") - .AddParameter("Force") - .AddParameter("Include", includeGlobs.Concat(VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework)) - .AddParameter("Exclude", excludeGlobs) - .AddParameter("Depth", maxDepth) - .AddParameter("FollowSymlink", !ignoreReparsePoints) + IEnumerable results = new SynchronousPowerShellTask( + logger, + psesInternalHost, + new PSCommand() + .AddCommand(@"Microsoft.PowerShell.Management\Get-ChildItem") + .AddParameter("LiteralPath", WorkspacePaths) + .AddParameter("Recurse") + .AddParameter("ErrorAction", "SilentlyContinue") + .AddParameter("Force") + .AddParameter("Include", includeGlobs.Concat(VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework)) + .AddParameter("Exclude", excludeGlobs) + .AddParameter("Depth", maxDepth) + .AddParameter("FollowSymlink", !ignoreReparsePoints) + .AddCommand("Where-Object") + .AddParameter("Property", "PSIsContainer") + .AddParameter("EQ") + .AddParameter("Value", false) .AddCommand("Select-Object") - .AddParameter("ExpandObject", "FullName"); - IEnumerable results = executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + .AddParameter("ExpandObject", "FullName"), + null, + CancellationToken.None) + .ExecuteAndGetResult(CancellationToken.None); return results; - } #endregion @@ -374,7 +387,7 @@ internal string ReadFileContents(DocumentUri uri) { string PSProvider = uri.Authority; string path = uri.Path.TrimStart('/'); - pspath = $"{PSProvider.Replace("-", "\\")}::{path}"; + pspath = $"{PSProvider}::{path}"; } /* * Authority = "" @@ -397,22 +410,25 @@ internal string ReadFileContents(DocumentUri uri) * Result "FileSystem://c:/Users/dkattan/source/repos/immybot-ref/submodules/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1" */ - psCommand.AddCommand("Get-Content") - .AddParameter("LiteralPath", pspath) - .AddParameter("Raw", true) - .AddParameter("ErrorAction", ActionPreference.Stop); + IEnumerable result; try { - IEnumerable result = executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None, new PowerShell.Execution.PowerShellExecutionOptions() - { - ThrowOnError = true - }).ConfigureAwait(false).GetAwaiter().GetResult(); - return result.FirstOrDefault(); + result = new SynchronousPowerShellTask( + logger, + psesInternalHost, + psCommand.AddCommand("Get-Content") + .AddParameter("LiteralPath", pspath) + .AddParameter("ErrorAction", ActionPreference.Stop), + new PowerShellExecutionOptions() + { + ThrowOnError = true + }, CancellationToken.None).ExecuteAndGetResult(CancellationToken.None); } catch (ActionPreferenceStopException ex) when (ex.ErrorRecord.CategoryInfo.Category == ErrorCategory.ObjectNotFound && ex.ErrorRecord.TargetObject is string[] missingFiles && missingFiles.Count() == 1) { throw new FileNotFoundException(ex.ErrorRecord.ToString(), missingFiles.First(), ex.ErrorRecord.Exception); } + return string.Join(Environment.NewLine, result); } internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(InitialWorkingDirectory, path); @@ -439,13 +455,18 @@ internal string ResolveRelativeScriptPath(string baseFilePath, string relativePa Path.Combine( baseFilePath, relativePath)); - - PSCommand psCommand = new(); - psCommand.AddCommand("Resolve-Path") - .AddParameter("Relative", true) - .AddParameter("Path", relativePath) - .AddParameter("RelativeBasePath", baseFilePath); - IEnumerable result = executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + IEnumerable result; + result = new SynchronousPowerShellTask( + logger, + psesInternalHost, + new PSCommand() + .AddCommand("Resolve-Path") + .AddParameter("Relative", true) + .AddParameter("Path", relativePath) + .AddParameter("RelativeBasePath", baseFilePath), + new(), + CancellationToken.None) + .ExecuteAndGetResult(CancellationToken.None); combinedPath = result.FirstOrDefault(); } catch (NotSupportedException e) From 097c8b8c5f6405a828f566df6aa5c241ea4bcb28 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Sat, 17 Feb 2024 15:25:34 -0600 Subject: [PATCH 5/6] WIP: Added details to TabExpansion2 error message --- .../Symbols/Visitors/AstOperations.cs | 104 ++++++++++-------- .../Handlers/CompletionHandler.cs | 2 +- 2 files changed, 57 insertions(+), 49 deletions(-) diff --git a/src/PowerShellEditorServices/Services/Symbols/Visitors/AstOperations.cs b/src/PowerShellEditorServices/Services/Symbols/Visitors/AstOperations.cs index 65d2d03e0..0f91159bd 100644 --- a/src/PowerShellEditorServices/Services/Symbols/Visitors/AstOperations.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Visitors/AstOperations.cs @@ -78,58 +78,66 @@ public static async Task GetCompletionsAsync( IScriptPosition cursorPosition = s_clonePositionWithNewOffset(scriptAst.Extent.StartScriptPosition, fileOffset); Stopwatch stopwatch = new(); logger.LogTrace($"Getting completions at offset {fileOffset} (line: {cursorPosition.LineNumber}, column: {cursorPosition.ColumnNumber})"); - - CommandCompletion commandCompletion = await executionService.ExecuteDelegateAsync( - representation: "CompleteInput", - new ExecutionOptions { Priority = ExecutionPriority.Next }, - (pwsh, _) => - { - stopwatch.Start(); - - // If the current runspace is not out of process, then we call TabExpansion2 so - // that we have the ability to issue pipeline stop requests on cancellation. - if (executionService is PsesInternalHost psesInternalHost - && !psesInternalHost.Runspace.RunspaceIsRemote) - { - IReadOnlyList completionResults = new SynchronousPowerShellTask( - logger, - psesInternalHost, - new PSCommand() - .AddCommand("TabExpansion2") - .AddParameter("ast", scriptAst) - .AddParameter("tokens", currentTokens) - .AddParameter("positionOfCursor", cursorPosition), - executionOptions: null, - cancellationToken) - .ExecuteAndGetResult(cancellationToken); - - if (completionResults is { Count: > 0 }) + CommandCompletion commandCompletion = null; + try + { + commandCompletion = await executionService.ExecuteDelegateAsync( + representation: "CompleteInput", + new ExecutionOptions { Priority = ExecutionPriority.Next }, + (pwsh, _) => { - return completionResults[0]; - } - - return null; - } - - // If the current runspace is out of process, we can't call TabExpansion2 - // because the output will be serialized. - return CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: pwsh); - }, - cancellationToken).ConfigureAwait(false); - - stopwatch.Stop(); - - if (commandCompletion is null) + stopwatch.Start(); + + // If the current runspace is not out of process, then we call TabExpansion2 so + // that we have the ability to issue pipeline stop requests on cancellation. + if (executionService is PsesInternalHost psesInternalHost + && !psesInternalHost.Runspace.RunspaceIsRemote) + { + IReadOnlyList completionResults = new SynchronousPowerShellTask( + logger, + psesInternalHost, + new PSCommand() + .AddCommand("TabExpansion2") + .AddParameter("ast", scriptAst) + .AddParameter("tokens", currentTokens) + .AddParameter("positionOfCursor", cursorPosition), + executionOptions: new PowerShellExecutionOptions() + { + ThrowOnError = true + }, + cancellationToken) + .ExecuteAndGetResult(cancellationToken); + + if (completionResults is { Count: > 0 }) + { + return completionResults[0]; + } + } + return null; + + // If the current runspace is out of process, we can't call TabExpansion2 + // because the output will be serialized. + return CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: pwsh); + }, + cancellationToken).ConfigureAwait(false); + + } + catch (WildcardPatternException) + { + // This hits when you press Ctrl+Space inside empty square brackets [] + } + catch (RuntimeException ex) { - logger.LogError("Error Occurred in TabExpansion2"); + logger.LogError($"Error Occurred in TabExpansion2: {ex.ErrorRecord}", ex.ErrorRecord.Exception); } - else + finally { + stopwatch.Stop(); logger.LogTrace( "IntelliSense completed in {elapsed}ms - WordToComplete: \"{word}\" MatchCount: {count}", stopwatch.ElapsedMilliseconds, diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs index 29e36ce25..3e43e817e 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs @@ -215,7 +215,7 @@ internal async Task GetCompletionsInFileAsync( _logger, cancellationToken).ConfigureAwait(false); - if (result.CompletionMatches.Count == 0) + if (result is not { CompletionMatches.Count: > 0 }) { return new CompletionResults(IsIncomplete: true, Array.Empty()); } From 7f57a6c02e461bcdb5d6301691948fcdeeec4bee Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Sat, 17 Feb 2024 15:26:18 -0600 Subject: [PATCH 6/6] Updated ConvertPSPathToUri method --- .../Debugging/DebugServiceTests.cs | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 60915bf41..73bfd4dee 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -1,27 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.PowerShell.EditorServices.Handlers; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Console; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Test; -using Microsoft.PowerShell.EditorServices.Test.Shared; -using Microsoft.PowerShell.EditorServices.Utility; -using OmniSharp.Extensions.LanguageServer.Protocol; -using Xunit; - namespace PowerShellEditorServices.Test.Debugging { internal class TestReadLine : IReadLine @@ -101,7 +80,7 @@ private string ConvertPSPathToUri(string pspath) string[] driveAndPath = pspathParts[1].Split("\\"); string drive = driveAndPath[0]; string path = driveAndPath[1]; - return new DocumentUri(scheme: "filesystem", authority: string.Empty, path: $"/{drive}:{path}", query: string.Empty, fragment: string.Empty).ToString(); + return new DocumentUri(scheme: provider, authority: string.Empty, path: $"/{drive}:{path}", query: string.Empty, fragment: string.Empty).ToString(); } public void Dispose()