From 7f13a1456e7c4fcb244f30bfbf39cb849530b055 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 16 Aug 2023 18:53:48 -0700 Subject: [PATCH 1/4] Fix up extension API Receive a `EditorOperationResponse` (not yet otherwise used). Delete dead code. Add optional `content` parameter to `NewFile`. Expose `CloseFile` and `SaveFile`. Fix an outdated warning message. --- .../Extensions/EditorRequests.cs | 6 +-- .../Extensions/EditorWindow.cs | 3 -- .../Extensions/EditorWorkspace.cs | 34 +++++++++++++---- .../Extensions/IEditorOperations.cs | 8 ++-- .../Extension/EditorOperationsService.cs | 37 +++++++++---------- 5 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorRequests.cs b/src/PowerShellEditorServices/Extensions/EditorRequests.cs index 446cd2dfc..34dca2928 100644 --- a/src/PowerShellEditorServices/Extensions/EditorRequests.cs +++ b/src/PowerShellEditorServices/Extensions/EditorRequests.cs @@ -25,10 +25,10 @@ internal class ExtensionCommandRemovedNotification internal class GetEditorContextRequest { } - internal enum EditorCommandResponse + internal enum EditorOperationResponse { - Unsupported, - OK + Completed, + Failed } internal class InsertTextRequest diff --git a/src/PowerShellEditorServices/Extensions/EditorWindow.cs b/src/PowerShellEditorServices/Extensions/EditorWindow.cs index 428c192ea..8c9048c3c 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWindow.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWindow.cs @@ -39,8 +39,6 @@ internal EditorWindow(IEditorOperations editorOperations) #endregion #region Public Methods - #pragma warning disable VSTHRD002 // These are public APIs that use async internal methods. - /// /// Shows an informational message to the user. /// @@ -72,7 +70,6 @@ internal EditorWindow(IEditorOperations editorOperations) /// A timeout in milliseconds for how long the message should remain visible. public void SetStatusBarMessage(string message, int timeout) => editorOperations.SetStatusBarMessageAsync(message, timeout).Wait(); - #pragma warning restore VSTHRD002 #endregion } } diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index aa5802564..d12de06e3 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -18,7 +18,8 @@ public sealed class EditorWorkspace #region Properties /// - /// Gets the current workspace path if there is one for the open editor or null otherwise. + /// Gets the server's initial working directory, since the extension API doesn't have a + /// multi-root workspace concept. /// public string Path => editorOperations.GetWorkspacePath(); @@ -31,22 +32,23 @@ public sealed class EditorWorkspace #endregion #region Public Methods - #pragma warning disable VSTHRD002 // These are public APIs that use async internal methods. + // TODO: Consider returning bool instead of void to indicate success? /// - /// Creates a new file in the editor + /// Creates a new file in the editor. /// - public void NewFile() => editorOperations.NewFileAsync().Wait(); + /// The content to place in the new file. + public void NewFile(string content = "") => editorOperations.NewFileAsync(content).Wait(); /// - /// Opens a file in the workspace. If the file is already open + /// Opens a file in the workspace. If the file is already open /// its buffer will be made active. /// /// The path to the file to be opened. public void OpenFile(string filePath) => editorOperations.OpenFileAsync(filePath).Wait(); /// - /// Opens a file in the workspace. If the file is already open + /// Opens a file in the workspace. If the file is already open /// its buffer will be made active. /// You can specify whether the file opens as a preview or as a durable editor. /// @@ -54,7 +56,25 @@ public sealed class EditorWorkspace /// Determines wether the file is opened as a preview or as a durable editor. public void OpenFile(string filePath, bool preview) => editorOperations.OpenFileAsync(filePath, preview).Wait(); - #pragma warning restore VSTHRD002 + /// + /// Closes a file in the workspace. + /// + /// The path to the file to be closed. + public void CloseFile(string filePath) => editorOperations.CloseFileAsync(filePath).Wait(); + + /// + /// Saves an open file in the workspace. + /// + /// The path to the file to be saved. + public void SaveFile(string filePath) => editorOperations.SaveFileAsync(filePath).Wait(); + + /// + /// Saves a file with a new name AKA a copy. + /// + /// The file to copy. + /// The file to create. + public void SaveFile(string oldFilePath, string newFilePath) => editorOperations.SaveFileAsync(oldFilePath, newFilePath).Wait(); + #endregion } } diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 6f282eeea..07472a487 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -20,9 +20,10 @@ internal interface IEditorOperations Task GetEditorContextAsync(); /// - /// Gets the path to the editor's active workspace. + /// Gets the server's initial working directory, since the extension API doesn't have a + /// multi-root workspace concept. /// - /// The workspace path or null if there isn't one. + /// The server's initial working directory. string GetWorkspacePath(); /// @@ -35,8 +36,9 @@ internal interface IEditorOperations /// /// Causes a new untitled file to be created in the editor. /// + /// The content to insert into the new file. /// A task that can be awaited for completion. - Task NewFileAsync(); + Task NewFileAsync(string content = ""); /// /// Causes a file to be opened in the editor. If the file is diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index fa6da7f90..949c2321e 100644 --- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -13,11 +13,8 @@ namespace Microsoft.PowerShell.EditorServices.Services.Extension { internal class EditorOperationsService : IEditorOperations { - private const bool DefaultPreviewSetting = true; - private readonly PsesInternalHost _psesHost; private readonly WorkspaceService _workspaceService; - private readonly ILanguageServerFacade _languageServer; public EditorOperationsService( @@ -72,7 +69,7 @@ public async Task InsertTextAsync(string filePath, string text, BufferRange inse Character = insertRange.End.Column - 1 } } - }).ReturningVoid(CancellationToken.None).ConfigureAwait(false); + }).Returning(CancellationToken.None).ConfigureAwait(false); } public async Task SetSelectionAsync(BufferRange selectionRange) @@ -98,7 +95,7 @@ public async Task SetSelectionAsync(BufferRange selectionRange) Character = selectionRange.End.Column - 1 } } - }).ReturningVoid(CancellationToken.None).ConfigureAwait(false); + }).Returning(CancellationToken.None).ConfigureAwait(false); } public EditorContext ConvertClientEditorContext( @@ -123,15 +120,15 @@ public EditorContext ConvertClientEditorContext( clientContext.CurrentFileLanguage); } - public async Task NewFileAsync() + public async Task NewFileAsync(string content = "") { if (!TestHasLanguageServer()) { return; } - await _languageServer.SendRequest("editor/newFile", null) - .ReturningVoid(CancellationToken.None) + await _languageServer.SendRequest("editor/newFile", content) + .Returning(CancellationToken.None) .ConfigureAwait(false); } @@ -145,8 +142,8 @@ public async Task OpenFileAsync(string filePath) await _languageServer.SendRequest("editor/openFile", new OpenFileDetails { FilePath = filePath, - Preview = DefaultPreviewSetting - }).ReturningVoid(CancellationToken.None).ConfigureAwait(false); + Preview = true + }).Returning(CancellationToken.None).ConfigureAwait(false); } public async Task OpenFileAsync(string filePath, bool preview) @@ -160,7 +157,7 @@ public async Task OpenFileAsync(string filePath, bool preview) { FilePath = filePath, Preview = preview - }).ReturningVoid(CancellationToken.None).ConfigureAwait(false); + }).Returning(CancellationToken.None).ConfigureAwait(false); } public async Task CloseFileAsync(string filePath) @@ -171,7 +168,7 @@ public async Task CloseFileAsync(string filePath) } await _languageServer.SendRequest("editor/closeFile", filePath) - .ReturningVoid(CancellationToken.None) + .Returning(CancellationToken.None) .ConfigureAwait(false); } @@ -188,11 +185,11 @@ public async Task SaveFileAsync(string currentPath, string newSavePath) { FilePath = currentPath, NewPath = newSavePath - }).ReturningVoid(CancellationToken.None).ConfigureAwait(false); + }).Returning(CancellationToken.None).ConfigureAwait(false); } - // TODO: This should get the current editor's context and use it to determine which - // workspace it's in. + // NOTE: This name is now outdated since we don't have a way to distinguish one workspace + // from another for the extension API. public string GetWorkspacePath() => _workspaceService.InitialWorkingDirectory; public string GetWorkspaceRelativePath(string filePath) => _workspaceService.GetRelativePath(filePath); @@ -205,7 +202,7 @@ public async Task ShowInformationMessageAsync(string message) } await _languageServer.SendRequest("editor/showInformationMessage", message) - .ReturningVoid(CancellationToken.None) + .Returning(CancellationToken.None) .ConfigureAwait(false); } @@ -217,7 +214,7 @@ public async Task ShowErrorMessageAsync(string message) } await _languageServer.SendRequest("editor/showErrorMessage", message) - .ReturningVoid(CancellationToken.None) + .Returning(CancellationToken.None) .ConfigureAwait(false); } @@ -229,7 +226,7 @@ public async Task ShowWarningMessageAsync(string message) } await _languageServer.SendRequest("editor/showWarningMessage", message) - .ReturningVoid(CancellationToken.None) + .Returning(CancellationToken.None) .ConfigureAwait(false); } @@ -244,7 +241,7 @@ public async Task SetStatusBarMessageAsync(string message, int? timeout) { Message = message, Timeout = timeout - }).ReturningVoid(CancellationToken.None).ConfigureAwait(false); + }).Returning(CancellationToken.None).ConfigureAwait(false); } public void ClearTerminal() @@ -267,7 +264,7 @@ private bool TestHasLanguageServer(bool warnUser = true) if (warnUser) { _psesHost.UI.WriteWarningLine( - "Editor operations are not supported in temporary consoles. Re-run the command in the main PowerShell Intergrated Console."); + "Editor operations are not supported in temporary consoles. Re-run the command in the main Extension Terminal."); } return false; From 88dbfc9df7e4c0db6c4aab25c2cd8024dcd10749 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:56:30 -0700 Subject: [PATCH 2/4] Support multi-root workspaces for `CurrentFile.RelativePath` --- .../Extensions/FileContext.cs | 3 +- .../Extensions/IEditorOperations.cs | 3 +- .../Extension/EditorOperationsService.cs | 2 +- .../Handlers/GetCommentHelpHandler.cs | 4 +- .../Services/TextDocument/ScriptFile.cs | 18 +--- .../Services/Workspace/WorkspaceService.cs | 68 ++++----------- .../Session/WorkspaceTests.cs | 83 +++++++++---------- 7 files changed, 63 insertions(+), 118 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/FileContext.cs b/src/PowerShellEditorServices/Extensions/FileContext.cs index 770cc33f7..c8c32e58e 100644 --- a/src/PowerShellEditorServices/Extensions/FileContext.cs +++ b/src/PowerShellEditorServices/Extensions/FileContext.cs @@ -58,8 +58,7 @@ public sealed class FileContext /// /// Gets the workspace-relative path of the file. /// - public string WorkspacePath => editorOperations.GetWorkspaceRelativePath( - scriptFile.FilePath); + public string WorkspacePath => editorOperations.GetWorkspaceRelativePath(scriptFile); #endregion diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 07472a487..63bae4574 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -29,9 +29,8 @@ internal interface IEditorOperations /// /// Resolves the given file path relative to the current workspace path. /// - /// The file path to be resolved. /// The resolved file path. - string GetWorkspaceRelativePath(string filePath); + string GetWorkspaceRelativePath(ScriptFile scriptFile); /// /// Causes a new untitled file to be created in the editor. diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index 949c2321e..a187533c5 100644 --- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -192,7 +192,7 @@ public async Task SaveFileAsync(string currentPath, string newSavePath) // from another for the extension API. public string GetWorkspacePath() => _workspaceService.InitialWorkingDirectory; - public string GetWorkspaceRelativePath(string filePath) => _workspaceService.GetRelativePath(filePath); + public string GetWorkspaceRelativePath(ScriptFile scriptFile) => _workspaceService.GetRelativePath(scriptFile); public async Task ShowInformationMessageAsync(string message) { diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommentHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommentHelpHandler.cs index 27288ce90..10013a09b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommentHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommentHelpHandler.cs @@ -51,7 +51,7 @@ public async Task Handle(CommentHelpRequestParams requ { // check if the previous character is `<` because it invalidates // the param block the follows it. - IList lines = ScriptFile.GetLinesInternal(funcText); + IList lines = ScriptFile.GetLines(funcText); int relativeTriggerLine0b = triggerLine - funcExtent.StartLineNumber; if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<", StringComparison.OrdinalIgnoreCase) > -1) { @@ -68,7 +68,7 @@ public async Task Handle(CommentHelpRequestParams requ return result; } - List helpLines = ScriptFile.GetLinesInternal(helpText); + List helpLines = ScriptFile.GetLines(helpText); if (helpLocation?.Equals("before", StringComparison.OrdinalIgnoreCase) == false) { diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index 3e52e4938..4909d1020 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -31,13 +31,6 @@ internal sealed class ScriptFile #region Properties - /// - /// Gets a unique string that identifies this file. At this time, - /// this property returns a normalized version of the value stored - /// in the FilePath property. - /// - public string Id => FilePath.ToLower(); - /// /// Gets the path at which this file resides. /// @@ -173,14 +166,7 @@ internal ScriptFile( /// /// Input string to be split up into lines. /// The lines in the string. - internal static IList GetLines(string text) => GetLinesInternal(text); - - /// - /// Get the lines in a string. - /// - /// Input string to be split up into lines. - /// The lines in the string. - internal static List GetLinesInternal(string text) + internal static List GetLines(string text) { if (text == null) { @@ -520,7 +506,7 @@ internal void SetFileContents(string fileContents) { // Split the file contents into lines and trim // any carriage returns from the strings. - FileLines = GetLinesInternal(fileContents); + FileLines = GetLines(fileContents); // Parse the contents to get syntax tree and errors ParseFileContents(); diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index f705101f8..fce8cab20 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -306,27 +306,31 @@ public void CloseFile(ScriptFile scriptFile) /// /// Gets the workspace-relative path of the given file path. /// - /// The original full file path. /// A relative file path - public string GetRelativePath(string filePath) + public string GetRelativePath(ScriptFile scriptFile) { - string resolvedPath = filePath; - - if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(InitialWorkingDirectory)) + Uri fileUri = scriptFile.DocumentUri.ToUri(); + if (!scriptFile.IsInMemory) { - Uri workspaceUri = new(InitialWorkingDirectory); - Uri fileUri = new(filePath); - - resolvedPath = workspaceUri.MakeRelativeUri(fileUri).ToString(); - - // Convert the directory separators if necessary - if (Path.DirectorySeparatorChar == '\\') + // Support calculating out-of-workspace relative paths in the common case of a + // single workspace folder. Otherwise try to get the matching folder. + foreach (WorkspaceFolder workspaceFolder in WorkspaceFolders) { - resolvedPath = resolvedPath.Replace('/', '\\'); + Uri workspaceUri = workspaceFolder.Uri.ToUri(); + if (WorkspaceFolders.Count == 1 || workspaceUri.IsBaseOf(fileUri)) + { + return workspaceUri.MakeRelativeUri(fileUri).ToString(); + } } } - return resolvedPath; + // Default to the absolute file path if possible, otherwise just return the URI. This + // removes the scheme and initial slash when possible. + if (fileUri.IsAbsoluteUri) + { + return fileUri.AbsolutePath; + } + return fileUri.ToString(); } /// @@ -407,42 +411,6 @@ internal static string ReadFileContents(DocumentUri uri) return reader.ReadToEnd(); } - internal static bool IsPathInMemory(string filePath) - { - bool isInMemory = false; - - // In cases where a "virtual" file is displayed in the editor, - // we need to treat the file differently than one that exists - // on disk. A virtual file could be something like a diff - // view of the current file or an untitled file. - try - { - // File system absolute paths will have a URI scheme of file:. - // Other schemes like "untitled:" and "gitlens-git:" will return false for IsFile. - Uri uri = new(filePath); - isInMemory = !uri.IsFile; - } - catch (UriFormatException) - { - // Relative file paths cause a UriFormatException. - // In this case, fallback to using Path.GetFullPath(). - try - { - Path.GetFullPath(filePath); - } - catch (Exception ex) when (ex is ArgumentException or NotSupportedException) - { - isInMemory = true; - } - catch (PathTooLongException) - { - // If we ever get here, it should be an actual file so, not in memory - } - } - - return isInMemory; - } - internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(InitialWorkingDirectory, path); internal string ResolveRelativeScriptPath(string baseFilePath, string relativePath) diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index 2c7a44279..1fff5eb3a 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -10,6 +10,9 @@ using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Xunit; +using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol; namespace PowerShellEditorServices.Test.Session { @@ -22,27 +25,51 @@ public class WorkspaceTests ? s_lazyDriveLetter.Value : string.Empty; + internal static ScriptFile CreateScriptFile(string path) => new(path, "", VersionUtils.PSVersion); + + [Fact] public void CanResolveWorkspaceRelativePath() { - string workspacePath = TestUtilities.NormalizePath("c:/Test/Workspace/"); - string testPathInside = TestUtilities.NormalizePath("c:/Test/Workspace/SubFolder/FilePath.ps1"); - string testPathOutside = TestUtilities.NormalizePath("c:/Test/PeerPath/FilePath.ps1"); - string testPathAnotherDrive = TestUtilities.NormalizePath("z:/TryAndFindMe/FilePath.ps1"); + string workspacePath = "c:/Test/Workspace/"; + ScriptFile testPathInside = CreateScriptFile("c:/Test/Workspace/SubFolder/FilePath.ps1"); + ScriptFile testPathOutside = CreateScriptFile("c:/Test/PeerPath/FilePath.ps1"); + ScriptFile testPathAnotherDrive = CreateScriptFile("z:/TryAndFindMe/FilePath.ps1"); WorkspaceService workspace = new(NullLoggerFactory.Instance); - // Test without a workspace path - Assert.Equal(testPathOutside, workspace.GetRelativePath(testPathOutside)); + // Test with zero workspace folders + Assert.Equal( + testPathOutside.DocumentUri.ToUri().AbsolutePath, + workspace.GetRelativePath(testPathOutside)); - string expectedInsidePath = TestUtilities.NormalizePath("SubFolder/FilePath.ps1"); - string expectedOutsidePath = TestUtilities.NormalizePath("../PeerPath/FilePath.ps1"); + string expectedInsidePath = "SubFolder/FilePath.ps1"; + string expectedOutsidePath = "../PeerPath/FilePath.ps1"; + + // Test with a single workspace folder + workspace.WorkspaceFolders.Add(new WorkspaceFolder + { + Uri = DocumentUri.FromFileSystemPath(workspacePath) + }); - // Test with a workspace path - workspace.InitialWorkingDirectory = workspacePath; Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside)); Assert.Equal(expectedOutsidePath, workspace.GetRelativePath(testPathOutside)); - Assert.Equal(testPathAnotherDrive, workspace.GetRelativePath(testPathAnotherDrive)); + Assert.Equal( + testPathAnotherDrive.DocumentUri.ToUri().AbsolutePath, + workspace.GetRelativePath(testPathAnotherDrive)); + + // Test with two workspace folders + string anotherWorkspacePath = "c:/Test/AnotherWorkspace/"; + ScriptFile anotherTestPathInside = CreateScriptFile("c:/Test/AnotherWorkspace/DifferentFolder/FilePath.ps1"); + string anotherExpectedInsidePath = "DifferentFolder/FilePath.ps1"; + + workspace.WorkspaceFolders.Add(new WorkspaceFolder + { + Uri = DocumentUri.FromFileSystemPath(anotherWorkspacePath) + }); + + Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside)); + Assert.Equal(anotherExpectedInsidePath, workspace.GetRelativePath(anotherTestPathInside)); } internal static WorkspaceService FixturesWorkspace() @@ -143,40 +170,6 @@ public void CanRecurseDirectoryTreeWithGlobs() }, actual); } - [Fact] - public void CanDetermineIsPathInMemory() - { - string tempDir = Path.GetTempPath(); - string shortDirPath = Path.Combine(tempDir, "GitHub", "PowerShellEditorServices"); - string shortFilePath = Path.Combine(shortDirPath, "foo.ps1"); - const string shortUriForm = "git:/c%3A/Users/Keith/GitHub/dahlbyk/posh-git/src/PoshGitTypes.ps1?%7B%22path%22%3A%22c%3A%5C%5CUsers%5C%5CKeith%5C%5CGitHub%5C%5Cdahlbyk%5C%5Cposh-git%5C%5Csrc%5C%5CPoshGitTypes.ps1%22%2C%22ref%22%3A%22~%22%7D"; - const string longUriForm = "gitlens-git:c%3A%5CUsers%5CKeith%5CGitHub%5Cdahlbyk%5Cposh-git%5Csrc%5CPoshGitTypes%3Ae0022701.ps1?%7B%22fileName%22%3A%22src%2FPoshGitTypes.ps1%22%2C%22repoPath%22%3A%22c%3A%2FUsers%2FKeith%2FGitHub%2Fdahlbyk%2Fposh-git%22%2C%22sha%22%3A%22e0022701fa12e0bc22d0458673d6443c942b974a%22%7D"; - - string[] inMemoryPaths = new[] { - // Test short non-file paths - "untitled:untitled-1", - shortUriForm, - "inmemory://foo.ps1", - // Test long non-file path - longUriForm - }; - - Assert.All(inMemoryPaths, (p) => Assert.True(WorkspaceService.IsPathInMemory(p))); - - string[] notInMemoryPaths = new[] { - // Test short file absolute paths - shortDirPath, - shortFilePath, - new Uri(shortDirPath).ToString(), - new Uri(shortFilePath).ToString(), - // Test short file relative paths - "foo.ps1", - Path.Combine(new[] { "..", "foo.ps1" }) - }; - - Assert.All(notInMemoryPaths, (p) => Assert.False(WorkspaceService.IsPathInMemory(p))); - } - [Fact] public void CanOpenAndCloseFile() { From 3f2e1427f2dd3e0701769e03a0cc4a6a74f340f1 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 17 Aug 2023 13:17:14 -0700 Subject: [PATCH 3/4] Add `Paths` to workspace API for multi-root workspaces Since `Path` now refers to initial working directory. --- .../Extensions/EditorWorkspace.cs | 5 +++++ .../Extensions/IEditorOperations.cs | 6 ++++++ .../Services/Extension/EditorOperationsService.cs | 3 +++ .../Services/Workspace/WorkspaceService.cs | 10 +++++----- .../Session/WorkspaceTests.cs | 8 ++++++++ 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index d12de06e3..46930cda6 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -23,6 +23,11 @@ public sealed class EditorWorkspace /// public string Path => editorOperations.GetWorkspacePath(); + /// + /// Get all the workspace folders' paths. + /// + public string[] Paths => editorOperations.GetWorkspacePaths(); + #endregion #region Constructors diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 63bae4574..c349bcae1 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -26,6 +26,12 @@ internal interface IEditorOperations /// The server's initial working directory. string GetWorkspacePath(); + /// + /// Get all the workspace folders' paths. + /// + /// + string[] GetWorkspacePaths(); + /// /// Resolves the given file path relative to the current workspace path. /// diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index a187533c5..4ff235d94 100644 --- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -6,6 +6,7 @@ using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -192,6 +193,8 @@ public async Task SaveFileAsync(string currentPath, string newSavePath) // from another for the extension API. public string GetWorkspacePath() => _workspaceService.InitialWorkingDirectory; + public string[] GetWorkspacePaths() => _workspaceService.WorkspacePaths.ToArray(); + public string GetWorkspaceRelativePath(ScriptFile scriptFile) => _workspaceService.GetRelativePath(scriptFile); public async Task ShowInformationMessageAsync(string message) diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index fce8cab20..e8f702321 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -104,6 +104,10 @@ public WorkspaceService(ILoggerFactory factory) #region Public Methods + public IEnumerable WorkspacePaths => WorkspaceFolders.Count == 0 + ? new List { InitialWorkingDirectory } + : WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath()); + /// /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. /// IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using @@ -358,15 +362,11 @@ public IEnumerable EnumeratePSFiles( int maxDepth, bool ignoreReparsePoints) { - IEnumerable rootPaths = WorkspaceFolders.Count == 0 - ? new List { InitialWorkingDirectory } - : WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath()); - Matcher matcher = new(); foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); } foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); } - foreach (string rootPath in rootPaths) + foreach (string rootPath in WorkspacePaths) { if (!Directory.Exists(rootPath)) { diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs index 1fff5eb3a..a25e4e8db 100644 --- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs @@ -80,6 +80,14 @@ internal static WorkspaceService FixturesWorkspace() }; } + [Fact] + public void HasDefaultForWorkspacePaths() + { + WorkspaceService workspace = FixturesWorkspace(); + string actual = Assert.Single(workspace.WorkspacePaths); + Assert.Equal(workspace.InitialWorkingDirectory, actual); + } + // These are the default values for the EnumeratePSFiles() method // in Microsoft.PowerShell.EditorServices.Workspace class private static readonly string[] s_defaultExcludeGlobs = Array.Empty(); From f6245f4bbd870432c30a2cb3b8db7e67e6d56067 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 22 Aug 2023 12:05:23 -0700 Subject: [PATCH 4/4] Use separate overload instead of optional argument So as to now add a binary breaking change. Co-authored-by: Patrick Meinecke --- .../Extensions/EditorWorkspace.cs | 7 ++++++- .../Extensions/IEditorOperations.cs | 8 +++++++- .../Services/Extension/EditorOperationsService.cs | 4 +++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 46930cda6..b01c6eca7 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -39,11 +39,16 @@ public sealed class EditorWorkspace #region Public Methods // TODO: Consider returning bool instead of void to indicate success? + /// + /// Creates a new file in the editor. + /// + public void NewFile() => editorOperations.NewFileAsync(string.Empty).Wait(); + /// /// Creates a new file in the editor. /// /// The content to place in the new file. - public void NewFile(string content = "") => editorOperations.NewFileAsync(content).Wait(); + public void NewFile(string content) => editorOperations.NewFileAsync(content).Wait(); /// /// Opens a file in the workspace. If the file is already open diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index c349bcae1..3ec33ebc6 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -38,12 +38,18 @@ internal interface IEditorOperations /// The resolved file path. string GetWorkspaceRelativePath(ScriptFile scriptFile); + /// + /// Causes a new untitled file to be created in the editor. + /// + /// A task that can be awaited for completion. + Task NewFileAsync(); + /// /// Causes a new untitled file to be created in the editor. /// /// The content to insert into the new file. /// A task that can be awaited for completion. - Task NewFileAsync(string content = ""); + Task NewFileAsync(string content); /// /// Causes a file to be opened in the editor. If the file is diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index 4ff235d94..7a7c6e6e7 100644 --- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -121,7 +121,9 @@ public EditorContext ConvertClientEditorContext( clientContext.CurrentFileLanguage); } - public async Task NewFileAsync(string content = "") + public async Task NewFileAsync() => await NewFileAsync(string.Empty).ConfigureAwait(false); + + public async Task NewFileAsync(string content) { if (!TestHasLanguageServer()) {